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
Call
POST /api/external/credits/purchasewithout a payment header, sign one returned x402 payment requirement, and retry the same purchase body withX-PAYMENT. - Create a wallet session
Call
POST /api/external/auth/sessionwith 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
Supported chain and token registry
| Chain | Network | Token | Contract | Decimals |
|---|---|---|---|---|
| Base | eip155:8453 | USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 |
| Base | eip155:8453 | EURC | 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42 | 6 |
| Arbitrum | eip155:42161 | USDC | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 | 6 |
| Optimism | eip155:10 | USDC | 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 | 6 |
| Polygon | eip155:137 | USDC | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | 6 |
| Avalanche | eip155:43114 | USDC | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E | 6 |
| Avalanche | eip155:43114 | EURC | 0xc891eb4cbdeff6e073e859e987815ed1505c2acd | 6 |
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.
x402 signing reference
Scope: all
{"all":{"acceptedPaymentHeaders":["payment-signature","PAYMENT-SIGNATURE","payment","PAYMENT","x-payment","X-PAYMENT"],"challengeHeaders":{"flag":"X-Payment-Required","required":"PAYMENT-REQUIRED"},"domainFields":[{"example":"\"USD Coin\"","field":"name","notes":"EIP-712 domain name as minted by the token contract (`name()`).","source":"acceptance.extra.name"},{"example":"\"2\"","field":"version","notes":"EIP-712 domain version (`version()` on the token contract).","source":"acceptance.extra.version"},{"example":"8453","field":"chainId","notes":"Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows.","source":"parse(acceptance.network)"},{"example":"\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"","field":"verifyingContract","notes":"The token contract address. Must match the asset the agent is paying with.","source":"acceptance.asset"}],"eip712Types":{"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"}]},"example":{"acceptance":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"initialRequestBody":{"credits":500,"payment_method":"x402","wallet_address":"0xyouragentwallet..."},"settlementBody":{"balance_credits":1500,"balance_usd":15,"message":"Credits purchased successfully","wallet_address":"0xyouragentwallet..."},"settlementResponse":{"network":"eip155:8453","payer":"0xyouragentwallet...","requirements":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"success":true,"transaction":"0xaa37bd14ff0f17d20ef9988b86c369e2615a20ed2948dd74b8423378f93ff267"},"signedEnvelope":{"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","network":"eip155:8453","payload":{"authorization":{"from":"0xyouragentwallet...","nonce":"0xc244d760baf20000000000000000000000000000000000000000000000000000","to":"0xagentpmtcollectorwallet...","validAfter":"0","validBefore":"1777057080","value":"5000000"},"signature":"0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334455667788991b"},"scheme":"exact","x402Version":2}},"maximumTimeoutSeconds":300,"outcomes":[{"agentAction":"Record the transaction hash, treat credits as available, and resume tool invocations.","bodyShape":"{ message, wallet_address, balance_credits, balance_usd }","label":"Success","status":200,"summary":"Broadcast confirmed and backend credited the account."},{"agentAction":"Retry the same request with an idempotent request_id until 200 or 400 is returned.","bodyShape":"{ success: false, status: \"pending\" | \"pending_confirmations\", transaction_hash, confirmations?, required_confirmations? }","label":"Pending","status":202,"summary":"Broadcast submitted but the required confirmations have not accrued on the self-broadcast path."},{"agentAction":"Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id.","bodyShape":"{ success: false, error, transaction_hash? }","label":"Rejected","status":400,"summary":"Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller."},{"agentAction":"Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times.","bodyShape":"{ success: false, error: \"An internal error occurred\" }","label":"Transient server error","status":500,"summary":"Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff."}],"paymentHeader":"X-PAYMENT","primaryType":"TransferWithAuthorization","protocolVersion":2,"responseHeaders":{"alias":"X-PAYMENT-RESPONSE","canonical":"PAYMENT-RESPONSE"},"scheme":"exact","scope":"all"},"challenge":{"acceptedPaymentHeaders":["payment-signature","PAYMENT-SIGNATURE","payment","PAYMENT","x-payment","X-PAYMENT"],"challengeHeaders":{"flag":"X-Payment-Required","required":"PAYMENT-REQUIRED"},"domainFields":[{"example":"\"USD Coin\"","field":"name","notes":"EIP-712 domain name as minted by the token contract (`name()`).","source":"acceptance.extra.name"},{"example":"\"2\"","field":"version","notes":"EIP-712 domain version (`version()` on the token contract).","source":"acceptance.extra.version"},{"example":"8453","field":"chainId","notes":"Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows.","source":"parse(acceptance.network)"},{"example":"\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"","field":"verifyingContract","notes":"The token contract address. Must match the asset the agent is paying with.","source":"acceptance.asset"}],"eip712Types":{"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"}]},"example":{"acceptance":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"initialRequestBody":{"credits":500,"payment_method":"x402","wallet_address":"0xyouragentwallet..."},"settlementBody":{"balance_credits":1500,"balance_usd":15,"message":"Credits purchased successfully","wallet_address":"0xyouragentwallet..."},"settlementResponse":{"network":"eip155:8453","payer":"0xyouragentwallet...","requirements":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"success":true,"transaction":"0xaa37bd14ff0f17d20ef9988b86c369e2615a20ed2948dd74b8423378f93ff267"},"signedEnvelope":{"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","network":"eip155:8453","payload":{"authorization":{"from":"0xyouragentwallet...","nonce":"0xc244d760baf20000000000000000000000000000000000000000000000000000","to":"0xagentpmtcollectorwallet...","validAfter":"0","validBefore":"1777057080","value":"5000000"},"signature":"0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334455667788991b"},"scheme":"exact","x402Version":2}},"maximumTimeoutSeconds":300,"outcomes":[{"agentAction":"Record the transaction hash, treat credits as available, and resume tool invocations.","bodyShape":"{ message, wallet_address, balance_credits, balance_usd }","label":"Success","status":200,"summary":"Broadcast confirmed and backend credited the account."},{"agentAction":"Retry the same request with an idempotent request_id until 200 or 400 is returned.","bodyShape":"{ success: false, status: \"pending\" | \"pending_confirmations\", transaction_hash, confirmations?, required_confirmations? }","label":"Pending","status":202,"summary":"Broadcast submitted but the required confirmations have not accrued on the self-broadcast path."},{"agentAction":"Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id.","bodyShape":"{ success: false, error, transaction_hash? }","label":"Rejected","status":400,"summary":"Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller."},{"agentAction":"Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times.","bodyShape":"{ success: false, error: \"An internal error occurred\" }","label":"Transient server error","status":500,"summary":"Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff."}],"paymentHeader":"X-PAYMENT","primaryType":"TransferWithAuthorization","protocolVersion":2,"responseHeaders":{"alias":"X-PAYMENT-RESPONSE","canonical":"PAYMENT-RESPONSE"},"scheme":"exact","scope":"challenge"},"settlement":{"acceptedPaymentHeaders":["payment-signature","PAYMENT-SIGNATURE","payment","PAYMENT","x-payment","X-PAYMENT"],"challengeHeaders":{"flag":"X-Payment-Required","required":"PAYMENT-REQUIRED"},"domainFields":[{"example":"\"USD Coin\"","field":"name","notes":"EIP-712 domain name as minted by the token contract (`name()`).","source":"acceptance.extra.name"},{"example":"\"2\"","field":"version","notes":"EIP-712 domain version (`version()` on the token contract).","source":"acceptance.extra.version"},{"example":"8453","field":"chainId","notes":"Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows.","source":"parse(acceptance.network)"},{"example":"\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"","field":"verifyingContract","notes":"The token contract address. Must match the asset the agent is paying with.","source":"acceptance.asset"}],"eip712Types":{"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"}]},"example":{"acceptance":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"initialRequestBody":{"credits":500,"payment_method":"x402","wallet_address":"0xyouragentwallet..."},"settlementBody":{"balance_credits":1500,"balance_usd":15,"message":"Credits purchased successfully","wallet_address":"0xyouragentwallet..."},"settlementResponse":{"network":"eip155:8453","payer":"0xyouragentwallet...","requirements":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"success":true,"transaction":"0xaa37bd14ff0f17d20ef9988b86c369e2615a20ed2948dd74b8423378f93ff267"},"signedEnvelope":{"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","network":"eip155:8453","payload":{"authorization":{"from":"0xyouragentwallet...","nonce":"0xc244d760baf20000000000000000000000000000000000000000000000000000","to":"0xagentpmtcollectorwallet...","validAfter":"0","validBefore":"1777057080","value":"5000000"},"signature":"0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334455667788991b"},"scheme":"exact","x402Version":2}},"maximumTimeoutSeconds":300,"outcomes":[{"agentAction":"Record the transaction hash, treat credits as available, and resume tool invocations.","bodyShape":"{ message, wallet_address, balance_credits, balance_usd }","label":"Success","status":200,"summary":"Broadcast confirmed and backend credited the account."},{"agentAction":"Retry the same request with an idempotent request_id until 200 or 400 is returned.","bodyShape":"{ success: false, status: \"pending\" | \"pending_confirmations\", transaction_hash, confirmations?, required_confirmations? }","label":"Pending","status":202,"summary":"Broadcast submitted but the required confirmations have not accrued on the self-broadcast path."},{"agentAction":"Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id.","bodyShape":"{ success: false, error, transaction_hash? }","label":"Rejected","status":400,"summary":"Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller."},{"agentAction":"Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times.","bodyShape":"{ success: false, error: \"An internal error occurred\" }","label":"Transient server error","status":500,"summary":"Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff."}],"paymentHeader":"X-PAYMENT","primaryType":"TransferWithAuthorization","protocolVersion":2,"responseHeaders":{"alias":"X-PAYMENT-RESPONSE","canonical":"PAYMENT-RESPONSE"},"scheme":"exact","scope":"settlement"},"sign":{"acceptedPaymentHeaders":["payment-signature","PAYMENT-SIGNATURE","payment","PAYMENT","x-payment","X-PAYMENT"],"challengeHeaders":{"flag":"X-Payment-Required","required":"PAYMENT-REQUIRED"},"domainFields":[{"example":"\"USD Coin\"","field":"name","notes":"EIP-712 domain name as minted by the token contract (`name()`).","source":"acceptance.extra.name"},{"example":"\"2\"","field":"version","notes":"EIP-712 domain version (`version()` on the token contract).","source":"acceptance.extra.version"},{"example":"8453","field":"chainId","notes":"Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows.","source":"parse(acceptance.network)"},{"example":"\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"","field":"verifyingContract","notes":"The token contract address. Must match the asset the agent is paying with.","source":"acceptance.asset"}],"eip712Types":{"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"}]},"example":{"acceptance":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"initialRequestBody":{"credits":500,"payment_method":"x402","wallet_address":"0xyouragentwallet..."},"settlementBody":{"balance_credits":1500,"balance_usd":15,"message":"Credits purchased successfully","wallet_address":"0xyouragentwallet..."},"settlementResponse":{"network":"eip155:8453","payer":"0xyouragentwallet...","requirements":{"amount":"5000000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","extra":{"name":"USD Coin","resourceUrl":"https://www.agentpmt.com/api/external/credits/purchase","version":"2"},"maxTimeoutSeconds":300,"network":"eip155:8453","payTo":"0xagentpmtcollectorwallet...","scheme":"exact"},"success":true,"transaction":"0xaa37bd14ff0f17d20ef9988b86c369e2615a20ed2948dd74b8423378f93ff267"},"signedEnvelope":{"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","network":"eip155:8453","payload":{"authorization":{"from":"0xyouragentwallet...","nonce":"0xc244d760baf20000000000000000000000000000000000000000000000000000","to":"0xagentpmtcollectorwallet...","validAfter":"0","validBefore":"1777057080","value":"5000000"},"signature":"0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334455667788991b"},"scheme":"exact","x402Version":2}},"maximumTimeoutSeconds":300,"outcomes":[{"agentAction":"Record the transaction hash, treat credits as available, and resume tool invocations.","bodyShape":"{ message, wallet_address, balance_credits, balance_usd }","label":"Success","status":200,"summary":"Broadcast confirmed and backend credited the account."},{"agentAction":"Retry the same request with an idempotent request_id until 200 or 400 is returned.","bodyShape":"{ success: false, status: \"pending\" | \"pending_confirmations\", transaction_hash, confirmations?, required_confirmations? }","label":"Pending","status":202,"summary":"Broadcast submitted but the required confirmations have not accrued on the self-broadcast path."},{"agentAction":"Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id.","bodyShape":"{ success: false, error, transaction_hash? }","label":"Rejected","status":400,"summary":"Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller."},{"agentAction":"Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times.","bodyShape":"{ success: false, error: \"An internal error occurred\" }","label":"Transient server error","status":500,"summary":"Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff."}],"paymentHeader":"X-PAYMENT","primaryType":"TransferWithAuthorization","protocolVersion":2,"responseHeaders":{"alias":"X-PAYMENT-RESPONSE","canonical":"PAYMENT-RESPONSE"},"scheme":"exact","scope":"sign"}}
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>"
}'
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.
Node (viem)
Buy credits end-to-end
// 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 MAX_RESPONSE_BYTES = 1024 * 1024;
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 readBoundedResponse(response) {
const declaredLength = Number(response.headers.get("content-length") || 0);
if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
await response.body?.cancel();
throw new Error("Response exceeded the one-megabyte limit");
}
if (!response.body) return "";
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8", { fatal: true });
let received = 0;
let body = "";
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
received += chunk.value.byteLength;
if (received > MAX_RESPONSE_BYTES) {
await reader.cancel();
throw new Error("Response exceeded the one-megabyte limit");
}
body += decoder.decode(chunk.value, { stream: true });
}
return body + decoder.decode();
}
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 readBoundedResponse(challenge)}`,
);
}
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 readBoundedResponse(settlement);
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
// 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";
const MAX_RESPONSE_BYTES = 1024 * 1024;
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 readBoundedResponse(response) {
const declaredLength = Number(response.headers.get("content-length") || 0);
if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
await response.body?.cancel();
throw new Error("Response exceeded the one-megabyte limit");
}
if (!response.body) return "";
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8", { fatal: true });
let received = 0;
let body = "";
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
received += chunk.value.byteLength;
if (received > MAX_RESPONSE_BYTES) {
await reader.cancel();
throw new Error("Response exceeded the one-megabyte limit");
}
body += decoder.decode(chunk.value, { stream: true });
}
return body + decoder.decode();
}
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 readBoundedResponse(sessionResp)}`);
}
const { session_nonce: sessionNonce } = JSON.parse(await readBoundedResponse(sessionResp));
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 readBoundedResponse(balanceResp));
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
"""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.13.7``, ``requests==2.32.5``. Python 3.12.
"""
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
"""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.13.7``, ``requests==2.32.5``. Python 3.12.
"""
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.mjs
Running 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.py
Use 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.
External agent signing reference
Scope: tools
{"canonicalJsonNote":"Canonical JSON: recursively sort every object's keys, serialize with no whitespace (`,`/`:` separators), UTF-8. Hash with SHA-256 and lowercase-hex encode. The server accepts both the raw-UTF-8 form (JS `JSON.stringify`) and the escaped form (Python `json.dumps(..., ensure_ascii=True)`); the hard requirements are sorted keys and the exact parameter object you send.","errors":[{"code":"EXTERNAL_SIGNATURE_SESSION_NONCE_INVALID","meaning":"The session nonce does not exist for this wallet.","recovery":"Create a fresh session nonce with POST /api/external/auth/session and re-sign.","status":401},{"code":"EXTERNAL_SIGNATURE_SESSION_NONCE_EXPIRED","meaning":"The session nonce expired.","recovery":"Create a new session nonce, use a fresh request_id, and re-sign.","status":401},{"code":"EXTERNAL_SIGNATURE_MALFORMED","meaning":"The signature could not be recovered as an EIP-191 personal_sign signature.","recovery":"Sign the exact UTF-8 message string and send a 0x-prefixed 65-byte signature.","status":401},{"code":"EXTERNAL_SIGNATURE_WALLET_MISMATCH","meaning":"The signature recovered to a different wallet than wallet_address.","recovery":"Compare details.expected_message, details.accepted_path_candidates, and details.expected_payload_hash; then sign with the wallet private key for wallet_address.","status":401},{"code":"EXTERNAL_SIGNATURE_REQUEST_REPLAY","meaning":"The request_id was already used for this session.","recovery":"Generate a fresh request_id and sign again.","status":409}],"messageTemplate":"agentpmt-external\nwallet:<lowercase_wallet_address>\nsession:<session_nonce>\nrequest:<request_id>\n<scope lines: method/path for tool invokes OR action/product for scoped endpoints>\npayload:<sha256(canonical_json(payload_object)) or empty>","rules":["Sign with EIP-191 personal-sign using the wallet in wallet_address.","Lowercase wallet_address before building the message.","Use the session_nonce returned by POST /api/external/auth/session.","Use a fresh request_id for every signed request.","For tool invokes include method/path and omit action/product; for scoped endpoints include action/product and omit method/path.","Hash the exact object you send with recursively sorted object keys; do not hash wrapper fields such as wallet_address, session_nonce, request_id, or signature.","Use the canonical signed path for tool invokes when possible; formatting variants are accepted only if they resolve to the same product/action."],"sections":[{"actions":[{"action":"invoke_action","endpoint":"POST /api/external/tools/{productSlug}/actions/{actionSlug}/invoke","method":"POST","path":"/external/tools/{productSlug}/actions/{actionSlug}/invoke","payload":"sha256(canonical_json(parameters))","requestIdExample":"invoke-uuid"}],"description":"Invoke externally enabled marketplace tools.","scope":"tools","title":"Tools"}],"signature":"EIP-191 personal-sign","signedPathNote":"Signed path tolerance: for tool invokes, the recommended signed `path:` is canonical `/external/tools/{productSlug}/actions/{actionSlug}/invoke`. The server also accepts the `/api` prefix, a trailing slash, and canonical-vs-raw slug casing variants when they resolve to the same product/action. Do not sign a different product or action."}
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
- Autonomous operations overview: /autonomous-agents
- Wallet generation utility: /agentaddress

