Every x402 settlement routed through a Leash facilitator is a single buyer-signed Solana transaction with up to seven instructions:
#InstructionWho pays rent
0SetComputeUnitLimit
1SetComputeUnitPrice
2CreateAssociatedTokenAccountIdempotent — seller’s payTo ATAFacilitator
3CreateAssociatedTokenAccountIdempotent — fee vault ATA (only when a fee leg is present)Facilitator
4TransferChecked(mint, amount) — seller legBuyer delegate
5TransferChecked(mint, fee) — protocol fee leg (only when extra['leash.fee'] is present)Buyer delegate
6Memo
Instructions 2 and 3 are idempotent: if the ATA already exists they are a no-op (small CU cost, no rent). This lets the first ever payment of a new mint (e.g. devnet USDG) land without any manual ATA setup — the facilitator auto-provisions both destinations and pays the rent. The two TransferChecked legs share the same buyer signature, blockhash, and tx_sig. Either both land or neither does. This is the only economic hook in the entire stack. There is no per-call subscription, no facilitator markup, no per-agent infra fee, and no spread on simulation. The 1% fee leg is the revenue model for the hosted Leash facilitators, and the same primitive anyone running @leash/facilitator can opt into.

The numbers

FieldValueWhere it lives
Rate100 bps (1.00%)LEASH_FEE_BPS env on facilitator + API
RoundingCeiling, in atomic unitscomputeFeeAtoms in @leash/core/fees
Mainnet authority3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7WLEASH_FEE_AUTHORITY_MAINNET (override only if self-hosting your own facilitator)
Devnet authority3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7WLEASH_FEE_AUTHORITY_DEVNET
Live snapshotGET /v1/health .protocol_fee blockAPI + @leash/facilitator /health
The hosted Leash mainnet and devnet facilitators always serve the defaults. Override the env only if you are operating your own facilitator with your own treasury — in that case the buyer SDK treats your facilitator as Leash-flavoured iff you publish a protocol_fee block on /health.

Math worked example

A buyer calls a 0.012345 USDC endpoint (atoms 12345, USDC has 6 decimals):
amount      = 12_345        atoms (= 0.012345 USDC, what the seller quoted)
bps         = 100           (1.00%)
fee         = ceil(12_345 * 100 / 10_000)
            = ceil(123.45)
            = 124           atoms (= 0.000124 USDC)
gross       = amount + fee
            = 12_469        atoms (= 0.012469 USDC, what the buyer signs)
The buyer signs one transaction with two TransferChecked instructions: 12 345 atoms to the seller, 124 atoms to Leash-fee-authority's USDC ATA. The seller’s receipt records:
{
  "kind": "earn",
  "price": {
    "amount": "12345",
    "currency": "USDC",
    "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "fee": "124",
    "gross": "12469",
    "feeBps": 100,
    "feeAuthority": "3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7W"
  },
  "tx_sig": "..."
}
The buyer’s spend receipt carries the same price block (modulo kind: 'spend').

Wire shape — extra['leash.fee']

Sellers advertise the fee rate in the 402’s accepts[] so buyers can verify it before signing. The shape is intentionally minimal — the dynamic fields (feeAtomic, grossAtomic, feeDestination) are derived on demand by both sides from the static fields below, so the buyer and facilitator always agree on the destination ATA without having to coordinate ahead of time.
{
  "scheme": "exact",
  "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
  "asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
  "payTo": "<seller payTo ATA>",
  "amount": "12345",
  "extra": {
    "leash.fee": {
      "v": "1",
      "bps": 100,
      "feeAuthority": "3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7W",
    },
  },
}
Buyers parse it with parseLeashFeeExtra from @leash/core and resolve the per-request triple (bps, feeAtomic, feeDestination) via computeLeashFeeForRequirements. Facilitators run the same function over the same inputs to verify the buyer’s transaction matches. If extra['leash.fee'] is absent, both sides treat the settlement as vanilla x402 (single-leg, no fee enforcement). This keeps non-Leash sellers and non-Leash facilitators interoperable.

Verifying the live rate

Before any UI signs a buyer’s wallet, fetch the current rate from the facilitator (or the API) — the rate is one source of truth, surfaced identically on both:
$ curl -sS https://api.leash.market/v1/health | jq .protocol_fee
{
  "bps": 100,
  "pct": "1.00%",
  "authorities": {
    "solana-mainnet": "3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7W",
    "solana-devnet":  "3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7W"
  }
}

$ curl -sS https://devnet-facilitator.leash.market/health | jq .protocol_fee
{ "bps": 100, "enforcement": "enforce", "authority": "3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7W" }
The web playground polls this on boot and refuses to construct a TransferChecked if the buyer-side computed feeAuthority disagrees with what the facilitator publishes.

Budgeting allowances and treasuries

Because the buyer signs gross, every spend allowance and every treasury balance has to be sized in gross, not amount. The two common foot-guns:
  • SPL Approve (executive allowance). A 5 USDC Approve only buys ~4.95 USDC of seller-quoted endpoints — the last fee leg pushes you over the cap and the settlement fails with insufficient_allowance. Either gross-up yourself with applyFeeGrossUp from @leash/core, or pass pad_for_protocol_fee: true on POST /v1/agents/{mint}/delegation/prepare — the response echoes fee_padding_atoms so you can audit.
  • Treasury balance. Same reasoning at the SPL balance level. Top up 5.05 USDC (atoms 5_050_000) if you want to comfortably consume 5 USDC of seller-quoted endpoints. The web playground’s “Provision stable ATAs” + “Set allowance” buttons both do the gross-up automatically.
Code path:
import { applyFeeGrossUp, resolveLeashFeeBps } from '@leash/core';

const sellerNetAtoms = 5_000_000n; // 5 USDC
const { fee, gross } = applyFeeGrossUp(sellerNetAtoms, resolveLeashFeeBps());
// fee   = 50_000n   (0.05 USDC)
// gross = 5_050_000n (5.05 USDC) — use this to size SPL Approve / fund treasury

What the facilitator does

@leash/facilitator registers two custom schemes — LeashExactSvmFacilitator (x402 v2) and LeashExactSvmFacilitatorV1 (legacy v1) — instead of the upstream @x402/svm defaults. On every POST /verify call they:
  1. Walk past leading CreateAssociatedTokenAccountIdempotent instructions (up to two — one for the seller’s payTo ATA, one for the fee vault ATA). Each is validated: payer must be the facilitator’s fee-payer, mint and token program must match the transfer leg, and each target ATA must be either the seller’s payTo or the fee authority’s ATA for the same mint. Unknown ATA creates are rejected.
  2. Identify the seller TransferChecked immediately after any ATA creates. Confirm it debits the buyer’s source ATA and credits the seller’s payTo ATA for the advertised amount on the advertised mint.
  3. If extra['leash.fee'] is present, confirm a second TransferChecked follows: same source ATA, same mint, targets the feeAuthority’s ATA, exact feeAtomic as derived from amount × bps.
  4. Reject (enforce), warn (warn), or ignore (off) per LEASH_FEE_ENFORCE. The hosted facilitators run enforce; warn is for staged rollouts.
  5. Simulate the fully co-signed transaction against the RPC to catch balance / blockhash problems before broadcast.
Settlement (/settle) adds the facilitator’s signature as fee payer and broadcasts. The facilitator pays all SOL transaction fees and ATA rent — buyers only spend stables.

What the indexer does

Every Leash fee ATA on every supported stable mint is added to the indexer’s watchlist at startup (seedLeashFeeWatchlist in apps/api/src/indexer/leash-fee-watchlist.ts, kind leash_fee_ata). Two paths emit protocol.fee.collected events:
  • Receipt-side. Whenever a settled earn receipt with price.fee is ingested via POST /v1/receipts/{agent}, /x/{id} (paywall), or the receipt-pull worker, emitProtocolFeeEvent fires once, idempotent on (network, receipt_hash).
  • On-chain fallback. When the indexer sees a positive tokenBalanceDeltas to the fee authority on a watched ATA, it emits the same event with metadata.source = 'on_chain'. This catches the rare case of a fee landing without a receipt (e.g. a buyer that never POSTed the spend receipt).
The explorer’s “Protocol fees collected” panel sums these by mint per network, and the receipt detail page renders Net (seller) / Protocol fee / Gross (buyer) / Fee authority.

Self-hosting @leash/facilitator with your own fee

The fee primitive is generic — anyone running their own facilitator can charge their own protocol fee against their own treasury, or disable the fee entirely while still benefiting from the rest of the verification stack:
EnvDefaultEffect
LEASH_FEE_BPS100Fee rate in basis points. Set to 0 to disable.
LEASH_FEE_ENFORCEenforceenforce rejects bad/missing fee legs; warn logs them; off skips verification.
LEASH_FEE_AUTHORITY_MAINNET3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7WPubkey that receives the mainnet fee leg.
LEASH_FEE_AUTHORITY_DEVNET3DdcJkvjW7KLtMeko3Zr57jEJWhqRHuPsEBFm1XJYh7WSame, for devnet.
Run @leash/facilitator with the env you want, point sellers at LEASH_FACILITATOR_URL=https://your-host, and the chain of effects above (sellers stamp your feeAuthority → buyers verify against your /health → indexer watchlists your ATAs → explorer aggregates) all wires up automatically.

SDK reference

import {
  applyFeeGrossUp,
  buildLeashFeeExtra,
  computeFeeAtoms,
  computeLeashFeeForRequirements,
  parseLeashFeeExtra,
  resolveLeashFeeAuthority,
  resolveLeashFeeBps,
  type LeashFeeExtra,
  type ResolvedLeashFee,
} from '@leash/core';
  • resolveLeashFeeBps() — current fee rate (env-overridable).
  • resolveLeashFeeAuthority(network) — fee owner pubkey for 'mainnet' | 'devnet'.
  • computeFeeAtoms(amount, bps) — pure, ceiling-rounded.
  • applyFeeGrossUp(amount, bps?) — returns { fee, gross }.
  • buildLeashFeeExtra({ network, bps?, authority? }) — synchronous, returns the wire shape sellers stamp on extra['leash.fee'].
  • parseLeashFeeExtra(extra) — parses the wire shape, returns null on absence/malformation.
  • computeLeashFeeForRequirements({ network, asset, tokenProgram, amount, extra }) — async, derives (bps, feeAtomic, grossAtomic, feeDestination) for a specific paymentRequirements entry. Used by both buyer scheme and facilitator verifier.
The SDK helpers are pure (modulo ATA derivation, which uses @solana-program/token / @solana-program/token-2022), so you can unit-test integrations without booting any Solana RPC.

See also

  • Run a facilitator — boot your own and configure the fee env.
  • Monetise an API — where the fee fits in the end-to-end seller flow.
  • Explorer tracking — how protocol.fee.collected events surface in the UI.
  • Receipt format — the canonical price.fee / price.gross / price.feeBps / price.feeAuthority fields on every ReceiptV1.