@leash/buyer-kit and
@leash/seller-kit.
The cast
| Role | What it signs | Holds funds in | Pays gas |
|---|---|---|---|
| Executive (Privy wallet, Node keypair, TEE) | The SPL transfer (as delegate of the buyer agent treasury) | — | Pays the buyer-side fee on the transfer |
| Buyer agent (Core asset) | Nothing — passive owner | Asset Signer PDA → SPL token ATA | — |
Seller (@leash/seller-kit middleware) | Nothing | Asset Signer PDA (payTo) | — |
Facilitator (devnet-facilitator.leash.market devnet, facilitator.leash.market mainnet, or any compatible host) | Nothing | — | Pays the network fee to broadcast payTo |
TransferChecked — see “Agent-funded transfers”
below.
Agent-funded transfers (the delegate trick)
Out-of-the-box@x402/svm’s ExactSvmScheme derives the source ATA from
the signer’s wallet — it assumes the signer also owns the funds. That’s
wrong for Leash agents: funds live on the agent treasury PDA, but the
PDA can only sign through mpl-core::Execute, which no production
facilitator wraps for you.
Leash sidesteps this with a one-time delegation:
- Owner runs
setSpendDelegation(@leash/registry-utils). It wraps an SPLApproveinmpl-core::Execute, signed by the owner; the program debits the asset signer PDA via CPI and authorises the executive as the ATA’s SPL delegate for up todelegated_amountunits. - Buyer-kit, when constructed with
sourceTokenAccount, registersLeashDelegateExactSvmScheme(@leash/core) instead ofExactSvmScheme. The on-chain transfer is still a vanillaTransferChecked— only thesourcefield switches from “executive’s ATA” to “agent treasury’s ATA”, andauthorityis the executive (signing as delegate). Every facilitator Leash tested settles it without modification. - The SPL token program automatically reduces
delegated_amountafter each transfer; once exhausted the next call returns402 + insufficient_funds. Re-approve to top up; revoke to lock down.
The headers
x402 uses three HTTP headers, all base64-encoded JSON:PAYMENT-REQUIRED (server → client, 402)
The seller’s offer. Leash sends an accepts[] with one object per accepted
mint on the route (primary currency plus any accepts_currencies from the
payment link or createSeller route config). Each row carries the same atomic
amount but a different asset mint. Example payload (decoded, two stables):
Buyer mint choice
@leash/buyer-kit maps preferredCurrency ('USDC' | 'USDT' | 'USDG')
to a concrete mint address for the buyer’s configured cluster, then passes
it to the x402 client selector as preferredAsset. The selector finds the
matching accepts[i] entry by mint and pays that one.
Strict, no silent fallback. If the seller’s accepts[] does not contain
the chosen mint, the fetch throws with
preferred_asset_unavailable: requested <mint>; seller offers <other mints>
before building any transaction. The ReceiptV1 records the thrown reason.
Receipt reflects the chosen mint. On a failed 402, the buyer-kit
decodes the payment-required header and prefers the entry matching the
buyer’s mint — so a failed USDG attempt stamps currency: "USDG" and
asset: 4F6PM96… on the receipt, not the seller’s primary (often USDC).
Validate the seller’s accepts[] before firing via fetchPaymentLinkMeta
(@leash/core) or the discovery JSON at the link URL.
Notes:
networkis the CAIP-2 id (e.g.solana:EtWT…for devnet,solana:5eyk…for mainnet). Thesolana-devnet/solana-mainnetaliases used by Leash configs map to these identifiers internally.amountis in the smallest unit of the SPL token atasset(USDC has 6 decimals →"1000"= 0.001 USDC).payTois the Asset Signer PDA of the seller’s Core asset, not a wallet pubkey.
PAYMENT-SIGNATURE (client → server, retry)
The buyer’s partially-signed transaction + a copy of the accepted
requirement (the middleware does a deep equality check). @leash/core
builds and signs this automatically via LeashDelegateExactSvmScheme
(agent treasury / delegate flow) or LeashExactSvmScheme (executive’s
own wallet). Both are wire-compatible with the exact scheme — the
on-chain instruction is always a plain TransferChecked.
PAYMENT-RESPONSE (server → client, 200)
Settlement details emitted after the facilitator broadcasts the
transfer. Includes tx_sig and payment_requirements_hash (SHA-256 of
the canonical accepts[i] the buyer paid against). Leash copies both
into the corresponding ReceiptV1 so explorers can chain the buyer’s
spend to the seller’s earn.
Swapping facilitators
Any HTTPS x402 facilitator works. The Leash defaults:| Network | Default facilitator | Notes |
|---|---|---|
solana-devnet | https://devnet-facilitator.leash.market | Leash-operated devnet, gas-sponsored, Token-2022 supported. |
solana-mainnet | https://facilitator.leash.market | Leash-operated mainnet, gas-sponsored. |
devnet-facilitator.leash.market (devnet), facilitator.leash.market (mainnet), community hosts, self-hosted @leash/facilitator.
Override per-process by setting LEASH_FACILITATOR_URL, or per-seller via
facilitator: '…':
accepts[], the buyer chooses which row to pay (via
preferredCurrency / preferredAsset). The buyer-kit records the facilitator
URL on every receipt for explorer cross-checking.
Choosing a network
| Alias | CAIP-2 | Default USDC mint |
|---|---|---|
solana-mainnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
solana-devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
Devnet faucets
Buyers need devnet SOL and whichever stable they will pay with (commonly USDC). Two faucets we recommend:- SOL: faucet.solana.com
- USDC: faucet.circle.com (mint
4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDUto the treasury ATA for agent-funded flows)

