The connector, by hand
Put your app behind it
Your own app behind a priced route, told who paid in three headers.
A route with a handler_url terminates there. The connector opens the sealed payload, makes exactly that HTTP request of your app, and seals the app's complete response back. Your app does not change, imports nothing, and never sees a payment.
[[routes]] prefix = "g.your.node.quotes" handler_url = "http://quotes:8080/" # your app, on the compose network price = 1000 # 0.001 USDC per request
That is everything your app needs from you. The rest of this page is what it will see.
What your app is handed
An ordinary HTTP request. Your app holds no key on the node's behalf and supplies nothing toward the packet's fulfilment; the connector derives that itself. So "the app answered" and "the packet was paid for" stay separable, and an app that knows nothing about payment cannot leak, forge or withhold one.
The one thing the connector adds is attribution, on a request it took the payment for itself:
| Header | Holds |
|---|---|
X-TOON-Payer | The paying channel. |
X-TOON-Amount | What this request was charged. On a route priced by size that is a figure your app's own table could not have known. |
X-TOON-Chain | The chain the channel settles on. |
Your app is free to ignore all three. They are handed over so it can log or rate-limit by payer if it wants to, not so it can decide anything about the payment. They are absent on a request this node did not take the payment for, so treat them as optional. Whatever a caller writes under those names is stripped before your app sees it. Trust them and do not re-check them: claim validation lives only in the connector.
What you can charge for
- You are paid for an answer, not the answer the caller wanted. A
404from your app is a real answer: it rides home on aFULFILLand costs the same as a200. Only unreachability or a refused target produces a reject. - The trailing slash on
handler_urlis load-bearing, and a request's target is resolved beneath the handler's path. An absolute path, a..segment, a scheme or an authority is refused (F00) before your app is touched. - A terminated route must carry a
price. Writeprice = 0if free is deliberate, because it is never silently free. A free route runs no claim gate and a caller needs no channel to use it. - One handler, one price. To charge differently for different work, expose a handler for each and give each its own route. To charge by size, give one route a
{ base, per_kib }schedule.
A worked example: the store
The store is a paid Arweave blob store, and its repository is kept as the reference for putting any app behind a node. Everything between the connector and the app is these two halves.
The connector's side, one route:
[[routes]] prefix = "g.toon.store" # the ILP address clients pay handler_url = "http://store:3300/store" # your backend; the path is literal price = 1000 # smallest unit of the token (0.001 USDC)
Your side: accept a POST, do the work, answer.
POST /store { "event": { ...a signed Nostr event... } } 200 { "accept": true, "txId": "8ZWWEDIHqnGcsP0KSpdPeNIDvwR9G2ntZH7Y2Y5SoFE" }
The store's whole seam with the connector is one file of about two hundred lines. Swapping the store for your own app is a three-line change to the route.
Running your app beside the node
With Docker Compose, your app is one more service on the same network, and handler_url names it by service name, as http://quotes:8080/ does in the config. Publish no port for the app: the connector is the only thing that should reach it.
If you would rather hand the whole thing to an agent, spaceturtle takes any image that listens on port 3100 and answers GET /health, runs it in a zone of its own with no published port, and puts the node in front of it at the price you approve.
Written from connector/docs/guide/put-your-app-behind-it.md and store/README.md.