Buyer clients

Buyer core

Discover gateway resources and pay with x402 from Python or TypeScript, within spending limits the model cannot change.

The buyer core discovers gateway resources and pays for them within spending limits you set. The Python and TypeScript implementations both pass the conformance suite and check those limits before signing a payment.

LanguagePackage
Pythonagent-commerce-client
TypeScript@devlab.group/agent-commerce-client

The core talks to the gateway only through its public HTTP contract: GET /api/resources, POST /api/resources/{id}/invoke and the x402 v2 headers PAYMENT-REQUIRED, PAYMENT-SIGNATURE and PAYMENT-RESPONSE. Merchants need no gateway changes to support these clients.

Framework packages map resources to tools and approval settings; the core handles payments.

Install

bash
pip install agent-commerce-client

Python 3.10 or newer. Payments use the official x402 package.

Quick start

python
import os

from agent_commerce_client import AgentCommerceClient, AgentCommerceError
from eth_account import Account

client = AgentCommerceClient(
    gateway_url="https://merchant.example.com",
    signer=Account.from_key(os.environ["BUYER_PRIVATE_KEY"]),
    networks=["eip155:84532"],  # Base Sepolia
    max_per_call="0.05",  # USDC
    max_per_day="1.00",  # USDC per UTC day
)

for tool in client.list_tools():
    print(tool.name, "-", tool.description)

try:
    result = client.invoke("report", {})
    print(result.status, result.body, result.payment)  # payment: network, transaction hash, payer
except AgentCommerceError as error:
    print(error.code, error.message, error.status)

await client.alist_tools() and await client.ainvoke(...) are the async forms. The sync methods use the same implementation and work even when an event loop is already running.

Read BUYER_PRIVATE_KEY in your application, create an account object, and pass that object to the client. Raw private keys are refused. Use a dedicated wallet funded to just above the daily limit.

Configuration

PythonTypeScriptRequiredMeaning
gateway_urlgatewayUrlYesGateway base URL, http or https
signerwalletYesWallet object (eth_account account, viem account). Raw keys are refused
networksnetworksYesAllowed CAIP-2 EVM networks, for example eip155:84532 (Base Sepolia)
max_per_calllimits.maxPerCallYesPer-invocation limit in USDC, a decimal string such as "0.05"
max_per_daylimits.maxPerDayYesLimit per UTC day in USDC
resourcesresourcesNoAllowlist of resource ids; every eligible resource when omitted
allow_mainnetallowMainnetNoMust be true before any mainnet network is accepted
approvalapprovalNopaid (default), always or never: which tools need human approval
spend_storespendStoreNoDaily spending counter; process memory by default
-fetchNoFetch implementation; globalThis.fetch by default
transport-NoHTTP transport override for tests; closed after each call

Limits are non-negative decimal strings with up to 6 decimal places. Payments use each network's USDC, with any asset allowed on local Anvil:

NetworkIdKindUSDC
Local Anvileip155:31337testnetany asset (local development chain)
Base Sepoliaeip155:84532testnet0x036CbD53842c5426634e7929541eC2318f3dCF7e
Arbitrum Sepoliaeip155:421614testnet0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
Baseeip155:8453mainnet0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Ethereumeip155:1mainnet0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Polygoneip155:137mainnet0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Arbitrum Oneeip155:42161mainnet0xaf88d065e77c8cC2239327C5EDb3A432268e5831
Avalanche C-Chaineip155:43114mainnet0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E

Mainnet networks require allow_mainnet=True in Python or allowMainnet: true in TypeScript. Unknown network ids are rejected even with that setting. Invalid configuration fails during client construction. An empty resources list excludes every resource; omit it to include all eligible ones.

By default, each client keeps its daily total in memory, so restarting resets it. To retain the total across restarts or share it across processes, pass a spend store with an atomic add(day, amount) method. It adds USDC base units to the given UTC day's total and returns the new total; negative amounts roll back a reservation.

Discovery

The core reads GET /api/resources and keeps resources that meet these conditions:

  • exposedVia includes http;
  • pricing.type is free, or paymentMethods includes x402;
  • the resources allowlist is absent or lists its id.

Resources excluded by these checks produce a warning. Eligible resources become tool descriptions:

  • name: the resource id;
  • description: description, falling back to name, then id; a full stop is added unless the text ends with ., ! or ?, followed by a space and Costs <amount> <currency> per call., Free., or Paid. when the price has no fixed amount;
  • inputSchema: inputSchema, or {"type": "object", "properties": {}};
  • paid: true unless pricing.type is free;
  • needsApproval: paid under approval: paid, always true under always, always false under never.

Invocation and spending limits

An invocation sends the tool input as the JSON body of POST /api/resources/{id}/invoke. A free resource returns its result directly. A paid resource returns 402 with an x402 challenge. The core uses the official x402 client (@x402/fetch and @x402/evm for TypeScript, or the Python x402 package) to handle the payment:

  1. Filter the offers

    An offer is acceptable only with the exact scheme, a network listed in networks, that network's USDC and the EIP-3009 transfer method (extra.assetTransferMethod absent or eip3009; Permit2 would need a prior on-chain approval). The core removes unacceptable offers before the x402 client selects one. It signs nothing if no offer qualifies.

  2. Check and reserve before signing

    The x402 before-payment hook checks the selected offer's network, scheme, asset and transfer method again. Its amount, in USDC base units (6 decimals), must fit both the per-call limit and the remaining daily allowance. The hook atomically reserves that amount in the spend store before signing. A rejected offer is never signed.

  3. Send once

    The core sends the signed request once, without retrying after any outcome. The default HTTP transport disables retries, and invocation requests do not follow redirects. Each invocation can produce at most one payment.

  4. Keep the reservation

    After signing, the reserved amount stays in the daily total even if the gateway rejects the payment, because the core cannot always tell whether funds moved.

The model calling the tool cannot change any limit.

A 2xx response returns { status, body, payment }: the HTTP status, the body and the decoded PAYMENT-RESPONSE header. The body is parsed as JSON when its Content-Type contains json and parsing succeeds; otherwise it stays text. payment carries the network, transaction hash and payer, or is null when the response has no valid payment header, as with a free call.

Errors

Runtime failures raise AgentCommerceError. Its short message starts with the code and omits keys, headers and backend bodies, so frameworks can show it to the model. For example: LIMIT_EXCEEDED: Price is above the per-call limit.

CodeWhen
LIMIT_EXCEEDEDThe price is above the per-call limit or the remaining daily allowance
NETWORK_NOT_ALLOWEDThe gateway offered no acceptable payment: exact, EIP-3009, USDC, allowed network
PAYMENT_FAILEDThe gateway rejected a signed payment (HTTP 402), or returned a failed PAYMENT-RESPONSE with another status (502 on the gateway). Rejected payments settle nothing; unconfirmed settlements may still complete on chain. The message includes a valid transaction hash when available, and the reservation stays in the daily total in both cases
BACKEND_ERRORAny other non-2xx response, including invocation redirects. The error carries the HTTP status. Backend failures settle nothing under the default authorization payment flow. With paymentFlow: upfront, payment has already settled and the gateway sends a successful PAYMENT-RESPONSE with the error. The client error omits the transaction hash; calling the tool again pays again
GATEWAY_UNREACHABLEThe gateway could not be reached

Not supported

The core supports x402 exact on EVM. It does not support MPP, Solana or NEAR, the MCP transport, resources requiring AP2 mandates or other authorization, or wallet key storage.

Based on: docs/buyer-core.md