AgentPMT

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

Credit Based Tool Usage With AgentAddress

Use this guide when an autonomous agent should buy reusable AgentPMT credits and then spend those credits through wallet-signed tool calls. This is the persistent AgentAddress path: activity stays attached to the agent wallet, the agent can reuse files and stored data between tool calls, more AgentPMT tools are available, and repeated calls do not require a new token payment handshake each time.

When to Use AgentAddress Credits

Buy credits before runtime calls when the agent needs:

  • Persistent spend linked to the AgentAddress.
  • Reusable files, stored data, jobs, and other AgentPMT state between tool calls.
  • Access to credit-backed tools and platform features that require stored credits.
  • A reusable balance for repeated calls.

Credit Usage Flow

Buy credits with x402

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

Chain
Base
Chain ID
8453
CAIP-2
eip155:8453
Token
USDC
Contract
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Decimals
6
EIP-712 Name
USD Coin
EIP-712 Version
2
Chain
Base
Chain ID
8453
CAIP-2
eip155:8453
Token
EURC
Contract
0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42
Decimals
6
EIP-712 Name
EURC
EIP-712 Version
2
Chain
Arbitrum
Chain ID
42161
CAIP-2
eip155:42161
Token
USDC
Contract
0xaf88d065e77c8cC2239327C5EDb3A432268e5831
Decimals
6
EIP-712 Name
USD Coin
EIP-712 Version
2
Chain
Optimism
Chain ID
10
CAIP-2
eip155:10
Token
USDC
Contract
0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
Decimals
6
EIP-712 Name
USD Coin
EIP-712 Version
2
Chain
Polygon
Chain ID
137
CAIP-2
eip155:137
Token
USDC
Contract
0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Decimals
6
EIP-712 Name
USD Coin
EIP-712 Version
2
Chain
Avalanche
Chain ID
43114
CAIP-2
eip155:43114
Token
USDC
Contract
0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
Decimals
6
EIP-712 Name
USD Coin
EIP-712 Version
2
Chain
Avalanche
Chain ID
43114
CAIP-2
eip155:43114
Token
EURC
Contract
0xc891eb4cbdeff6e073e859e987815ed1505c2acd
Decimals
6
EIP-712 Name
Euro Coin
EIP-712 Version
2

x402 Credit Purchase Contract

Autonomous agents cannot complete a credit purchase without the exact request/response contract below. The decoded JSON payloads show the x402 challenge, signed envelope, and settlement response field names.

1. Request the payment challenge

Send the initial POST without a payment header to receive a 402 Payment Required response. The challenge arrives both as a JSON body and as the base64-encoded PAYMENT-REQUIRED header that carries the machine-readable accepts list.

Initial request body

{
  "wallet_address": "0xyouragentwallet...",
  "credits": 500,
  "payment_method": "x402"
}

Decoded PAYMENT-REQUIRED header payload

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "https://www.agentpmt.com/api/external/credits/purchase",
    "description": "Purchase AgentPMT Credits",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "5000000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0xagentpmtcollectorwallet...",
      "maxTimeoutSeconds": 300,
      "extra": {
        "name": "USD Coin",
        "version": "2",
        "resourceUrl": "https://www.agentpmt.com/api/external/credits/purchase"
      }
    }
  ]
}

2. Sign the authorization and retry

Pick an acceptance, derive the EIP-712 domain from its fields, sign the TransferWithAuthorization typed data with the payer wallet, and retry the request with the base64-encoded envelope in the X-PAYMENT header.

Field
name
Source
acceptance.extra.name
Example
"USD Coin"
Notes
EIP-712 domain name as minted by the token contract (`name()`).
Field
version
Source
acceptance.extra.version
Example
"2"
Notes
EIP-712 domain version (`version()` on the token contract).
Field
chainId
Source
parse(acceptance.network)
Example
8453
Notes
Strip the `eip155:` prefix from the CAIP-2 network id and parse the integer that follows.
Field
verifyingContract
Source
acceptance.asset
Example
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
Notes
The token contract address. Must match the asset the agent is paying with.

EIP-712 types

{
  "TransferWithAuthorization": [
    {
      "name": "from",
      "type": "address"
    },
    {
      "name": "to",
      "type": "address"
    },
    {
      "name": "value",
      "type": "uint256"
    },
    {
      "name": "validAfter",
      "type": "uint256"
    },
    {
      "name": "validBefore",
      "type": "uint256"
    },
    {
      "name": "nonce",
      "type": "bytes32"
    }
  ]
}

Signed envelope (before base64 encoding)

{
  "x402Version": 2,
  "scheme": "exact",
  "network": "eip155:8453",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "payload": {
    "signature": "0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334455667788991b",
    "authorization": {
      "from": "0xyouragentwallet...",
      "to": "0xagentpmtcollectorwallet...",
      "value": "5000000",
      "validAfter": "0",
      "validBefore": "1777057080",
      "nonce": "0xc244d760baf20000000000000000000000000000000000000000000000000000"
    }
  }
}

3. Read the settlement response

On success the route returns 200 with the updated balance and sets the base64 PAYMENT-RESPONSE header so clients can parse the on-chain transaction hash and the acceptance the broadcast satisfied.

Response body

{
  "message": "Credits purchased successfully",
  "wallet_address": "0xyouragentwallet...",
  "balance_credits": 1500,
  "balance_usd": 15
}

Decoded PAYMENT-RESPONSE header payload

{
  "success": true,
  "transaction": "0xaa37bd14ff0f17d20ef9988b86c369e2615a20ed2948dd74b8423378f93ff267",
  "network": "eip155:8453",
  "payer": "0xyouragentwallet...",
  "requirements": {
    "scheme": "exact",
    "network": "eip155:8453",
    "amount": "5000000",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0xagentpmtcollectorwallet...",
    "maxTimeoutSeconds": 300,
    "extra": {
      "name": "USD Coin",
      "version": "2",
      "resourceUrl": "https://www.agentpmt.com/api/external/credits/purchase"
    }
  }
}
Status
200
Label
Success
Summary
Broadcast confirmed and backend credited the account.
Agent action
Record the transaction hash, treat credits as available, and resume tool invocations.
Status
202
Label
Pending
Summary
Broadcast submitted but the required confirmations have not accrued on the self-broadcast path.
Agent action
Retry the same request with an idempotent request_id until 200 or 400 is returned.
Status
400
Label
Rejected
Summary
Validation, pack-size, signature, or on-chain revert failure. The error field is safe to surface to the caller.
Agent action
Do not retry blindly. Surface the error text, correct the input, and submit a fresh request_id.
Status
500
Label
Transient server error
Summary
Infrastructure, Circle broadcast, or upstream configuration failure. Agents must treat this as retryable with backoff.
Agent action
Back off exponentially, keep the original request_id (so a repeated on-chain broadcast is deduplicated), and retry a bounded number of times.

Autonomous Agent Self-Purchase

Use this path when the agent wallet holds USDC and is allowed to buy its own credits directly.

Request payment requirements

curl -i -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
  -H "Content-Type: application/json" \
  -d '{ "wallet_address":"0xAGENT_WALLET", "credits": 500, "payment_method":"x402" }'

The server responds 402 Payment Required. Decode the PAYMENT-REQUIRED header or read the response body, select an acceptable network/token requirement, and sign its EIP-712 TransferWithAuthorization payload.

Sign the returned authorization and retry

Pick an acceptance from accepts[], build the EIP-712 domain from acceptance.extra plus the CAIP-2 chain id, sign the TransferWithAuthorization typed data with the agent wallet key, base64-encode the envelope, and retry the original POST with the header set.

curl -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-envelope>" \
  -d '{ "wallet_address":"0xAGENT_WALLET", "credits": 500, "payment_method":"x402" }'

Human-Sponsored Credit Purchase

Use this path when a human pays from a separate wallet but wants credits allocated to the agent wallet.

  • wallet_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.

curl -i -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet_address":"0xAGENT_WALLET",
    "credits": 500,
    "payment_method":"x402",
    "payer_wallet_address":"0xHUMAN_WALLET"
  }'

Build x402 authorization and sponsor message

Select one returned accepts[] entry and generate the EIP-3009 authorization for the payer wallet. Use the same authorization.nonce in the sponsor message:

agentpmt-external-sponsor
payer:0xhuman_wallet_lower...
recipient:0xagent_wallet_lower...
credits:500
nonce:0x<same-nonce-as-authorization>

The human payer signs this sponsor message with EIP-191 personal-sign. The payer also signs the EIP-712 TransferWithAuthorization payment authorization.

Retry with payment and sponsor signatures

curl -s -X POST "https://www.agentpmt.com/api/external/credits/purchase" \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-envelope>" \
  -d '{
    "wallet_address":"0xAGENT_WALLET",
    "credits": 500,
    "payment_method":"x402",
    "payer_wallet_address":"0xHUMAN_WALLET",
    "sponsor_signature":"0x<signature-by-human-wallet>"
  }'
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:

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

scripts/docs-reference/buy-credits-example.mjs

// Reference implementation: x402 credit purchase for autonomous agents.
//
// This script exercises the full `/api/external/credits/purchase` flow:
//   1. POST without a payment header to receive the 402 `PAYMENT-REQUIRED`
//      challenge.
//   2. Parse the challenge, pick a supported acceptance (Base USDC here),
//      and sign an EIP-3009 `transferWithAuthorization` authorization via
//      EIP-712 typed-data signing.
//   3. Base64-encode the signed envelope and retry the POST with the
//      `X-PAYMENT` header set.
//
// Environment:
//   TEST_WALLET           JSON literal of the form
//                         {"address":"0x...","private_key":"0x..."}
//                         (64-hex private key, 0x-prefixed or not).
//   CREDITS_ENDPOINT      Optional override. Defaults to production.
//   PURCHASE_CREDITS      Optional override. Defaults to 500.
//   NETWORK_CAIP2         Optional override. Defaults to "eip155:8453".
//   TOKEN_ASSET           Optional override. Defaults to Base USDC
//                         (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).
//
// Node >= 22. Dependencies: viem.

import { privateKeyToAccount } from "viem/accounts";

const ENDPOINT =
  process.env.CREDITS_ENDPOINT ||
  "https://www.agentpmt.com/api/external/credits/purchase";
const CREDITS = Number(process.env.PURCHASE_CREDITS || 500);
const NETWORK = process.env.NETWORK_CAIP2 || "eip155:8453";
const DEFAULT_TOKEN_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";

const TRANSFER_WITH_AUTHORIZATION_TYPES = {
  TransferWithAuthorization: [
    { name: "from", type: "address" },
    { name: "to", type: "address" },
    { name: "value", type: "uint256" },
    { name: "validAfter", type: "uint256" },
    { name: "validBefore", type: "uint256" },
    { name: "nonce", type: "bytes32" },
  ],
};

function loadWallet() {
  const raw = process.env.TEST_WALLET;
  if (!raw) {
    throw new Error(
      "TEST_WALLET env var is required (JSON with address + private_key).",
    );
  }
  const parsed = JSON.parse(raw);
  const address = String(parsed.address || "").toLowerCase();
  const pkRaw = String(parsed.private_key || "");
  const privateKey = pkRaw.startsWith("0x") ? pkRaw : `0x${pkRaw}`;
  if (!/^0x[a-fA-F0-9]{64}$/.test(privateKey)) {
    throw new Error("TEST_WALLET.private_key must be a 32-byte hex string.");
  }
  if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
    throw new Error("TEST_WALLET.address must be a 20-byte hex address.");
  }
  return { address, privateKey };
}

function loadTokenAsset() {
  const tokenAsset = String(process.env.TOKEN_ASSET || DEFAULT_TOKEN_ASSET).toLowerCase();
  if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAsset)) {
    throw new Error("TOKEN_ASSET must be a 20-byte hex token contract address.");
  }
  return tokenAsset;
}

function b64encodeJson(value) {
  return Buffer.from(JSON.stringify(value)).toString("base64");
}

function b64decodeJson(value) {
  return JSON.parse(Buffer.from(value, "base64").toString("utf8"));
}

function caip2ToChainId(network) {
  // CAIP-2 identifiers look like `eip155:<chainId>`. The purchase endpoint
  // advertises chains in this form.
  const match = /^eip155:(\d+)$/.exec(network);
  if (!match) {
    throw new Error(`Unsupported network identifier: ${network}`);
  }
  return Number(match[1]);
}

async function main() {
  const wallet = loadWallet();
  const tokenAsset = loadTokenAsset();
  const account = privateKeyToAccount(wallet.privateKey);
  if (account.address.toLowerCase() !== wallet.address) {
    throw new Error(
      `Address mismatch: env=${wallet.address} derived=${account.address.toLowerCase()}`,
    );
  }
  console.log("payer:", account.address);

  // Step 1: trigger the 402 challenge.
  const initialBody = {
    wallet_address: account.address.toLowerCase(),
    credits: CREDITS,
    payment_method: "x402",
  };
  const challenge = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(initialBody),
  });
  if (challenge.status !== 402) {
    throw new Error(
      `Expected 402 challenge, got ${challenge.status}: ${await challenge.text()}`,
    );
  }
  const required =
    challenge.headers.get("payment-required") || challenge.headers.get("PAYMENT-REQUIRED");
  if (!required) {
    throw new Error("PAYMENT-REQUIRED header missing from 402 response");
  }
  const requiredPayload = b64decodeJson(required);
  const acceptance = requiredPayload.accepts.find(
    (a) =>
      a.network === NETWORK && String(a.asset || "").toLowerCase() === tokenAsset,
  );
  if (!acceptance) {
    throw new Error(`No acceptance for network ${NETWORK} and asset ${tokenAsset}`);
  }
  if (!acceptance.extra?.name || !acceptance.extra?.version) {
    throw new Error("Selected acceptance is missing EIP-712 domain metadata.");
  }
  console.log(
    `acceptance: asset=${acceptance.asset} network=${acceptance.network} ` +
      `amount=${acceptance.amount} payTo=${acceptance.payTo}`,
  );

  // Step 2: sign the EIP-3009 authorization via EIP-712 typed data.
  const nowSeconds = Math.floor(Date.now() / 1000);
  const validAfter = 0n;
  const validBefore = BigInt(
    nowSeconds + Math.min(240, acceptance.maxTimeoutSeconds - 60),
  );
  const nonceBytes = crypto.getRandomValues(new Uint8Array(32));
  const nonce = `0x${Array.from(nonceBytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;

  const domain = {
    name: acceptance.extra.name,
    version: acceptance.extra.version,
    chainId: caip2ToChainId(acceptance.network),
    verifyingContract: acceptance.asset,
  };
  const message = {
    from: account.address,
    to: acceptance.payTo,
    value: BigInt(acceptance.amount),
    validAfter,
    validBefore,
    nonce,
  };
  const signature = await account.signTypedData({
    domain,
    types: TRANSFER_WITH_AUTHORIZATION_TYPES,
    primaryType: "TransferWithAuthorization",
    message,
  });

  // Step 3: retry with the base64-encoded envelope in the `X-PAYMENT` header.
  const envelope = {
    x402Version: 2,
    scheme: "exact",
    network: acceptance.network,
    asset: acceptance.asset,
    payload: {
      signature,
      authorization: {
        from: account.address,
        to: acceptance.payTo,
        value: acceptance.amount,
        validAfter: validAfter.toString(),
        validBefore: validBefore.toString(),
        nonce,
      },
    },
  };
  const xPaymentHeader = b64encodeJson(envelope);

  const settlement = await fetch(ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-PAYMENT": xPaymentHeader,
    },
    body: JSON.stringify(initialBody),
  });
  const settlementBody = await settlement.text();
  console.log("settlement status:", settlement.status);
  console.log("settlement body  :", settlementBody);
  const paymentResponseHeader =
    settlement.headers.get("payment-response") || settlement.headers.get("PAYMENT-RESPONSE");
  if (paymentResponseHeader) {
    console.log(
      "settlement header:",
      JSON.stringify(b64decodeJson(paymentResponseHeader), null, 2),
    );
  }

  if (settlement.status !== 200) {
    process.exitCode = 1;
  }
}

main().catch((error) => {
  console.error("FATAL:", error?.stack || error);
  process.exitCode = 1;
});

Check credit balance

scripts/docs-reference/check-credits-balance-example.mjs

// Reference implementation: signed credit-balance query for autonomous agents.
//
// Uses the two-step EIP-191 flow:
//   1. POST /api/external/auth/session with the wallet address to obtain a
//      session_nonce.
//   2. Build the canonical signing message, sign it with personal-sign, and
//      POST /api/external/credits/balance with the envelope fields.
//
// Environment:
//   TEST_WALLET        JSON literal {"address":"0x...","private_key":"0x..."}
//   BASE_URL           Optional override. Defaults to production.
//
// Node >= 22. Dependencies: viem.

import { privateKeyToAccount } from "viem/accounts";

const BASE_URL = process.env.BASE_URL || "https://www.agentpmt.com";

function loadWallet() {
  const raw = process.env.TEST_WALLET;
  if (!raw) {
    throw new Error("TEST_WALLET env var is required (JSON with address + private_key).");
  }
  const parsed = JSON.parse(raw);
  const address = String(parsed.address || "").toLowerCase();
  const pkRaw = String(parsed.private_key || "");
  const privateKey = pkRaw.startsWith("0x") ? pkRaw : `0x${pkRaw}`;
  if (!/^0x[a-fA-F0-9]{64}$/.test(privateKey)) {
    throw new Error("TEST_WALLET.private_key must be a 32-byte hex string.");
  }
  return { address, privateKey };
}

async function main() {
  const wallet = loadWallet();
  const account = privateKeyToAccount(wallet.privateKey);

  // 1) Open a wallet session to receive a session nonce.
  const sessionResp = await fetch(`${BASE_URL}/api/external/auth/session`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ wallet_address: wallet.address }),
  });
  if (!sessionResp.ok) {
    throw new Error(`Session creation failed: ${sessionResp.status} ${await sessionResp.text()}`);
  }
  const { session_nonce: sessionNonce } = await sessionResp.json();
  if (!sessionNonce) {
    throw new Error("Session response missing session_nonce");
  }

  // 2) Build the canonical EIP-191 message. The balance action uses an
  //    empty payload and a dash placeholder for the product field.
  const requestId = crypto.randomUUID();
  const canonicalMessage = [
    "agentpmt-external",
    `wallet:${wallet.address}`,
    `session:${sessionNonce}`,
    `request:${requestId}`,
    "action:balance",
    "product:-",
    "payload:",
  ].join("\n");
  const signature = await account.signMessage({ message: canonicalMessage });

  const balanceResp = await fetch(`${BASE_URL}/api/external/credits/balance`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      wallet_address: wallet.address,
      session_nonce: sessionNonce,
      request_id: requestId,
      signature,
    }),
  });
  console.log("balance status:", balanceResp.status);
  console.log("balance body  :", await balanceResp.text());
  if (!balanceResp.ok) {
    process.exitCode = 1;
  }
}

main().catch((error) => {
  console.error("FATAL:", error?.stack || error);
  process.exitCode = 1;
});

Python (eth_account)

Buy credits end-to-end

scripts/docs-reference/buy_credits_example.py

"""Reference implementation: x402 credit purchase for autonomous agents.

Mirrors ``buy-credits-example.mjs`` so Python-based agents can follow an
identical flow without reading TypeScript source.

Environment:
    TEST_WALLET           JSON literal ``{"address": "0x...", "private_key": "0x..."}``.
    CREDITS_ENDPOINT      Optional override. Defaults to production.
    PURCHASE_CREDITS      Optional override. Defaults to 500.
    NETWORK_CAIP2         Optional override. Defaults to ``eip155:8453``.
    TOKEN_ASSET           Optional override. Defaults to Base USDC
                          (``0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913``).

Dependencies: ``eth_account>=0.11``, ``requests>=2.32``. Python 3.11+.
"""

from __future__ import annotations

import base64
import json
import os
import re
import secrets
import sys
import time
from typing import Any

import requests
from eth_account import Account
from eth_account.messages import encode_typed_data


DEFAULT_ENDPOINT = "https://www.agentpmt.com/api/external/credits/purchase"
DEFAULT_TOKEN_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"

TRANSFER_WITH_AUTHORIZATION_TYPES = {
    "TransferWithAuthorization": [
        {"name": "from", "type": "address"},
        {"name": "to", "type": "address"},
        {"name": "value", "type": "uint256"},
        {"name": "validAfter", "type": "uint256"},
        {"name": "validBefore", "type": "uint256"},
        {"name": "nonce", "type": "bytes32"},
    ],
}


def _load_wallet() -> dict[str, str]:
    raw = os.environ.get("TEST_WALLET")
    if not raw:
        raise SystemExit("TEST_WALLET env var is required.")
    parsed = json.loads(raw)
    address = str(parsed["address"]).lower()
    pk = str(parsed["private_key"])
    if not pk.startswith("0x"):
        pk = "0x" + pk
    if not re.fullmatch(r"0x[0-9a-fA-F]{64}", pk):
        raise SystemExit("TEST_WALLET.private_key must be a 32-byte hex value.")
    if not re.fullmatch(r"0x[0-9a-fA-F]{40}", address):
        raise SystemExit("TEST_WALLET.address must be a 20-byte hex address.")
    return {"address": address, "private_key": pk}


def _load_token_asset() -> str:
    token_asset = os.environ.get("TOKEN_ASSET", DEFAULT_TOKEN_ASSET).lower()
    if not re.fullmatch(r"0x[0-9a-fA-F]{40}", token_asset):
        raise SystemExit("TOKEN_ASSET must be a 20-byte hex token contract address.")
    return token_asset


def _b64_json(value: Any) -> str:
    return base64.b64encode(json.dumps(value, separators=(",", ":")).encode("utf-8")).decode(
        "ascii"
    )


def _parse_required_header(header_value: str) -> dict[str, Any]:
    return json.loads(base64.b64decode(header_value.encode("ascii")).decode("utf-8"))


def _caip2_to_chain_id(network: str) -> int:
    match = re.fullmatch(r"eip155:(\d+)", network)
    if not match:
        raise SystemExit(f"Unsupported network identifier: {network}")
    return int(match.group(1))


def main() -> int:
    endpoint = os.environ.get("CREDITS_ENDPOINT", DEFAULT_ENDPOINT)
    credits = int(os.environ.get("PURCHASE_CREDITS", "500"))
    network = os.environ.get("NETWORK_CAIP2", "eip155:8453")
    token_asset = _load_token_asset()

    wallet = _load_wallet()
    account = Account.from_key(wallet["private_key"])
    if account.address.lower() != wallet["address"]:
        raise SystemExit(
            f"Address mismatch: env={wallet['address']} derived={account.address.lower()}"
        )
    print("payer:", account.address)

    # Step 1: trigger the 402 challenge.
    initial_body: dict[str, Any] = {
        "wallet_address": wallet["address"],
        "credits": credits,
        "payment_method": "x402",
    }
    challenge = requests.post(
        endpoint,
        json=initial_body,
        headers={"Content-Type": "application/json"},
        timeout=30,
    )
    if challenge.status_code != 402:
        raise SystemExit(f"Expected 402 challenge, got {challenge.status_code}: {challenge.text}")
    required_header = challenge.headers.get("PAYMENT-REQUIRED") or challenge.headers.get(
        "payment-required"
    )
    if not required_header:
        raise SystemExit("PAYMENT-REQUIRED header missing from 402 response")
    required_payload = _parse_required_header(required_header)

    acceptance = next(
        (
            a
            for a in required_payload["accepts"]
            if a["network"] == network and str(a.get("asset", "")).lower() == token_asset
        ),
        None,
    )
    if not acceptance:
        raise SystemExit(f"No acceptance for network {network} and asset {token_asset}")
    if not acceptance.get("extra", {}).get("name") or not acceptance.get("extra", {}).get(
        "version"
    ):
        raise SystemExit("Selected acceptance is missing EIP-712 domain metadata.")
    print(
        f"acceptance: asset={acceptance['asset']} network={acceptance['network']} "
        f"amount={acceptance['amount']} payTo={acceptance['payTo']}"
    )

    # Step 2: sign the EIP-3009 authorization via EIP-712 typed data.
    now_seconds = int(time.time())
    valid_after = 0
    valid_before = now_seconds + min(240, acceptance["maxTimeoutSeconds"] - 60)
    nonce_hex = "0x" + secrets.token_hex(32)

    typed_data = {
        "types": {
            "EIP712Domain": [
                {"name": "name", "type": "string"},
                {"name": "version", "type": "string"},
                {"name": "chainId", "type": "uint256"},
                {"name": "verifyingContract", "type": "address"},
            ],
            **TRANSFER_WITH_AUTHORIZATION_TYPES,
        },
        "primaryType": "TransferWithAuthorization",
        "domain": {
            "name": acceptance["extra"]["name"],
            "version": acceptance["extra"]["version"],
            "chainId": _caip2_to_chain_id(acceptance["network"]),
            "verifyingContract": acceptance["asset"],
        },
        "message": {
            "from": account.address,
            "to": acceptance["payTo"],
            "value": int(acceptance["amount"]),
            "validAfter": valid_after,
            "validBefore": valid_before,
            "nonce": nonce_hex,
        },
    }
    signable = encode_typed_data(full_message=typed_data)
    signed = account.sign_message(signable)
    signature_hex = signed.signature.hex()
    if not signature_hex.startswith("0x"):
        signature_hex = "0x" + signature_hex

    # Step 3: retry with the base64-encoded envelope in the `X-PAYMENT` header.
    envelope = {
        "x402Version": 2,
        "scheme": "exact",
        "network": acceptance["network"],
        "asset": acceptance["asset"],
        "payload": {
            "signature": signature_hex,
            "authorization": {
                "from": account.address,
                "to": acceptance["payTo"],
                "value": acceptance["amount"],
                "validAfter": str(valid_after),
                "validBefore": str(valid_before),
                "nonce": nonce_hex,
            },
        },
    }
    header_value = _b64_json(envelope)

    settlement = requests.post(
        endpoint,
        json=initial_body,
        headers={"Content-Type": "application/json", "X-PAYMENT": header_value},
        timeout=180,
    )
    print("settlement status:", settlement.status_code)
    print("settlement body  :", settlement.text)
    payment_response = settlement.headers.get("PAYMENT-RESPONSE") or settlement.headers.get(
        "payment-response"
    )
    if payment_response:
        print(
            "settlement header:",
            json.dumps(_parse_required_header(payment_response), indent=2),
        )

    return 0 if settlement.status_code == 200 else 1


if __name__ == "__main__":
    sys.exit(main())

Check credit balance

scripts/docs-reference/check_credits_balance_example.py

"""Reference implementation: signed credit-balance query for autonomous agents.

Mirrors ``check-credits-balance-example.mjs``.

Environment:
    TEST_WALLET        JSON literal ``{"address": "0x...", "private_key": "0x..."}``.
    BASE_URL           Optional override. Defaults to production.

Dependencies: ``eth_account>=0.11``, ``requests>=2.32``. Python 3.11+.
"""

from __future__ import annotations

import json
import os
import sys
import uuid

import requests
from eth_account import Account
from eth_account.messages import encode_defunct


DEFAULT_BASE_URL = "https://www.agentpmt.com"


def _load_wallet() -> dict[str, str]:
    raw = os.environ.get("TEST_WALLET")
    if not raw:
        raise SystemExit("TEST_WALLET env var is required.")
    parsed = json.loads(raw)
    address = str(parsed["address"]).lower()
    pk = str(parsed["private_key"])
    if not pk.startswith("0x"):
        pk = "0x" + pk
    return {"address": address, "private_key": pk}


def main() -> int:
    base_url = os.environ.get("BASE_URL", DEFAULT_BASE_URL)
    wallet = _load_wallet()
    account = Account.from_key(wallet["private_key"])

    # 1) Open a wallet session.
    session_resp = requests.post(
        f"{base_url}/api/external/auth/session",
        json={"wallet_address": wallet["address"]},
        headers={"Content-Type": "application/json"},
        timeout=30,
    )
    if not session_resp.ok:
        raise SystemExit(
            f"Session creation failed: {session_resp.status_code} {session_resp.text}"
        )
    session_nonce = session_resp.json().get("session_nonce")
    if not session_nonce:
        raise SystemExit("Session response missing session_nonce")

    # 2) Build and sign the canonical EIP-191 balance message.
    request_id = str(uuid.uuid4())
    canonical_message = "\n".join(
        [
            "agentpmt-external",
            f"wallet:{wallet['address']}",
            f"session:{session_nonce}",
            f"request:{request_id}",
            "action:balance",
            "product:-",
            "payload:",
        ]
    )
    signable = encode_defunct(text=canonical_message)
    signed = account.sign_message(signable)
    signature_hex = signed.signature.hex()
    if not signature_hex.startswith("0x"):
        signature_hex = "0x" + signature_hex

    balance_resp = requests.post(
        f"{base_url}/api/external/credits/balance",
        json={
            "wallet_address": wallet["address"],
            "session_nonce": session_nonce,
            "request_id": request_id,
            "signature": signature_hex,
        },
        headers={"Content-Type": "application/json"},
        timeout=30,
    )
    print("balance status:", balance_resp.status_code)
    print("balance body  :", balance_resp.text)
    return 0 if balance_resp.ok else 1


if __name__ == "__main__":
    sys.exit(main())

Running the Node scripts

export TEST_WALLET='{"address":"0x...","private_key":"0x..."}'
# optional: choose a specific accepted asset instead of default Base USDC
export TOKEN_ASSET='0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
# purchase flow
node scripts/docs-reference/buy-credits-example.mjs

# balance flow (signed EIP-191)
node scripts/docs-reference/check-credits-balance-example.mjs

Running the Python scripts

python -m venv .venv
source .venv/bin/activate
pip install "eth_account>=0.11" "requests>=2.32"

export TEST_WALLET='{"address":"0x...","private_key":"0x..."}'
export TOKEN_ASSET='0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
python scripts/docs-reference/buy_credits_example.py
python scripts/docs-reference/check_credits_balance_example.py

Use Credits With AgentAddress

After credits are granted, the agent uses wallet signatures for runtime operations.

Create a session nonce

curl -s -X POST "https://www.agentpmt.com/api/external/auth/session" \
  -H "Content-Type: application/json" \
  -d '{ "wallet_address":"0xYOUR_WALLET" }'

The session nonce anchors the wallet session. The API still requires a unique request_id for each signed operation so retried calls cannot be replayed as new work.

Payload hash rules

  • Use the exact request payload object that the endpoint verifies.
  • Do not include signature envelope fields in the hashed payload unless the endpoint explicitly says so. Envelope fields are wallet_address, session_nonce, request_id, and signature.
  • For tool invocation, hash parameters.
  • For empty payload actions, leave the payload: line empty.

Sign invoke message and run a tool with credits

Sign this exact message:

agentpmt-external
wallet:0xyourwallet...
session:<session_nonce>
request:<request_id>
method:POST
path:/external/tools/<productSlug>/actions/<actionSlug>/invoke
payload:<sha256(canonical_json(parameters))>

Invoke request:

curl -s -X POST "https://www.agentpmt.com/api/external/tools/<productSlug>/actions/<actionSlug>/invoke" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet_address":"0xYOUR_WALLET",
    "session_nonce":"<session_nonce>",
    "request_id":"invoke-uuid",
    "signature":"0x<signature>",
    "parameters": {
      "your_param": "value"
    }
  }'

Runtime credential injection

Some tools require runtime credentials. Pass those credentials under parameters._credentials. Because tool invokes hash the entire parameters object, add _credentials before computing sha256(canonical_json(parameters)).

{
  "parameters": {
    "your_param": "value",
    "_credentials": {
      "google_oauth": {
        "access_token": "ya29...",
        "expires_at": "2026-02-17T12:00:00Z"
      }
    }
  }
}

Check credit balance

The balance endpoint is signed identically to every other runtime call. The canonical message has an empty payload line and a dash placeholder for the product field:

agentpmt-external
wallet:0xyourwallet...
session:<session_nonce>
request:balance-uuid
action:balance
product:-
payload:

Sign that message with EIP-191 personal-sign using the agent wallet, then POST the envelope:

curl -s -X POST "https://www.agentpmt.com/api/external/credits/balance" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet_address":"0xYOUR_WALLET",
    "session_nonce":"<session_nonce>",
    "request_id":"balance-uuid",
    "signature":"0x<signature>"
  }'

Operational Guardrails

  • Keep the agent wallet key isolated from any human payer key.
  • Use credit top-ups as bounded spend control for autonomous runtimes.
  • Keep request IDs unique per signed call to prevent replay.
  • Log wallet address, request ID, and action for operational traceability.

Build it yourself, or hire AgentPMT to build it with you.

Let’s turn these ideas into reality. Dive in and build it yourself, or bring in the AgentPMT consulting team for a seamless, end-to-end implementation.

Free to start. Consulting available when you want expert implementation.