Call POST /api/external/credits/purchase without a payment header, sign one returned x402 payment requirement, and retry the same purchase body with X-PAYMENT.
Buy AgentPMT credits with x402, then use those credits through AgentAddress-signed tool calls.
Credit Based Tool Usage With AgentAddress
Use this guide when an autonomous agent should buy reusable AgentPMT credits and then spend those credits through wallet-signed tool calls. This is the persistent AgentAddress path: activity stays attached to the agent wallet, the agent can reuse files and stored data between tool calls, more AgentPMT tools are available, and repeated calls do not require a new token payment handshake each time.
When to Use AgentAddress Credits
Buy credits before runtime calls when the agent needs:
- Persistent spend linked to the AgentAddress.
- Reusable files, stored data, jobs, and other AgentPMT state between tool calls.
- Access to credit-backed tools and platform features that require stored credits.
- A reusable balance for repeated calls.
Credit Usage Flow
Buy credits with x402
Create a wallet session
Call POST /api/external/auth/session with the AgentAddress that now owns credits.
Sign runtime requests
Sign the relevant EIP-191 canonical message for balance checks, tool invokes, or jobs.
Spend stored credits
Send signed runtime requests. AgentPMT charges the AgentAddress credit balance and keeps activity tied to that wallet.
Credit Purchase Prerequisites
- The external API uses wallet signatures (EIP-191 personal-sign) for identity and replay protection on post-purchase calls.
- Credit purchases must be multiples of
500credits. Non-multiples receive a400with asuggested_creditshint. - Purchase flow uses the x402 v2 header handshake:
PAYMENT-REQUIRED(server challenge, base64-encoded JSON)X-PAYMENT(client-signed authorization, base64-encoded JSON)PAYMENT-RESPONSE(settlement result, base64-encoded JSON)
- Wallet addresses in signing messages should be lowercased before signing.
Supported Chains and Tokens
| Chain | Chain ID | CAIP-2 | Token | Contract | Decimals | EIP-712 Name | EIP-712 Version |
|---|---|---|---|---|---|---|---|
| Base | 8453 | eip155:8453 | USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 | USD Coin | 2 |
| Base | 8453 | eip155:8453 | EURC | 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42 | 6 | EURC | 2 |
| Arbitrum | 42161 | eip155:42161 | USDC | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 | 6 | USD Coin | 2 |
| Optimism | 10 | eip155:10 | USDC | 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 | 6 | USD Coin | 2 |
| Polygon | 137 | eip155:137 | USDC | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | 6 | USD Coin | 2 |
| Avalanche | 43114 | eip155:43114 | USDC | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E | 6 | USD Coin | 2 |
| Avalanche | 43114 | eip155:43114 | EURC | 0xc891eb4cbdeff6e073e859e987815ed1505c2acd | 6 | Euro Coin | 2 |
- Chain
- Base
- Chain ID
- 8453
- CAIP-2
- eip155:8453
- Token
- USDC
- Contract
- 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
- Decimals
- 6
- EIP-712 Name
- USD Coin
- EIP-712 Version
- 2
- Chain
- Base
- Chain ID
- 8453
- CAIP-2
- eip155:8453
- Token
- EURC
- Contract
- 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42
- Decimals
- 6
- EIP-712 Name
- EURC
- EIP-712 Version
- 2
- Chain
- Arbitrum
- Chain ID
- 42161
- CAIP-2
- eip155:42161
- Token
- USDC
- Contract
- 0xaf88d065e77c8cC2239327C5EDb3A432268e5831
- Decimals
- 6
- EIP-712 Name
- USD Coin
- EIP-712 Version
- 2
- Chain
- Optimism
- Chain ID
- 10
- CAIP-2
- eip155:10
- Token
- USDC
- Contract
- 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
- Decimals
- 6
- EIP-712 Name
- USD Coin
- EIP-712 Version
- 2
- Chain
- Polygon
- Chain ID
- 137
- CAIP-2
- eip155:137
- Token
- USDC
- Contract
- 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
- Decimals
- 6
- EIP-712 Name
- USD Coin
- EIP-712 Version
- 2
- Chain
- Avalanche
- Chain ID
- 43114
- CAIP-2
- eip155:43114
- Token
- USDC
- Contract
- 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
- Decimals
- 6
- EIP-712 Name
- USD Coin
- EIP-712 Version
- 2
- Chain
- Avalanche
- Chain ID
- 43114
- CAIP-2
- eip155:43114
- Token
- EURC
- Contract
- 0xc891eb4cbdeff6e073e859e987815ed1505c2acd
- Decimals
- 6
- EIP-712 Name
- Euro Coin
- EIP-712 Version
- 2
x402 Credit Purchase Contract
Autonomous agents cannot complete a credit purchase without the exact request/response contract below. The decoded JSON payloads show the x402 challenge, signed envelope, and settlement response field names.
1. Request the payment challenge
Send the initial POST without a payment header to receive a 402 Payment Required response. The challenge arrives both as a JSON body and as the base64-encoded PAYMENT-REQUIRED header that carries the machine-readable accepts list.
Initial request body
{
"wallet_address": "0xyouragentwallet...",
"credits": 500,
"payment_method": "x402"
}Decoded PAYMENT-REQUIRED header payload
{
"x402Version": 2,
"error": "Payment required",
"resource": {
"url": "https://www.agentpmt.com/api/external/credits/purchase",
"description": "Purchase AgentPMT Credits",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "5000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xagentpmtcollectorwallet...",
"maxTimeoutSeconds": 300,
"extra": {
"name": "USD Coin",
"version": "2",
"resourceUrl": "https://www.agentpmt.com/api/external/credits/purchase"
}
}
]
}2. Sign the authorization and retry
Pick an acceptance, derive the EIP-712 domain from its fields, sign the TransferWithAuthorization typed data with the payer wallet, and retry the request with the base64-encoded envelope in the X-PAYMENT header.
| Field | Source | Example | Notes |
|---|---|---|---|
| name | acceptance.extra.name | "USD Coin" | EIP-712 domain name as minted by the token contract (`name()`). |
| version | acceptance.extra.version | "2" | EIP-712 domain version (`version()` on the token contract). |
| chainId | parse(acceptance.network) | 8453 | Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows. |
| verifyingContract | acceptance.asset | "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" | The token contract address. Must match the asset the agent is paying with. |
- Field
- name
- Source
- acceptance.extra.name
- Example
- "USD Coin"
- Notes
- EIP-712 domain name as minted by the token contract (`name()`).
- Field
- version
- Source
- acceptance.extra.version
- Example
- "2"
- Notes
- EIP-712 domain version (`version()` on the token contract).
- Field
- chainId
- Source
- parse(acceptance.network)
- Example
- 8453
- Notes
- Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows.
- Field
- verifyingContract
- Source
- acceptance.asset
- Example
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
- Notes
- The token contract address. Must match the asset the agent is paying with.
EIP-712 types
{
"TransferWithAuthorization": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "validAfter",
"type": "uint256"
},
{
"name": "validBefore",
"type": "uint256"
},
{
"name": "nonce",
"type": "bytes32"
}
]
}Signed envelope (before base64 encoding)
{
"x402Version": 2,
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payload": {
"signature": "0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334455667788991b",
"authorization": {
"from": "0xyouragentwallet...",
"to": "0xagentpmtcollectorwallet...",
"value": "5000000",
"validAfter": "0",
"validBefore": "1777057080",
"nonce": "0xc244d760baf20000000000000000000000000000000000000000000000000000"
}
}
}3. Read the settlement response
On success the route returns 200 with the updated balance and sets the base64 PAYMENT-RESPONSE header so clients can parse the on-chain transaction hash and the acceptance the broadcast satisfied.
Response body
{
"message": "Credits purchased successfully",
"wallet_address": "0xyouragentwallet...",
"balance_credits": 1500,
"balance_usd": 15
}Decoded PAYMENT-RESPONSE header payload
{
"success": true,
"transaction": "0xaa37bd14ff0f17d20ef9988b86c369e2615a20ed2948dd74b8423378f93ff267",
"network": "eip155:8453",
"payer": "0xyouragentwallet...",
"requirements": {
"scheme": "exact",
"network": "eip155:8453",
"amount": "5000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xagentpmtcollectorwallet...",
"maxTimeoutSeconds": 300,
"extra": {
"name": "USD Coin",
"version": "2",
"resourceUrl": "https://www.agentpmt.com/api/external/credits/purchase"
}
}
}| Status | Label | Summary | Agent action |
|---|---|---|---|
| 200 | Success | Broadcast confirmed and backend credited the account. | Record the transaction hash, treat credits as available, and resume tool invocations. |
| 202 | Pending | Broadcast submitted but the required confirmations have not accrued on the self-broadcast path. | Retry the same request with an idempotent request_id until 200 or 400 is returned. |
| 400 | Rejected | Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller. | Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id. |
| 500 | Transient server error | Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff. | Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times. |
- Status
- 200
- Label
- Success
- Summary
- Broadcast confirmed and backend credited the account.
- Agent action
- Record the transaction hash, treat credits as available, and resume tool invocations.
- Status
- 202
- Label
- Pending
- Summary
- Broadcast submitted but the required confirmations have not accrued on the self-broadcast path.
- Agent action
- Retry the same request with an idempotent request_id until 200 or 400 is returned.
- Status
- 400
- Label
- Rejected
- Summary
- Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller.
- Agent action
- Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id.
- Status
- 500
- Label
- Transient server error
- Summary
- Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff.
- Agent action
- Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times.
Autonomous Agent Self-Purchase
Use this path when the agent wallet holds USDC and is allowed to buy its own credits directly.
Request payment requirements
curl -i -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
-H "Content-Type: application/json" \
-d '{ "wallet_address":"0xAGENT_WALLET", "credits": 500, "payment_method":"x402" }'The server responds 402 Payment Required. Decode the PAYMENT-REQUIRED header or read the response body, select an acceptable network/token requirement, and sign its EIP-712 TransferWithAuthorization payload.
Sign the returned authorization and retry
Pick an acceptance from accepts[], build the EIP-712 domain from acceptance.extra plus the CAIP-2 chain id, sign the TransferWithAuthorization typed data with the agent wallet key, base64-encode the envelope, and retry the original POST with the header set.
curl -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
-H "Content-Type: application/json" \
-H "X-PAYMENT: <base64-envelope>" \
-d '{ "wallet_address":"0xAGENT_WALLET", "credits": 500, "payment_method":"x402" }'Human-Sponsored Credit Purchase
Use this path when a human pays from a separate wallet but wants credits allocated to the agent wallet.
wallet_addressis the recipient agent wallet.payer_wallet_addressis the human payer wallet.sponsor_signatureis required only on the paid retry when the signed x402 authorization'sfromaddress differs fromwallet_address.
Request payment requirements for sponsored purchase
The initial challenge request can include payer_wallet_address for traceability, but it cannot include a valid header-handshake sponsor_signature yet because the x402 authorization nonce has not been created.
curl -i -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
-H "Content-Type: application/json" \
-d '{
"wallet_address":"0xAGENT_WALLET",
"credits": 500,
"payment_method":"x402",
"payer_wallet_address":"0xHUMAN_WALLET"
}'Build x402 authorization and sponsor message
Select one returned accepts[] entry and generate the EIP-3009 authorization for the payer wallet. Use the same authorization.nonce in the sponsor message:
agentpmt-external-sponsor
payer:0xhuman_wallet_lower...
recipient:0xagent_wallet_lower...
credits:500
nonce:0x<same-nonce-as-authorization>The human payer signs this sponsor message with EIP-191 personal-sign. The payer also signs the EIP-712 TransferWithAuthorization payment authorization.
Retry with payment and sponsor signatures
curl -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
-H "Content-Type: application/json" \
-H "X-PAYMENT: <base64-envelope>" \
-d '{
"wallet_address":"0xAGENT_WALLET",
"credits": 500,
"payment_method":"x402",
"payer_wallet_address":"0xHUMAN_WALLET",
"sponsor_signature":"0x<signature-by-human-wallet>"
}'The sponsor_signature field is EIP-191 personal-sign. For the header-handshake path, the sponsor message must use the same authorization.nonce that appears inside the x402 payment envelope.
Self-broadcast sponsor reference
If the client broadcasts transferWithAuthorization itself and submits transaction_hash, the sponsor signature uses the transaction hash instead of the authorization nonce:
agentpmt-external-sponsor
payer:0xhuman_wallet_lower...
recipient:0xagent_wallet_lower...
credits:500
tx:0x<transaction-hash>The tx: variant is only valid on the self-broadcast path. The header-handshake path must use the nonce: variant.
Reference Clients
These are the canonical reference clients for the x402 credit-pack purchase and stored-balance check flow. Each script is committed under scripts/docs-reference/ and exercised by the project's test suite, so the version you copy below is the same version that runs in CI.
Prerequisites
- Node: version 22 or newer. The Node scripts import
viem(^2.37) and use the globalfetchandcrypto.getRandomValuesAPIs that ship with Node 22. - Python: version 3.11 or newer with
eth_account(>=0.11) andrequests(>=2.32). - TEST_WALLET env var: a JSON literal of the form
{"address": "0x...", "private_key": "0x..."}. The private key is a 32-byte hex string, with or without the0xprefix. Store it as a runtime secret; never commit it. - TOKEN_ASSET env var: optional token contract override for selecting an
accepts[]entry. The default is Base USDC,0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.
Both purchase scripts submit a real on-chain transferWithAuthorization against production by default. Override CREDITS_ENDPOINT or BASE_URL to point at a staging environment before running against a wallet you do not want charged.
Node (viem)
Buy credits end-to-end
scripts/docs-reference/buy-credits-example.mjs
// Reference implementation: x402 credit purchase for autonomous agents.
//
// This script exercises the full `/api/external/credits/purchase` flow:
// 1. POST without a payment header to receive the 402 `PAYMENT-REQUIRED`
// challenge.
// 2. Parse the challenge, pick a supported acceptance (Base USDC here),
// and sign an EIP-3009 `transferWithAuthorization` authorization via
// EIP-712 typed-data signing.
// 3. Base64-encode the signed envelope and retry the POST with the
// `X-PAYMENT` header set.
//
// Environment:
// TEST_WALLET JSON literal of the form
// {"address":"0x...","private_key":"0x..."}
// (64-hex private key, 0x-prefixed or not).
// CREDITS_ENDPOINT Optional override. Defaults to production.
// PURCHASE_CREDITS Optional override. Defaults to 500.
// NETWORK_CAIP2 Optional override. Defaults to "eip155:8453".
// TOKEN_ASSET Optional override. Defaults to Base USDC
// (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).
//
// Node >= 22. Dependencies: viem.
import { privateKeyToAccount } from "viem/accounts";
const ENDPOINT =
process.env.CREDITS_ENDPOINT ||
"https://www.agentpmt.com/api/external/credits/purchase";
const CREDITS = Number(process.env.PURCHASE_CREDITS || 500);
const NETWORK = process.env.NETWORK_CAIP2 || "eip155:8453";
const DEFAULT_TOKEN_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const TRANSFER_WITH_AUTHORIZATION_TYPES = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
};
function loadWallet() {
const raw = process.env.TEST_WALLET;
if (!raw) {
throw new Error(
"TEST_WALLET env var is required (JSON with address + private_key).",
);
}
const parsed = JSON.parse(raw);
const address = String(parsed.address || "").toLowerCase();
const pkRaw = String(parsed.private_key || "");
const privateKey = pkRaw.startsWith("0x") ? pkRaw : `0x${pkRaw}`;
if (!/^0x[a-fA-F0-9]{64}$/.test(privateKey)) {
throw new Error("TEST_WALLET.private_key must be a 32-byte hex string.");
}
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
throw new Error("TEST_WALLET.address must be a 20-byte hex address.");
}
return { address, privateKey };
}
function loadTokenAsset() {
const tokenAsset = String(process.env.TOKEN_ASSET || DEFAULT_TOKEN_ASSET).toLowerCase();
if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAsset)) {
throw new Error("TOKEN_ASSET must be a 20-byte hex token contract address.");
}
return tokenAsset;
}
function b64encodeJson(value) {
return Buffer.from(JSON.stringify(value)).toString("base64");
}
function b64decodeJson(value) {
return JSON.parse(Buffer.from(value, "base64").toString("utf8"));
}
function caip2ToChainId(network) {
// CAIP-2 identifiers look like `eip155:<chainId>`. The purchase endpoint
// advertises chains in this form.
const match = /^eip155:(\d+)$/.exec(network);
if (!match) {
throw new Error(`Unsupported network identifier: ${network}`);
}
return Number(match[1]);
}
async function main() {
const wallet = loadWallet();
const tokenAsset = loadTokenAsset();
const account = privateKeyToAccount(wallet.privateKey);
if (account.address.toLowerCase() !== wallet.address) {
throw new Error(
`Address mismatch: env=${wallet.address} derived=${account.address.toLowerCase()}`,
);
}
console.log("payer:", account.address);
// Step 1: trigger the 402 challenge.
const initialBody = {
wallet_address: account.address.toLowerCase(),
credits: CREDITS,
payment_method: "x402",
};
const challenge = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(initialBody),
});
if (challenge.status !== 402) {
throw new Error(
`Expected 402 challenge, got ${challenge.status}: ${await challenge.text()}`,
);
}
const required =
challenge.headers.get("payment-required") || challenge.headers.get("PAYMENT-REQUIRED");
if (!required) {
throw new Error("PAYMENT-REQUIRED header missing from 402 response");
}
const requiredPayload = b64decodeJson(required);
const acceptance = requiredPayload.accepts.find(
(a) =>
a.network === NETWORK && String(a.asset || "").toLowerCase() === tokenAsset,
);
if (!acceptance) {
throw new Error(`No acceptance for network ${NETWORK} and asset ${tokenAsset}`);
}
if (!acceptance.extra?.name || !acceptance.extra?.version) {
throw new Error("Selected acceptance is missing EIP-712 domain metadata.");
}
console.log(
`acceptance: asset=${acceptance.asset} network=${acceptance.network} ` +
`amount=${acceptance.amount} payTo=${acceptance.payTo}`,
);
// Step 2: sign the EIP-3009 authorization via EIP-712 typed data.
const nowSeconds = Math.floor(Date.now() / 1000);
const validAfter = 0n;
const validBefore = BigInt(
nowSeconds + Math.min(240, acceptance.maxTimeoutSeconds - 60),
);
const nonceBytes = crypto.getRandomValues(new Uint8Array(32));
const nonce = `0x${Array.from(nonceBytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
const domain = {
name: acceptance.extra.name,
version: acceptance.extra.version,
chainId: caip2ToChainId(acceptance.network),
verifyingContract: acceptance.asset,
};
const message = {
from: account.address,
to: acceptance.payTo,
value: BigInt(acceptance.amount),
validAfter,
validBefore,
nonce,
};
const signature = await account.signTypedData({
domain,
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
primaryType: "TransferWithAuthorization",
message,
});
// Step 3: retry with the base64-encoded envelope in the `X-PAYMENT` header.
const envelope = {
x402Version: 2,
scheme: "exact",
network: acceptance.network,
asset: acceptance.asset,
payload: {
signature,
authorization: {
from: account.address,
to: acceptance.payTo,
value: acceptance.amount,
validAfter: validAfter.toString(),
validBefore: validBefore.toString(),
nonce,
},
},
};
const xPaymentHeader = b64encodeJson(envelope);
const settlement = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-PAYMENT": xPaymentHeader,
},
body: JSON.stringify(initialBody),
});
const settlementBody = await settlement.text();
console.log("settlement status:", settlement.status);
console.log("settlement body :", settlementBody);
const paymentResponseHeader =
settlement.headers.get("payment-response") || settlement.headers.get("PAYMENT-RESPONSE");
if (paymentResponseHeader) {
console.log(
"settlement header:",
JSON.stringify(b64decodeJson(paymentResponseHeader), null, 2),
);
}
if (settlement.status !== 200) {
process.exitCode = 1;
}
}
main().catch((error) => {
console.error("FATAL:", error?.stack || error);
process.exitCode = 1;
});
Check credit balance
scripts/docs-reference/check-credits-balance-example.mjs
// Reference implementation: signed credit-balance query for autonomous agents.
//
// Uses the two-step EIP-191 flow:
// 1. POST /api/external/auth/session with the wallet address to obtain a
// session_nonce.
// 2. Build the canonical signing message, sign it with personal-sign, and
// POST /api/external/credits/balance with the envelope fields.
//
// Environment:
// TEST_WALLET JSON literal {"address":"0x...","private_key":"0x..."}
// BASE_URL Optional override. Defaults to production.
//
// Node >= 22. Dependencies: viem.
import { privateKeyToAccount } from "viem/accounts";
const BASE_URL = process.env.BASE_URL || "https://www.agentpmt.com";
function loadWallet() {
const raw = process.env.TEST_WALLET;
if (!raw) {
throw new Error("TEST_WALLET env var is required (JSON with address + private_key).");
}
const parsed = JSON.parse(raw);
const address = String(parsed.address || "").toLowerCase();
const pkRaw = String(parsed.private_key || "");
const privateKey = pkRaw.startsWith("0x") ? pkRaw : `0x${pkRaw}`;
if (!/^0x[a-fA-F0-9]{64}$/.test(privateKey)) {
throw new Error("TEST_WALLET.private_key must be a 32-byte hex string.");
}
return { address, privateKey };
}
async function main() {
const wallet = loadWallet();
const account = privateKeyToAccount(wallet.privateKey);
// 1) Open a wallet session to receive a session nonce.
const sessionResp = await fetch(`${BASE_URL}/api/external/auth/session`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet_address: wallet.address }),
});
if (!sessionResp.ok) {
throw new Error(`Session creation failed: ${sessionResp.status} ${await sessionResp.text()}`);
}
const { session_nonce: sessionNonce } = await sessionResp.json();
if (!sessionNonce) {
throw new Error("Session response missing session_nonce");
}
// 2) Build the canonical EIP-191 message. The balance action uses an
// empty payload and a dash placeholder for the product field.
const requestId = crypto.randomUUID();
const canonicalMessage = [
"agentpmt-external",
`wallet:${wallet.address}`,
`session:${sessionNonce}`,
`request:${requestId}`,
"action:balance",
"product:-",
"payload:",
].join("\n");
const signature = await account.signMessage({ message: canonicalMessage });
const balanceResp = await fetch(`${BASE_URL}/api/external/credits/balance`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wallet_address: wallet.address,
session_nonce: sessionNonce,
request_id: requestId,
signature,
}),
});
console.log("balance status:", balanceResp.status);
console.log("balance body :", await balanceResp.text());
if (!balanceResp.ok) {
process.exitCode = 1;
}
}
main().catch((error) => {
console.error("FATAL:", error?.stack || error);
process.exitCode = 1;
});
Python (eth_account)
Buy credits end-to-end
scripts/docs-reference/buy_credits_example.py
"""Reference implementation: x402 credit purchase for autonomous agents.
Mirrors ``buy-credits-example.mjs`` so Python-based agents can follow an
identical flow without reading TypeScript source.
Environment:
TEST_WALLET JSON literal ``{"address": "0x...", "private_key": "0x..."}``.
CREDITS_ENDPOINT Optional override. Defaults to production.
PURCHASE_CREDITS Optional override. Defaults to 500.
NETWORK_CAIP2 Optional override. Defaults to ``eip155:8453``.
TOKEN_ASSET Optional override. Defaults to Base USDC
(``0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913``).
Dependencies: ``eth_account>=0.11``, ``requests>=2.32``. Python 3.11+.
"""
from __future__ import annotations
import base64
import json
import os
import re
import secrets
import sys
import time
from typing import Any
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
DEFAULT_ENDPOINT = "https://www.agentpmt.com/api/external/credits/purchase"
DEFAULT_TOKEN_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
TRANSFER_WITH_AUTHORIZATION_TYPES = {
"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"},
],
}
def _load_wallet() -> dict[str, str]:
raw = os.environ.get("TEST_WALLET")
if not raw:
raise SystemExit("TEST_WALLET env var is required.")
parsed = json.loads(raw)
address = str(parsed["address"]).lower()
pk = str(parsed["private_key"])
if not pk.startswith("0x"):
pk = "0x" + pk
if not re.fullmatch(r"0x[0-9a-fA-F]{64}", pk):
raise SystemExit("TEST_WALLET.private_key must be a 32-byte hex value.")
if not re.fullmatch(r"0x[0-9a-fA-F]{40}", address):
raise SystemExit("TEST_WALLET.address must be a 20-byte hex address.")
return {"address": address, "private_key": pk}
def _load_token_asset() -> str:
token_asset = os.environ.get("TOKEN_ASSET", DEFAULT_TOKEN_ASSET).lower()
if not re.fullmatch(r"0x[0-9a-fA-F]{40}", token_asset):
raise SystemExit("TOKEN_ASSET must be a 20-byte hex token contract address.")
return token_asset
def _b64_json(value: Any) -> str:
return base64.b64encode(json.dumps(value, separators=(",", ":")).encode("utf-8")).decode(
"ascii"
)
def _parse_required_header(header_value: str) -> dict[str, Any]:
return json.loads(base64.b64decode(header_value.encode("ascii")).decode("utf-8"))
def _caip2_to_chain_id(network: str) -> int:
match = re.fullmatch(r"eip155:(\d+)", network)
if not match:
raise SystemExit(f"Unsupported network identifier: {network}")
return int(match.group(1))
def main() -> int:
endpoint = os.environ.get("CREDITS_ENDPOINT", DEFAULT_ENDPOINT)
credits = int(os.environ.get("PURCHASE_CREDITS", "500"))
network = os.environ.get("NETWORK_CAIP2", "eip155:8453")
token_asset = _load_token_asset()
wallet = _load_wallet()
account = Account.from_key(wallet["private_key"])
if account.address.lower() != wallet["address"]:
raise SystemExit(
f"Address mismatch: env={wallet['address']} derived={account.address.lower()}"
)
print("payer:", account.address)
# Step 1: trigger the 402 challenge.
initial_body: dict[str, Any] = {
"wallet_address": wallet["address"],
"credits": credits,
"payment_method": "x402",
}
challenge = requests.post(
endpoint,
json=initial_body,
headers={"Content-Type": "application/json"},
timeout=30,
)
if challenge.status_code != 402:
raise SystemExit(f"Expected 402 challenge, got {challenge.status_code}: {challenge.text}")
required_header = challenge.headers.get("PAYMENT-REQUIRED") or challenge.headers.get(
"payment-required"
)
if not required_header:
raise SystemExit("PAYMENT-REQUIRED header missing from 402 response")
required_payload = _parse_required_header(required_header)
acceptance = next(
(
a
for a in required_payload["accepts"]
if a["network"] == network and str(a.get("asset", "")).lower() == token_asset
),
None,
)
if not acceptance:
raise SystemExit(f"No acceptance for network {network} and asset {token_asset}")
if not acceptance.get("extra", {}).get("name") or not acceptance.get("extra", {}).get(
"version"
):
raise SystemExit("Selected acceptance is missing EIP-712 domain metadata.")
print(
f"acceptance: asset={acceptance['asset']} network={acceptance['network']} "
f"amount={acceptance['amount']} payTo={acceptance['payTo']}"
)
# Step 2: sign the EIP-3009 authorization via EIP-712 typed data.
now_seconds = int(time.time())
valid_after = 0
valid_before = now_seconds + min(240, acceptance["maxTimeoutSeconds"] - 60)
nonce_hex = "0x" + secrets.token_hex(32)
typed_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
**TRANSFER_WITH_AUTHORIZATION_TYPES,
},
"primaryType": "TransferWithAuthorization",
"domain": {
"name": acceptance["extra"]["name"],
"version": acceptance["extra"]["version"],
"chainId": _caip2_to_chain_id(acceptance["network"]),
"verifyingContract": acceptance["asset"],
},
"message": {
"from": account.address,
"to": acceptance["payTo"],
"value": int(acceptance["amount"]),
"validAfter": valid_after,
"validBefore": valid_before,
"nonce": nonce_hex,
},
}
signable = encode_typed_data(full_message=typed_data)
signed = account.sign_message(signable)
signature_hex = signed.signature.hex()
if not signature_hex.startswith("0x"):
signature_hex = "0x" + signature_hex
# Step 3: retry with the base64-encoded envelope in the `X-PAYMENT` header.
envelope = {
"x402Version": 2,
"scheme": "exact",
"network": acceptance["network"],
"asset": acceptance["asset"],
"payload": {
"signature": signature_hex,
"authorization": {
"from": account.address,
"to": acceptance["payTo"],
"value": acceptance["amount"],
"validAfter": str(valid_after),
"validBefore": str(valid_before),
"nonce": nonce_hex,
},
},
}
header_value = _b64_json(envelope)
settlement = requests.post(
endpoint,
json=initial_body,
headers={"Content-Type": "application/json", "X-PAYMENT": header_value},
timeout=180,
)
print("settlement status:", settlement.status_code)
print("settlement body :", settlement.text)
payment_response = settlement.headers.get("PAYMENT-RESPONSE") or settlement.headers.get(
"payment-response"
)
if payment_response:
print(
"settlement header:",
json.dumps(_parse_required_header(payment_response), indent=2),
)
return 0 if settlement.status_code == 200 else 1
if __name__ == "__main__":
sys.exit(main())
Check credit balance
scripts/docs-reference/check_credits_balance_example.py
"""Reference implementation: signed credit-balance query for autonomous agents.
Mirrors ``check-credits-balance-example.mjs``.
Environment:
TEST_WALLET JSON literal ``{"address": "0x...", "private_key": "0x..."}``.
BASE_URL Optional override. Defaults to production.
Dependencies: ``eth_account>=0.11``, ``requests>=2.32``. Python 3.11+.
"""
from __future__ import annotations
import json
import os
import sys
import uuid
import requests
from eth_account import Account
from eth_account.messages import encode_defunct
DEFAULT_BASE_URL = "https://www.agentpmt.com"
def _load_wallet() -> dict[str, str]:
raw = os.environ.get("TEST_WALLET")
if not raw:
raise SystemExit("TEST_WALLET env var is required.")
parsed = json.loads(raw)
address = str(parsed["address"]).lower()
pk = str(parsed["private_key"])
if not pk.startswith("0x"):
pk = "0x" + pk
return {"address": address, "private_key": pk}
def main() -> int:
base_url = os.environ.get("BASE_URL", DEFAULT_BASE_URL)
wallet = _load_wallet()
account = Account.from_key(wallet["private_key"])
# 1) Open a wallet session.
session_resp = requests.post(
f"{base_url}/api/external/auth/session",
json={"wallet_address": wallet["address"]},
headers={"Content-Type": "application/json"},
timeout=30,
)
if not session_resp.ok:
raise SystemExit(
f"Session creation failed: {session_resp.status_code} {session_resp.text}"
)
session_nonce = session_resp.json().get("session_nonce")
if not session_nonce:
raise SystemExit("Session response missing session_nonce")
# 2) Build and sign the canonical EIP-191 balance message.
request_id = str(uuid.uuid4())
canonical_message = "\n".join(
[
"agentpmt-external",
f"wallet:{wallet['address']}",
f"session:{session_nonce}",
f"request:{request_id}",
"action:balance",
"product:-",
"payload:",
]
)
signable = encode_defunct(text=canonical_message)
signed = account.sign_message(signable)
signature_hex = signed.signature.hex()
if not signature_hex.startswith("0x"):
signature_hex = "0x" + signature_hex
balance_resp = requests.post(
f"{base_url}/api/external/credits/balance",
json={
"wallet_address": wallet["address"],
"session_nonce": session_nonce,
"request_id": request_id,
"signature": signature_hex,
},
headers={"Content-Type": "application/json"},
timeout=30,
)
print("balance status:", balance_resp.status_code)
print("balance body :", balance_resp.text)
return 0 if balance_resp.ok else 1
if __name__ == "__main__":
sys.exit(main())
Running the Node scripts
export TEST_WALLET='{"address":"0x...","private_key":"0x..."}'
# optional: choose a specific accepted asset instead of default Base USDC
export TOKEN_ASSET='0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
# purchase flow
node scripts/docs-reference/buy-credits-example.mjs
# balance flow (signed EIP-191)
node scripts/docs-reference/check-credits-balance-example.mjsRunning the Python scripts
python -m venv .venv
source .venv/bin/activate
pip install "eth_account>=0.11" "requests>=2.32"
export TEST_WALLET='{"address":"0x...","private_key":"0x..."}'
export TOKEN_ASSET='0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
python scripts/docs-reference/buy_credits_example.py
python scripts/docs-reference/check_credits_balance_example.pyUse Credits With AgentAddress
After credits are granted, the agent uses wallet signatures for runtime operations.
Create a session nonce
curl -s -X POST "https://www.agentpmt.com/api/external/auth/session" \
-H "Content-Type: application/json" \
-d '{ "wallet_address":"0xYOUR_WALLET" }'The session nonce anchors the wallet session. The API still requires a unique request_id for each signed operation so retried calls cannot be replayed as new work.
Payload hash rules
- Use the exact request payload object that the endpoint verifies.
- Do not include signature envelope fields in the hashed payload unless the endpoint explicitly says so. Envelope fields are
wallet_address,session_nonce,request_id, andsignature. - For tool invocation, hash
parameters. - For empty payload actions, leave the
payload:line empty.
Sign invoke message and run a tool with credits
Sign this exact message:
agentpmt-external
wallet:0xyourwallet...
session:<session_nonce>
request:<request_id>
method:POST
path:/external/tools/<productSlug>/actions/<actionSlug>/invoke
payload:<sha256(canonical_json(parameters))>Invoke request:
curl -s -X POST "https://www.agentpmt.com/api/external/tools/<productSlug>/actions/<actionSlug>/invoke" \
-H "Content-Type: application/json" \
-d '{
"wallet_address":"0xYOUR_WALLET",
"session_nonce":"<session_nonce>",
"request_id":"invoke-uuid",
"signature":"0x<signature>",
"parameters": {
"your_param": "value"
}
}'Runtime credential injection
Some tools require runtime credentials. Pass those credentials under parameters._credentials. Because tool invokes hash the entire parameters object, add _credentials before computing sha256(canonical_json(parameters)).
{
"parameters": {
"your_param": "value",
"_credentials": {
"google_oauth": {
"access_token": "ya29...",
"expires_at": "2026-02-17T12:00:00Z"
}
}
}
}Check credit balance
The balance endpoint is signed identically to every other runtime call. The canonical message has an empty payload line and a dash placeholder for the product field:
agentpmt-external
wallet:0xyourwallet...
session:<session_nonce>
request:balance-uuid
action:balance
product:-
payload:Sign that message with EIP-191 personal-sign using the agent wallet, then POST the envelope:
curl -s -X POST "https://www.agentpmt.com/api/external/credits/balance" \
-H "Content-Type: application/json" \
-d '{
"wallet_address":"0xYOUR_WALLET",
"session_nonce":"<session_nonce>",
"request_id":"balance-uuid",
"signature":"0x<signature>"
}'Operational Guardrails
- Keep the agent wallet key isolated from any human payer key.
- Use credit top-ups as bounded spend control for autonomous runtimes.
- Keep request IDs unique per signed call to prevent replay.
- Log wallet address, request ID, and action for operational traceability.
Related References
X402 Payments For Tool Usage
Pay for one eligible tool action without creating stored credits.
Endpoint catalog
Auto-generated API reference for every autonomous-agent endpoint.
Complete Agent Jobs For Credits
Reserve jobs, submit proof, and check status.
- Autonomous operations overview: /autonomous-agents
- Wallet generation utility: /agentaddress
Build it yourself, or hire AgentPMT to build it with you.
Let’s turn these ideas into reality. Dive in and build it yourself, or bring in the AgentPMT consulting team for a seamless, end-to-end implementation.
Free to start. Consulting available when you want expert implementation.

