Reference
Protocol and specs
The packet, the greeting and the self-description, and where the specs live.
The wire is defined by the Rust connector, not by any client. Its committed wire vectors are the normative cross-repository contract; the client, the rig and the swap replay them as their own conformance suites. Vectors are normative; prose is not. This page is the map to the specs, with the three things most readers come for: the packet, the greeting, and the self-description.
curl -s https://proxy.ario.devnet.toonprotocol.dev/ilp | jq # a node's self-description, free npx toon-client price g.toon.store https://proxy.ario.devnet.toonprotocol.dev # one route's price
The packet
A packet is an OER envelope, gift-wrapped to the identity of the connector that terminates the destination, carrying a signed voucher that pays for it. The answer's fulfilment is derived from the secret inside that wrap, so the sender can check a delivery end to end. Nothing on the wire is HTTP text.
The packet is ILPv4's semantics in TOON's encoding. The three type bytes, the field order and meanings, and the F/T/R reject taxonomy are RFC 0027's. The bytes are not, deliberately, in exactly four places: no outer type-length wrapper; amount is a VarUInt, not a fixed 8-byte integer; expiresAt is a 19-byte GeneralizedTime; and there is no executionCondition. A one-byte greeting flag sits where the 32-byte condition would be. So an off-the-shelf ILPv4 encoder does not produce a packet this edge accepts.
PREPARE 0x0c ‖ VarUInt(amount) ‖ expiresAt(19) ‖ greeting(1)
‖ VarOctetString(destination) ‖ VarOctetString(data)
FULFILL 0x0d ‖ fulfilment(32) ‖ VarOctetString(data)
REJECT 0x0e ‖ code(3) ‖ VarOctetString(triggeredBy)
‖ VarOctetString(message) ‖ VarOctetString(data)Canonical VarUInt, and no trailing bytes: a packet with either is refused rather than read leniently. The voucher travels inside ILP in the ILP-Payment-Channel-Claim header over HTTP, or a payment-channel-claim protocolData entry over BTP. The exact bytes are claim_voucher in the vectors.
Forming one by hand
- Ask the connector who it is.
GET /ilpgivesedgeIdentity.publicKey, an uncompressed secp256k1 key. There is no default to fall back on: sealing to the wrong key is a confidentiality failure that merely presents as an undeliverable packet. - Ask the route what it costs. The route table is the price list. To learn what a path beyond this connector costs, send a probe: a packet sent in the expectation that it will be refused, so the refusal can state
accumulatedCost. A probe traverses for free, so it is accepted only from a sender the connector recognises by a channel, within a rate limit, and is never delivered to a terminating route. - Seal the envelope to that key and keep the secret that opens the answer.
- Sign a voucher advancing the channel's cumulative total by the charge.
- Send both as one
POST /ilp, or one BTP message, and check the fulfilment against your own derivation.
The greeting
A request to a priced route with no voucher is answered with the route's terms: HTTP 402 on the client edge, F06 over BTP. The body is an x402 v2 PaymentRequired document. accepts[] holds one batch-settlement entry per chain the node settles on, and nothing else. TOON's own terms ride in extensions.toon.info: the quoted amount, price, pricePerKib on a size-priced route, ilpAddress, endpoint, and the node's addresses.
{
"scheme": "batch-settlement",
"network": "eip155:84532",
"amount": "1000",
"asset": "0x0C996d7c934c79a6255254875607Fe69df25C0E1",
"payTo": "0x…",
"extra": {
"receiverAuthorizer": "0x…",
"withdrawDelay": 86400,
"name": "USDC",
"version": "2",
"assetTransferMethod": "eip3009",
"facilitator": "https://onboard.devnet.toonprotocol.dev"
}
}The greeting answers a well-formed packet that carries no voucher. A bare curl -X POST /ilp with no packet body is not that; it gets 400 invalid packet type byte. To read a price without forming a packet, use GET /ilp | jq .routes or npx toon-client price <destination> <url>.
Each entry is valid x402: a CAIP-2 network, the asset, payTo set to the node's settlement address, and an extra holding everything a stock x402 client needs to open a channel the node will accept. Two things a stock x402 client can get wrong: the node refuses a zero payerAuthorizer, and the deposit's own voucher must be at least one base unit. GET /ilp publishes the same facts under batchSettlements; the two are projections of one value and never disagree.
The self-description
GET /ilp, free and unauthenticated, answers with the node's ilpAddresses, httpEndpoint and btpEndpoint, peerCarriages, edgeIdentity, batchSettlements, voucherSigners, routes with prices, and supportedVersions. Fields a node has nothing to say about are omitted entirely, never sent empty. Peering shows a full one. A connector answers; it never announces.
The specs
| Document | Covers |
|---|---|
| client-edge-spec.md | What a client sends to POST /ilp and over BTP, the greeting (§1.4), the voucher (§1.3, §1.10), the claim-state read, the Solana sponsor endpoint (§1.11). |
| packet-flow-spec.md | How a packet moves through a node: routing, forwarding, termination, rejects. |
| payment-spec.md | How a claim covers a packet, fees, caps, and what a hop may keep. |
| peer-carriage-spec.md | BTP over wss:// and ILP-over-HTTP between nodes, and how a peer's role is proved per packet. |
| self-description-spec.md | Every field of GET /ilp. |
| configuration-spec.md | Every key of connector.toml, with its default and its refusal. |
| operator-spec.md | The numbered rules of the operator surface. |
| wire-vectors.md and vectors/wire-vectors.json | The exact bytes. Normative. |
| RFCs | Interledger underneath, and the places TOON chose differently. |
The decisions
The connector keeps its reasoning as status-tracked architecture decision records. The ones that shape everything above:
- ADR 0075: every channel is an x402 channel, and a peering is two of them. One claim scheme, on contracts TOON does not have to deploy.
- ADR 0076: the operator names the facilitator and pays its gas.
- ADR 0046: there is no discovery layer. A node answers for itself and announces nothing.
- ADR 0058: a peering is established from a URL.
- ADR 0063: the ILP packet is TOON's dialect, not RFC 0027's.
- ADR 0069: the execution condition is removed from the wire.
- The full index.
The relay and the NIPs
Agent nodes also carry a relay, a Nostr relay that speaks plain NIP-01 for reads and takes writes only through the node in front of it. Apps such as the store and the gas station answer NIP-90 Data Vending Machine jobs: kind:5094 for Arweave storage, kind:5095 for ArNS names, kind:5096 and kind:5098 for Solana and EVM gas. Protocol that agents need and no existing NIP covers is drafted in toon_cli's nips/.
Written from toon-client/docs/how-a-paid-packet-works.md, connector/docs/operators/x402-batch-settlement.md and the connector's docs/protocol/ index.