@leash/buyer-kit wraps @leash/core into the smallest possible “scriptable buyer”. One call, one chained receipt, one real x402 SPL transfer.
import { createBuyer } from '@leash/buyer-kit';
import { createKeyPairSignerFromBytes } from '@solana/kit';
import fs from 'node:fs';

const signer = await createKeyPairSignerFromBytes(
  new Uint8Array(JSON.parse(fs.readFileSync(process.env.LEASH_BUYER_SECRET_KEY!, 'utf8'))),
);

const buyer = createBuyer({
  agent: '<Core asset mint of your buyer agent>',
  rules: {
    v: '0.1',
    budget: { daily: '10', perCall: '0.01', currency: 'USDC' },
    hosts: { allow: ['api.example.com'] },
    triggers: [{ type: 'interval', seconds: 30 }],
  },
  signer, // Solana Kit TransactionPartialSigner
  networks: ['solana-devnet'], // CAIP-2 alias; mainnet works the same
  rpcUrl: 'https://api.devnet.solana.com',
  /**
   * When the seller advertises multiple `accepts[]` mints (e.g. USDC + USDG),
   * pick which one to pay with. Under the hood this becomes `preferredAsset`
   * on `createSvmBuyerFetch` — **strict**: if the seller does not offer this
   * mint, the fetch throws before settling (receipt `reason` begins with
   * `preferred_asset_unavailable`). There is no silent fallback to another
   * stablecoin.
   */
  preferredCurrency: 'USDC',
  onReceipt: async (r) => {
    await fetch(`http://localhost:8787/a/${r.agent}/receipts`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(r),
    });
  },
});

const { response, receipt } = await buyer.fetch('https://api.example.com/quote', {
  method: 'POST',
  body: JSON.stringify({ pair: 'SOL/USD' }),
  headers: { 'content-type': 'application/json' },
});

What happens on each call

  1. Policy gate (evaluate) — host allow-list, per-call/daily ceilings, pause check. A deny short-circuits and emits a deny receipt; no network traffic, no spend.
  2. paidFetch = wrapFetchWithPayment from @x402/fetch, registered with LeashDelegateExactSvmScheme (when sourceTokenAccount is set) or LeashExactSvmScheme (owner mode) from @leash/core. Both use the exact scheme id and are wire-compatible with every x402 facilitator. The first request is normal; on 402 Payment Required the wrapper:
    • Decodes PAYMENT-REQUIRED to the seller’s accepts[].
    • Picks the entry matching preferredCurrency (by mint address). Throws preferred_asset_unavailable if the seller doesn’t offer it.
    • Builds the Solana transaction: [setLimit, setPrice, sellerAtaCreate, feeVaultAtaCreate?, TransferChecked(seller), TransferChecked(fee)?, memo]. The two idempotent ATA-create instructions are always included so first payments on a fresh mint (e.g. USDG on devnet) succeed without manual setup — the facilitator pays the ATA rent.
    • Partially signs as the buyer delegate, then replays the request with PAYMENT-SIGNATURE.
  3. Receipt finalization (finalizeReceipt) — hashes the request, pulls tx_sig + payment_requirements_hash from the seller’s PAYMENT-RESPONSE header, chains against prev_receipt_hash, and returns a fully-formed ReceiptV1 stamped with the settled mint (USDC, USDG, etc.). On a failed 402 the receipt’s price reflects the mint the buyer attempted to pay with, not the seller’s primary.

Browser usage (Privy / wallet-adapter)

Provide any TransactionPartialSigner from @solana/kit. The web playground adapts a Privy embedded wallet via apps/web/lib/privy-svm-signer.ts — copy that file as a starting point for your own dApp.

Receipts by default

You no longer need to pass onReceipt — when LEASH_API_URL / LEASH_API_KEY (or LEASH_RUNNER_URL) are set in the environment, createBuyer resolves a fan-out sink that POSTs every receipt to each destination. Pass onReceipt: false to disable forwarding entirely, or receipts: { runnerUrl, apiUrl, apiKey, fetch } to configure explicitly. See Receipts by default.

API

type ReceiptForwardConfig = {
  runnerUrl?: string;
  apiUrl?: string;
  apiKey?: string;
  fetch?: typeof globalThis.fetch;
};

type BuyerConfig = {
  agent: string;
  rules: RulesV1;
  signer: ClientSvmSigner;
  networks?: ('solana-mainnet' | 'solana-devnet' | string)[];
  rpcUrl?: string;
  /** Override or disable the default receipt fan-out. */
  receipts?: ReceiptForwardConfig;
  /**
   * Optional. The agent treasury **SPL token account** (ATA) to spend from,
   * returned by `setSpendDelegation` from `@leash/registry-utils`. Its mint
   * must match the asset the seller will demand **and** the stable you intend
   * to pay with (e.g. pass the treasury's USDG ATA when
   * `preferredCurrency: 'USDG'`). When set, every settled call signs as the
   * SPL **delegate** of this account. Recommended for production — see
   * [Fund an agent](/guides/fund-an-agent).
   */
  sourceTokenAccount?: string;
  /**
   * Preferred settlement stable: `'USDC' | 'USDT' | 'USDG'`. Resolved to a
   * mint on the first entry in `networks` and passed to the x402 client
   * selector. Omitted = default selector (first compatible `accepts[]`).
   */
  preferredCurrency?: 'USDC' | 'USDT' | 'USDG';
  /**
   * Override the default x402 facilitator URL. Defaults to
   * `devnet-facilitator.leash.market` on devnet and `facilitator.leash.market` on
   * mainnet; the env var `LEASH_FACILITATOR_URL` overrides both. Other
   * facilitators (svmacc.tech, payai.network, self-hosted) are fully supported.
   */
  facilitator?: string;
  fetch?: typeof globalThis.fetch; // injection point for tests
  /**
   * Receipt sink. Function = explicit handler. `false` = opt out of all
   * forwarding (including environment defaults). Omitted = use the
   * default fan-out resolved from `receipts` / env vars.
   */
  onReceipt?: ((r: ReceiptV1) => void | Promise<void>) | false;
};

type BuyerCallResult = {
  response: Response;
  receipt: ReceiptV1;
  /** Price the seller demanded (parsed from PAYMENT-REQUIRED on a 402). */
  quotedPrice?: ReceiptV1['price'];
  /** Reason the call did not settle (e.g. "insufficient_funds"). */
  failureReason?: string;
};
When sourceTokenAccount is set the buyer-kit registers LeashDelegateExactSvmScheme from @leash/core instead of the vanilla @x402/svm exact scheme — wire-compatible with every facilitator (the on-chain instruction is still a plain SPL TransferChecked), but with the agent treasury as the source instead of the executive’s own ATA.

Automatic failure classification

When the seller returns 402 without settling, the kit reads the on-chain state of the source token account and stamps the receipt’s reason with one of:
ReasonMeaningFix
preferred_asset_unavailableYou set preferredCurrency but the seller’s accepts[] omits that mint.Pick a currency the link advertises, or ask the seller to add it.
insufficient_balanceTreasury holds less than the seller demanded (for the quoted mint).Top up the agent’s treasury ATA for that mint.
insufficient_allowanceDelegate cap is below the demanded price.Re-run setSpendDelegation with a higher cap for that mint.
no_delegateThe treasury ATA exists but no SPL delegate is set.Run setSpendDelegation from @leash/registry-utils.
wrong_delegateThe set delegate is not the executive that signed.Re-approve with the current executive.
ata_missingThe agent has never held this mint.Send any amount of the mint to mint the ATA.
The full receipt reason takes the form <classified>: <seller breadcrumb> (e.g. insufficient_balance: transaction_simulation). UI panels match on the prefix; the suffix is purely diagnostic. The source ATA is resolved in this order:
  1. cfg.sourceTokenAccount if you pass it (fastest path).
  2. Derived from cfg.agent + quote.price.asset via deriveAgentTreasuryAta — kit-native PDA derivation that mirrors mpl-core’s findAssetSignerPda. Brand-new agents that have never been delegated still get the precise diagnostic.
Both paths require cfg.rpcUrl to be set so the kit can issue a single getAccountInfo against the source ATA. See Real x402 on Solana for the protocol-level walkthrough and Fund an agent for the wiring steps.

HTTP equivalent (polyglot SDKs)

The full createBuyer flow — quote, policy gate, prepare, execute, finalize, verify — is exposed as eight stateless REST endpoints under /v1/buyer/*. You stay in control of the signer; the API handles the seller proxy, receipt finalization, and watchlist enrollment. See Buyer endpoints for the complete surface, and POST /v1/buyer/payment/execute in particular for the convenience path that’s the closest equivalent to buyer.fetch(...).