Docs

Agents

spaceturtle

Callers

Payment channels

A channel you open without gas, pay from with signatures, and leave with your deposit.

A payment channel is an x402 batch-settlement channel: a deposit this client locks on chain, payable to one connector, that lets it pay that connector many times while touching the chain only to open, top up and leave. It is the only way the client pays.

npx toon-client channel open --deposit 100000   # once, no gas: 0.10 USDC into a channel
npx toon-client channel status                   # deposit, signed so far, available
  • Base: x402's own x402BatchSettlement contract.
  • Solana: the Solana Foundation's payment-channels program.

A channel is one-way, payer to connector. Requests are paid by vouchers signed against the channel. A voucher states the channel's cumulative total, so each one supersedes the last: losing one costs nothing, and replaying one gains nothing. Signing a voucher costs no gas. Opening a channel costs no native gas either. Leaving a channel does.

A route priced at zero needs no channel

Not every route costs money. A connector states a free route explicitly with price = 0; a route is never silently free. A free route runs no claim gate, so an unpaid request to it is simply routed. The client opens no channel, signs no voucher and touches no chain to use one, and send() returns a result with no claim rather than a zero-valued one.

const answer = await client.send('g.toon.relay.ephemeral', { body: 'hello' });
answer.fulfilled;      // true: the app answered
answer.claim;          // undefined: nothing was paid, so there is no receipt

Where the terms come from

Every fact needed to open a channel comes from the node's own self-description, which the client reads from GET /ilp, never from a preset: the network (CAIP-2), the token, the connector's receiving address, the withdrawal delay, and on Solana also the sponsor key, the minimum deposit and the sponsor endpoint. A 402 greeting carries the same facts in accepts[], priced for the route.

const description = await client.describe();
console.log(description.batchSettlements);   // one entry per chain the node is paid on

Opening costs no gas

The first paid send() opens a channel by itself, unless autoOpenChannel is false. You can also open one explicitly with npx toon-client channel open --deposit 1000000 or client.channel.open().

  • Base. The client signs a deposit authorization. An x402 facilitator submits it and pays the gas, so a wallet holding USDC and no ETH can open a channel. The facilitator is, in order: your facilitatorUrl (or --facilitator); the one the connector names in its terms, since the seller pays the gas as a cost of the sale; the devnet's own, onboard.devnet.toonprotocol.dev, on Base Sepolia only.
  • Solana. The client builds and signs the open, then posts it to the sponsorEndpoint the node publishes. The connector co-signs as fee payer, submits it, and pays the fee and the rent. The payer's USDC token account must already exist and hold the deposit, and the deposit must be at least the node's minDeposit. A refusal throws SponsorRefusedError, carrying the node's own reason.
  • Any ERC-20. A connector names how its token moves: eip3009 for USDC-style tokens, permit2 for any other ERC-20. For Permit2 the facilitator sponsors the one-time approval where it can, and otherwise your own ETH pays for it.

Who pays the gas

StepWho pays
Base deposit or top-upThe facilitator the connector names, at the seller's expense; or you, with --deposit-gas self from your own ETH, contacting no facilitator at all.
Permit2 approval (once, non-USDC tokens)The facilitator if it sponsors approvals for that token; otherwise your own ETH.
Solana openThe connector: the transaction fee and the rent.
Every voucherNobody. A signature.
Landing vouchers on chainThe connector, in batches.
Leaving the channelYou: Base Sepolia ETH or devnet SOL, the only time you need native gas.

--deposit-gas (depositGas) picks auto (the facilitator, else your own ETH), facilitator or self. Under auto, a deposit the facilitator refused is sent directly with the same single-use authorization, so it can never land twice.

Paying from the channel

Each paid packet carries one voucher, inside ILP, stating the new cumulative total. The connector accepts it only if it strictly exceeds the last voucher it accepted on that channel by at least the route's charge; otherwise it answers F03 and the client reprices from the watermark the refusal names. A voucher above what the channel holds is also F03: top up and send again. The library tops up on its own when a channel runs low; the CLI never does anything on chain as a side effect.

The watermark, and why the store must be durable

The connector remembers the highest cumulative amount it has accepted on each channel. Your side has to remember the same figure, plus the channel's config, which the chain never gives back. Both live in the channel store: ~/.toon/channels.json for the CLI, channelStore for the library. A client that lost its store asks POST /ilp/claim-state, an owner-authenticated read that cannot be replayed as a payment, and resumes from the connector's figure.

Without the channel's config you cannot leave the channel, and its deposit stays locked on chain. Keep the store, back it up, and never share one store between two processes paying the same connector.

Leaving

  • Base. initiateWithdraw, then after the channel's withdrawDelay (one day by default on TOON nodes) finalizeWithdraw. The connector watches for the first and lands its latest voucher at once, so what comes back to you is the deposit minus everything you signed for.
  • Solana. request_close, then after the grace period your withdrawal. The connector lands its latest voucher inside the grace period, distributes, and reclaims its rent.

Both steps need native gas from your own wallet. The CLI's channel subcommands drive them; the full channels document on GitHub has the exact commands, what the channel id is derived from, and the recovery procedure for a store that was lost.

Written from toon-client/docs/channels.md and connector/docs/operators/x402-batch-settlement.md.