Skip to content
Updated Jun 9, 2026

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

  1. Buy credits with x402

    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.

  2. Create a wallet session

    Call POST /api/external/auth/session with the AgentAddress that now owns credits.

  3. Sign runtime requests

    Sign the relevant EIP-191 canonical message for balance checks, tool invokes, or jobs.

  4. 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 500 credits. Non-multiples receive a 400 with a suggested_credits hint.
  • 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

ChainNetworkTokenContractDecimals
Baseeip155:8453USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA029136
Baseeip155:8453EURC0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb426
Arbitrumeip155:42161USDC0xaf88d065e77c8cC2239327C5EDb3A432268e58316
Optimismeip155:10USDC0x0b2C639c533813f4Aa9D7837CAf62653d097Ff856
Polygoneip155:137USDC0x3c499c542cEF5E3811e1192ce70d8cC03d5c33596
Avalancheeip155:43114USDC0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E6
Avalancheeip155:43114EURC0xc891eb4cbdeff6e073e859e987815ed1505c2acd6

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

json
{"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

bash
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.

bash
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_address is the recipient agent wallet.
  • payer_wallet_address is the human payer wallet.
  • sponsor_signature is required only on the paid retry when the signed x402 authorization's from address differs from wallet_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.

bash
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:

text
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

bash
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:

text
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 global fetch and crypto.getRandomValues APIs that ship with Node 22.
  • Python: version 3.11 or newer with eth_account (>=0.11) and requests (>=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 the 0x prefix. 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

javascript
// 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

javascript
// 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

python
"""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

python
"""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

bash
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

bash
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

bash
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

json
{"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:

bash
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)).

json
{
  "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:

text
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:

bash
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.