Unidirectional payment channels on Solana: escrow a deposit, authorize cumulative spend with off-chain Ed25519 vouchers, then settle and distribute on-chain. A small Pinocchio program over SPL Token / Token-2022.
Status — live on mainnet: CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX
One on-chain open and one settle replace a transaction per payment. The payer escrows a ceiling; the merchant claims only what off-chain vouchers authorize; the payer recovers the rest. That makes metered, streamed, or many-small payments viable where settling every request on-chain is too slow and too expensive.
flowchart LR
A["open<br/>(escrow a deposit)"] --> B["off-chain vouchers<br/>(cumulative spend)"]
B --> C["settle<br/>(advance settled amount)"]
C --> D["distribute<br/>(payee + splits, refund payer)"]
D --> E["reclaim<br/>(recover channel rent)"]
stateDiagram-v2
[*] --> Open: open
Open --> Open: settle / top_up / distribute (partial)
Open --> Sealed: settle_and_seal (cooperative)
Open --> Closing: request_close (forced)
Closing --> Sealed: settle_and_seal (mid-grace) / seal (after grace)
Sealed --> Distributed: distribute (pay out, close escrow)
Distributed --> [*]: reclaim (after epoch window)
Sealed --> [*]: distribute (fast path, window already elapsed)
Vouchers are signed off-chain (Ed25519) and carry a cumulative amount, so a newer voucher supersedes older ones and the program never settles more than the deposit. distribute / withdraw_payer move the settled funds out and refund the unspent remainder the moment the channel is sealed — no payout ever waits. The channel account itself is then fully deallocated (directly, or by a later reclaim once the epoch window elapses), returning 100% of its rent: a closed channel leaves nothing on chain.
This program is the on-chain settlement layer behind two pay.sh payment primitives. Both deposit a ceiling here, meter off-chain, and settle the actual amount on this program:
- x402
upto— a single metered call: the operator settles one voucher for the actual amount and refunds the rest. - MPP
session— a streamed channel: many cumulative vouchers, settled once when the session idle-closes.
See Payment channels on pay.sh for the protocol handshakes and when to pick each.
| Instruction | Role |
|---|---|
open |
Create the channel PDA and escrow the deposit. |
settle |
Advance the on-chain settled amount from a signed voucher. |
settle_and_seal |
Settle a final voucher and seal in one step (cooperative). |
top_up |
Add funds to an open channel. |
request_close |
Payer-initiated forced close — starts the grace period. |
seal |
Seal a forced-closing channel once the grace period elapses. |
distribute |
Pay the payee and any split recipients; refund the payer; close the escrow. |
withdraw_payer |
Payer recovers the unspent remainder. |
reclaim |
Deallocate a distributed channel and recover its rent (batchable). |
just setup
just build-program
just generate-client
just test-programCluster builds (just build-mainnet-beta, just build-devnet, …) require that cluster's real TREASURY_OWNER in program/payment_channels/src/constants.rs and refuse to compile with the placeholder. No production keypair is committed — pass the program-id keypair explicitly when deploying.
- State machine
- HTTP protocol
- Instruction reference
- Generated clients: TypeScript, Rust.
MIT. See LICENSE.