@leashmarket/buyer-kit wraps @leashmarket/core into the smallest possible “scriptable buyer”. One call, one chained receipt, one real x402 SPL transfer. Before paying a seller you can ask Leash for an identity trust verdict with the same checks agents use for automated payments:
import { verifyAgentIdentityDecision } from '@leashmarket/buyer-kit';

const decision = await verifyAgentIdentityDecision({
  request: {
    selector: { domain: 'seller.example' },
    intent: 'pay',
    capability: { slug: 'seller/quote-api', protocol: 'x402' },
    thresholds: { min_rating: 0.7, require_verified_domain: true },
  },
});

if (decision.verdict === 'deny') throw new Error('seller identity denied');
import { createBuyer } from '@leashmarket/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',
  identity: {
    selector: { domain: 'seller.example' },
    capability: { slug: 'seller/quote-api', protocol: 'x402' },
    thresholds: { require_verified_domain: true },
  },
  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' },
});
When identity is set, createBuyer calls POST /v1/identity/verify before signing. deny blocks payment and emits a denied spend receipt. warn is allowed by default; set blockOnWarn: true when the caller wants warnings to stop settlement too.

x402 and MPP (dual protocol)

createBuyer and buyer.fetch auto-detect whether the seller speaks x402 (402 + PAYMENT-REQUIRED) or MPP (402 + application/problem+json) on the first unpaid response. You do not pass a protocol flag on the buyer. MPP settlements yield ReceiptV02 (protocol: "mpp", facilitator metadata). When forwarding or storing receipts, prefer parseReceiptAny from @leashmarket/schemas so both v0.1 and v0.2 lines work. See MPP on Solana (Leash) and Receipt V2.

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 @leashmarket/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/playground/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

import type { ReceiptAny, ReceiptV1 } from '@leashmarket/schemas';

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 `@leashmarket/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
   * `facilitator-devnet.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: ReceiptAny) => void | Promise<void>) | false;
};

type BuyerCallResult = {
  response: Response;
  /** Alias: `ReceiptAny` — x402 yields `ReceiptV1`, MPP yields `ReceiptV02`. */
  receipt: ReceiptAny;
  /** 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 @leashmarket/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 @leashmarket/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 (the buyer-kit pre-flight derives the ATA under the right program — spl-token for USDC/USDT, spl-token-2022 for USDG — so this is a real “treasury empty for this mint” signal, not a Token-2022 mismatch).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(...).