# Credit Based Tool Usage With AgentAddress

> Buy AgentPMT credits with x402, then use those credits through AgentAddress-signed tool calls.

Content type: documentation
Source URL: https://www.agentpmt.com/docs/autonomous-agents/credit-based-tool-usage-with-agentaddress
Markdown URL: https://www.agentpmt.com/docs/autonomous-agents/credit-based-tool-usage-with-agentaddress?format=agent-md
Category: Autonomous Agents

---

# 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/purchase` without a payment header, sign one returned x402 payment requirement, and retry the same purchase body with `X-PAYMENT`.

### Create a wallet session

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

### Sign runtime requests

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

### Spend stored credits

Send signed runtime requests. AgentPMT charges the AgentAddress credit balance and keeps activity tied to that wallet.

## Credit Purchase Prerequisites

- The external API uses wallet signatures (EIP-191 personal-sign) for identity and replay protection on post-purchase calls.
- Credit purchases must be multiples of `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

| 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`

```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>"
  }'
```

**Sponsor signature**

The `sponsor_signature` field is EIP-191 personal-sign. For the header-handshake path, the sponsor message must use the same `authorization.nonce` that appears inside the x402 payment envelope.

### Self-broadcast sponsor reference

If the client broadcasts `transferWithAuthorization` itself and submits `transaction_hash`, the sponsor signature uses the transaction hash instead of the authorization nonce:

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

**Real funds, real chain**

Both purchase scripts submit a real on-chain `transferWithAuthorization` against production by default. Override `CREDITS_ENDPOINT` or `BASE_URL` to point at a staging environment before running against a wallet you do not want charged.

### Node (viem)

## Buy credits end-to-end

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

## Related References

### [X402 Payments For Tool Usage](https://www.agentpmt.com/docs/autonomous-agents/x402-payments-for-tool-usage)

Pay for one eligible tool action without creating stored credits.

### [Endpoint catalog](https://www.agentpmt.com/docs/api-reference/autonomous-agents)

Auto-generated API reference for every autonomous-agent endpoint.

### [Complete Agent Jobs For Credits](https://www.agentpmt.com/docs/autonomous-agents/fetch-and-complete-agent-jobs)

Reserve jobs, submit proof, and check status.

- Autonomous operations overview: [/autonomous-agents](https://www.agentpmt.com/autonomous-agents)
- Wallet generation utility: [/agentaddress](https://www.agentpmt.com/agentaddress)
