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.
| Language | Package |
|---|---|
| Python | agent-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.
StructuredTools, with approval middleware.Microsoft Agent FrameworkResources as FunctionTools with an approval mode.Vercel AI SDKAI SDK tools wired to toolApproval.Framework packages map resources to tools and approval settings; the core handles payments.
Install
pip install agent-commerce-clientPython 3.10 or newer. Payments use the official x402 package.
npm install @devlab.group/agent-commerce-client viemNode.js 20 or newer. viem provides the wallet account.
Quick start
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.
import { privateKeyToAccount } from 'viem/accounts';
import { AgentCommerceError, createAgentCommerceClient } from '@devlab.group/agent-commerce-client';
const client = createAgentCommerceClient({
gatewayUrl: 'https://merchant.example.com',
wallet: privateKeyToAccount(process.env.BUYER_PRIVATE_KEY as `0x${string}`),
networks: ['eip155:84532'],
limits: { maxPerCall: '0.05', maxPerDay: '1.00' },
});
for (const tool of await client.listTools()) {
console.log(`${tool.name}: ${tool.description}`);
}
try {
const { status, body, payment } = await client.invoke('report', {});
console.log(status, body, payment?.transaction);
} catch (error) {
if (!(error instanceof AgentCommerceError)) throw error;
console.error(error.code, error.status, error.message);
}Requests time out after 60 seconds.
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
| Python | TypeScript | Required | Meaning |
|---|---|---|---|
gateway_url | gatewayUrl | Yes | Gateway base URL, http or https |
signer | wallet | Yes | Wallet object (eth_account account, viem account). Raw keys are refused |
networks | networks | Yes | Allowed CAIP-2 EVM networks, for example eip155:84532 (Base Sepolia) |
max_per_call | limits.maxPerCall | Yes | Per-invocation limit in USDC, a decimal string such as "0.05" |
max_per_day | limits.maxPerDay | Yes | Limit per UTC day in USDC |
resources | resources | No | Allowlist of resource ids; every eligible resource when omitted |
allow_mainnet | allowMainnet | No | Must be true before any mainnet network is accepted |
approval | approval | No | paid (default), always or never: which tools need human approval |
spend_store | spendStore | No | Daily spending counter; process memory by default |
| - | fetch | No | Fetch implementation; globalThis.fetch by default |
transport | - | No | HTTP 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:
| Network | Id | Kind | USDC |
|---|---|---|---|
| Local Anvil | eip155:31337 | testnet | any asset (local development chain) |
| Base Sepolia | eip155:84532 | testnet | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Arbitrum Sepolia | eip155:421614 | testnet | 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d |
| Base | eip155:8453 | mainnet | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Ethereum | eip155:1 | mainnet | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Polygon | eip155:137 | mainnet | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
| Arbitrum One | eip155:42161 | mainnet | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 |
| Avalanche C-Chain | eip155:43114 | mainnet | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E |
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:
exposedViaincludeshttp;pricing.typeisfree, orpaymentMethodsincludesx402;- the
resourcesallowlist is absent or lists itsid.
Resources excluded by these checks produce a warning. Eligible resources become tool descriptions:
name: the resourceid;description:description, falling back toname, thenid; a full stop is added unless the text ends with.,!or?, followed by a space andCosts <amount> <currency> per call.,Free., orPaid.when the price has no fixed amount;inputSchema:inputSchema, or{"type": "object", "properties": {}};paid:trueunlesspricing.typeisfree;needsApproval:paidunderapproval: paid, alwaystrueunderalways, alwaysfalseundernever.
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:
- Filter the offers
An offer is acceptable only with the
exactscheme, a network listed innetworks, that network's USDC and the EIP-3009 transfer method (extra.assetTransferMethodabsent oreip3009; 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. - 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.
- 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.
- 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.
| Code | When |
|---|---|
LIMIT_EXCEEDED | The price is above the per-call limit or the remaining daily allowance |
NETWORK_NOT_ALLOWED | The gateway offered no acceptable payment: exact, EIP-3009, USDC, allowed network |
PAYMENT_FAILED | The 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_ERROR | Any 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_UNREACHABLE | The 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