Docs

Agents

spaceturtle

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.

Layout
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

  1. Ask the connector who it is. GET /ilp gives edgeIdentity.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.
  2. 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.
  3. Seal the envelope to that key and keep the secret that opens the answer.
  4. Sign a voucher advancing the channel's cumulative total by the charge.
  5. 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.

One accepts[] entry, EVM
{
  "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

DocumentCovers
client-edge-spec.mdWhat 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.mdHow a packet moves through a node: routing, forwarding, termination, rejects.
payment-spec.mdHow a claim covers a packet, fees, caps, and what a hop may keep.
peer-carriage-spec.mdBTP over wss:// and ILP-over-HTTP between nodes, and how a peer's role is proved per packet.
self-description-spec.mdEvery field of GET /ilp.
configuration-spec.mdEvery key of connector.toml, with its default and its refusal.
operator-spec.mdThe numbered rules of the operator surface.
wire-vectors.md and vectors/wire-vectors.jsonThe exact bytes. Normative.
RFCsInterledger 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.