Developer Handbook

Agent Payments Handbook

Give your AI agent a post-quantum signed spending budget. It pays autonomously. You stay in control.

ML-DSA-65 · NIST FIPS 204 Airwallex + Wise live · 5 rails 13/13 guardrail tests Arbitrum on-chain audit npm i @pqsafe/agent-pay

Quickstart — 60 seconds

npm install @pqsafe/agent-pay
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js'
import { bytesToHex } from '@noble/hashes/utils.js'
import { createEnvelope, signEnvelope, executeAgentPayment } from '@pqsafe/agent-pay'

// 1. Generate post-quantum keypair (wallet side — keep secretKey private)
const { secretKey, publicKey } = ml_dsa65.keygen(crypto.getRandomValues(new Uint8Array(32)))
const issuer = 'pq1' + bytesToHex(publicKey.slice(0, 20))

// 2. Issue a signed SpendEnvelope — defines what the agent is allowed to spend
const envelope = createEnvelope({
  issuer,
  agent: 'research-agent-v1',
  maxAmount: 50,
  currency: 'USD',
  allowedRecipients: ['perplexity.ai'],
  ttlSeconds: 3600,
  rail: 'airwallex',
})

const signed = signEnvelope(envelope, secretKey, publicKey)

// 3. Agent calls this — verifies PQ sig + enforces all limits before paying
const result = await executeAgentPayment(signed, {
  recipient: 'perplexity.ai',
  amount: 20,
  memo: 'Perplexity Pro — research task',
})

console.log(result.txId)  // real Airwallex sandbox UUID

The SpendEnvelope concept

A SpendEnvelope is a signed JSON token that encodes exactly what an AI agent is authorized to spend. It is the authorization — no centralized server, no API key delegation, no human-in-the-loop approval per payment.

versionMust be 1
issuerPQSafe wallet address of the human owner (pq1…)
agentIdentifier string for the AI agent ("research-agent-v1")
maxAmountMaximum spend ceiling — SDK rejects anything above this
currencyISO 4217 code: USD, HKD, EUR
allowedRecipientsAllowlist — agent cannot pay anyone not on this list
validFrom / validUntilUnix timestamps — time-bounded authorization window
nonce128-bit random hex — prevents replay attacks
railOptional: airwallex | wise | stripe | usdc-base | x402

The entire envelope is signed with ML-DSA-65 (NIST FIPS 204 lattice-based signature, 128-bit post-quantum security). The signature covers all fields deterministically — any field alteration invalidates the signature. The agent is cryptographically prevented from exceeding its budget.

Installation

TypeScript / Node.js SDK (ES2022, ESM, Node 18+):

npm install @pqsafe/agent-pay @noble/post-quantum @noble/hashes

Python SDK (Python 3.10+):

pip install pqsafe        # short form, pulls pqsafe-agent-pay
pip install pqsafe-agent-pay  # explicit, same SDK

Source at github.com/PQSafe/pqsafe — MIT license.

Create & sign an envelope

import { createEnvelope, signEnvelope } from '@pqsafe/agent-pay'

// Build unsigned envelope
const envelope = createEnvelope({
  issuer,               // 'pq1' + 40 hex chars
  agent: 'my-agent-v1',
  maxAmount: 100,
  currency: 'USD',
  allowedRecipients: ['anthropic.com/billing', 'openai.com'],
  startsInSeconds: 0,   // valid immediately (default)
  ttlSeconds: 3600,     // valid for 1 hour (default)
  rail: 'airwallex',       // optional — omit to let router choose
})

// Sign with issuer's ML-DSA-65 secret key
const signed = signEnvelope(envelope, secretKey, publicKey)
// → { envelopeJson, signature, dsaPublicKey }

The signed object is safe to hand to the agent process — it contains only the envelope JSON, signature, and public key. The secret key never needs to leave the wallet.

Verify & execute payment (agent side)

import { executeAgentPayment } from '@pqsafe/agent-pay'

const result = await executeAgentPayment(signed, {
  recipient: 'anthropic.com/billing',   // must be in allowedRecipients
  amount: 20,                              // must be ≤ maxAmount
  memo: 'Anthropic API credits — Oct 2026',
})

console.log(result.txId)       // Airwallex transfer UUID
console.log(result.success)    // true
console.log(result.executedAt) // ISO 8601

executeAgentPayment runs all checks internally — signature verification, schema validation, temporal validity, allowlist, amount ceiling — then routes to the configured rail. It throws on any violation before attempting any network call to the payment provider.

Guardrails enforced

CheckWhat it prevents
ML-DSA-65 signatureForged envelopes, tampered fields, wrong signer
Schema validationMalformed or incomplete envelopes
validFrom checkPre-activated envelopes used too early
validUntil checkExpired envelopes reused after TTL
allowedRecipientsPayments to unauthorized addresses
maxAmount ceilingOver-spend attacks by a compromised agent
nonce (128-bit)Replay attacks — nonce is part of the signed payload

All 13 guardrail tests run on every commit. See tests/envelope.test.ts.

Payment rails

Rail keyProviderStatusUse case
airwallex Airwallex Live sandbox USD / multi-currency wire, ACH, LOCAL
stripe Stripe Mock ready Invoice payment (in_xxx), PaymentIntent confirm (pi_xxx), payment link
wise Wise Business Live sandbox IBAN / sort code / ABA — mid-market rate, international
usdc-base Coinbase CDP / Base Mock ready USDC on Base L2 — inject viem/ethers/CDP AgentKit signer
x402 HTTP 402 / Coinbase x402 Mock ready HTTP 402 micropayments — probe endpoint, validate requirements, pay

To contribute a rail implementation, add a file to agent-pay/src/rails/ and register it in rails/index.ts.

Configuration

Set via environment variables or setAgentPayConfig():

# Airwallex sandbox (demo)
AIRWALLEX_CLIENT_ID=your_client_id
AIRWALLEX_API_KEY=your_api_key
AIRWALLEX_ENV=demo         # 'demo' for sandbox, 'production' for live

# Mock mode — no real API calls (default when creds absent)
PQSAFE_MOCK_MODE=1

When PQSAFE_MOCK_MODE=1 or credentials are absent, the SDK returns a realistic mock PaymentResult without hitting any payment provider. All signing, verification, and guardrails still run — only the final wire call is mocked. Useful for integration testing.

Wise rail

WISE_API_KEY=your_wise_api_key
WISE_ENV=sandbox         # 'live' for production

Wise auto-detects recipient format: IBAN (GB29NWBK…), UK sort code (60-00-01/12345678), or US ABA (021000021/12345678).

Telegram approval gate

TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
TELEGRAM_CHAT_ID=123456789
PQSAFE_APPROVAL_THRESHOLD=100    # USD — above this, require human approval
PQSAFE_APPROVAL_TIMEOUT_S=300    # seconds to wait before auto-reject
import { executeWithApproval } from '@pqsafe/agent-pay'

// Payments ≤ $100 → autonomous. Payments > $100 → Telegram [APPROVE][REJECT]
const result = await executeWithApproval(signed, {
  recipient: 'vendor@company.com',
  amount: 150,
  memo: 'Q2 supplier invoice',
}, { autoApproveThreshold: 100 })

USDC-Base rail

Send USDC stablecoins on Coinbase Base. PQSafe handles policy enforcement — wire in your own EVM signer.

BASE_NETWORK=sepolia         # 'mainnet' for production
import { executeAgentPayment } from '@pqsafe/agent-pay'
import { createWalletClient, http } from 'viem'
import { base } from 'viem/chains'

// Your EVM wallet (viem, ethers, or CDP AgentKit)
const walletClient = createWalletClient({ account, chain: base, transport: http() })

// Envelope with USDC currency + usdc-base rail
const envelope = createEnvelope({
  agent: 'my-agent',
  maxAmount: 500,
  currency: 'USDC',
  allowedRecipients: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'],
  rail: 'usdc-base',
})
const signed = signEnvelope(envelope, secretKey, publicKey)

// Execute — PQSafe enforces policy, viem signs and broadcasts
const result = await executeAgentPayment(signed, {
  recipient: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  amount: 100,
  memo: 'USDC payment via Base',
}, {
  usdcBase: {
    network: 'mainnet',
    signAndSend: async ({ to, data }) => walletClient.sendTransaction({ to, data }),
  }
})
console.log(result.txId)  // 0x... Base transaction hash

With Coinbase CDP AgentKit: replace the viem signAndSend with agentkit.sendTransaction({ to, data, network: 'base-mainnet' }). USDC contract addresses: mainnet 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, Sepolia 0x036CbD53842c5426634e7929541eC2318f3dCF7e.

LangChain integration

pip install langchain-pqsafe
from langchain_pqsafe import PQSafePaymentTool
from langchain.agents import AgentExecutor, create_react_agent

tool = PQSafePaymentTool(signed_envelope=signed_envelope_json)
executor = AgentExecutor(agent=create_react_agent(llm, [tool]), tools=[tool])

result = executor.invoke({
    "input": "Renew Perplexity Pro at perplexity.ai — $20 USD for research-agent-v1"
})

Full guide: plugins/langchain-pqsafe

CrewAI integration

pip install crewai-pqsafe
from crewai_pqsafe import PQSafePaymentTool
from crewai import Agent, Task, Crew

finance_agent = Agent(
    role='Finance Officer',
    goal='Execute authorized payments autonomously',
    tools=[PQSafePaymentTool(signed_envelope=signed_envelope_json)],
)

pay_task = Task(
    description='Renew Perplexity Pro subscription at perplexity.ai — $20 USD.',
    agent=finance_agent,
    expected_output='Payment confirmation with txId and rail',
)

Crew(agents=[finance_agent], tasks=[pay_task]).kickoff()

Full guide: plugins/crewai-pqsafe

Mastra integration

npm install mastra-pqsafe
import { createPQSafeIntegration } from 'mastra-pqsafe'

const pqsafe = createPQSafeIntegration()

const result = await pqsafe.pay(signedEnvelope, {
  recipient: 'anthropic.com/billing',
  amount: 20,
  memo: 'Perplexity Pro — research agent auto-renewal',
})

Full guide: plugins/mastra-pqsafe

MCP server — Claude Code, Claude Desktop, Cursor

The PQSafe MCP server exposes payment tools to any MCP-compatible AI agent. Claude Code can pay for its own API credits.

Connect to Claude Desktop / Claude Code

// ~/.claude/claude_desktop_config.json  (or Claude Code settings)
{
  "mcpServers": {
    "pqsafe": {
      "url": "https://mcp.pqsafe.xyz/mcp"
    }
  }
}

Once connected, Claude has 4 payment tools:

ToolWhat it does
pqsafe_create_envelopeBuild a SpendEnvelope (unsigned — operator must sign)
pqsafe_payVerify + execute payment via Airwallex
pqsafe_check_balanceRead envelope constraints without paying
pqsafe_commit_onchainReturn on-chain commit IDs for Arbitrum registry

The killer use case

# Claude Code running autonomously hits API rate limit
You: "Keep working — you have a $50 PQSafe envelope for API credits."
Claude: I see my Anthropic credits are depleted.
Calling pqsafe_pay → anthropic.com/billing, $20 USD...
✓ txId: awx_sbx_abc123 — credits topped up. Resuming task.
Self-hosted (full Airwallex integration): Run npm run build in mcp-server/ and deploy as a Cloudflare Worker with your AIRWALLEX_CLIENT_ID and AIRWALLEX_API_KEY env vars. Source: mcp-server/

OpenHands / Devin / AutoGen

Any agent that supports MCP tool calls, or that can import TypeScript/Python SDKs, can use PQSafe:

// OpenHands custom tool
import { executeAgentPayment } from '@pqsafe/agent-pay'

// Register as tool in your agent framework
const pqsafePayTool = {
  name: 'pay',
  execute: async ({ recipient, amount, memo }) =>
    executeAgentPayment(signedEnvelope, { recipient, amount, memo })
}

Arbitrum on-chain audit layer

Arbitrum Trailblazer AI Grant — D1/D2/D3 complete

Every PQSafe payment can be anchored on-chain by committing the SpendEnvelope hash + ML-DSA-65 signature fingerprint to the SpendEnvelopeRegistry contract on Arbitrum One.

This creates an immutable, publicly auditable record. Any compliance officer can verify — without trusting PQSafe's servers — that a payment was pre-authorized by an operator who held the post-quantum private key.

Deploy the registry

# Install Foundry
curl -L https://foundry.paradigm.xyz | bash && foundryup

# Install + build
cd evm && forge install foundry-rs/forge-std && forge build

# Run tests (13 tests — 11 unit + 2 fuzz)
forge test -vv

# Deploy to Arbitrum Sepolia
export PRIVATE_KEY=0x...
export ARBITRUM_SEPOLIA_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
forge script script/Deploy.s.sol --rpc-url arbitrum_sepolia --broadcast --verify

Commit from TypeScript

import { keccak_256 } from '@noble/hashes/sha3.js'
import {
  createEnvelope, signEnvelope, executeAgentPayment,
  commitEnvelopeToArbitrum,
} from '@pqsafe/agent-pay'

// After payment executes:
const onchain = await commitEnvelopeToArbitrum(signed, envelope, {
  rpcUrl: process.env.ARBITRUM_RPC_URL,
  contractAddress: process.env.ARBITRUM_CONTRACT_ADDRESS,
  privateKey: process.env.ARBITRUM_PRIVATE_KEY,
  chainId: 421614, // Arbitrum Sepolia
  keccak256: (data) => keccak_256(data),
  signTx: async (tx, pk) => {
    // inject viem or ethers for tx signing
    const { signTransaction } = await import('viem/accounts')
    return signTransaction({ ...tx, privateKey: pk })
  },
})

console.log('Arbitrum TX:', onchain.txHash)
console.log('Envelope ID:', onchain.envelopeId)

Claude Agents integration (D3)

Run the complete demo — Claude receives a task, verifies the SpendEnvelope, executes payment, and commits on-chain autonomously:

# No Anthropic key — shows tool call flow only
npm run demo:claude

# With real Claude agent
ANTHROPIC_API_KEY=sk-... npm run demo:claude

# Full stack: real payment + real Arbitrum
ANTHROPIC_API_KEY=... AIRWALLEX_CLIENT_ID=... AIRWALLEX_API_KEY=... \
  ARBITRUM_RPC_URL=... ARBITRUM_PRIVATE_KEY=... ARBITRUM_CONTRACT_ADDRESS=... \
  npm run demo:claude

Contract interface

FunctionWho callsPurpose
commit(envelopeId, sigFingerprint, agent, maxAmount, currency, validUntil, nonce)OperatorPre-authorize a payment on-chain
markUsed(envelopeId, txReference, amountUsed)OperatorRecord Airwallex txId + actual amount after payment executes
isCommitted(envelopeId)AnyoneCheck if payment was pre-authorized
getRecord(envelopeId)AnyoneFull audit record including operator, cap, currency, sig fingerprint
Why Arbitrum? ~$0.01 gas per commit() vs ~$5 on mainnet. 250ms finality — suitable for real-time payment flows. EVM-equivalent. See evm/README.md for deployment instructions.

Public audit ledger

After each successful payment, the SDK can optionally submit an anonymized record to ledger.pqsafe.xyz. Opt-in via environment variables. No PII, no exact amounts, no recipient addresses.

PQSAFE_LEDGER_URL=https://ledger.pqsafe.xyz
PQSAFE_LEDGER_API_KEY=your-ledger-api-key  # from your PQSafe account

What is logged per payment:

FieldValueReversible?
envelopeHashSHA-256 of signed envelope bytesNo
agentIdHashSHA-256 of agent identifier stringNo
raile.g. airwallex, wise
amountBucketOne of: <10, 10-100, 100-1000, 1000-10000, >10000
currencyISO code (e.g. USD, USDC)
outcomesuccess or failed
timestampUnix seconds

Submission is best-effort — failures are silently swallowed and never interrupt payment execution. Self-host the ledger by deploying ledger/ to your own Cloudflare Workers account.

// Query the public ledger
const stats = await fetch('https://ledger.pqsafe.xyz/v1/stats').then(r => r.json())
// { totalTransfers: 847, totalUSDRouted: 42300, activeAgents: 23, lastUpdated: ... }

// Or from the SDK directly
import { submitToLedger, buildLedgerRecord } from '@pqsafe/agent-pay'
// submitToLedger is called automatically — or call manually for custom flows

Security model

Post-quantum signature. ML-DSA-65 (NIST FIPS 204, Module Lattice Digital Signature Algorithm). 128-bit security against classical and quantum adversaries. Public key 1952 bytes, signature 3309 bytes. Implemented via @noble/post-quantum.

No key custody. The SDK never holds or stores signing keys. The secret key is provided per-call by the wallet layer and used only during signEnvelope().

Deterministic serialization. Envelope JSON is serialized with sorted keys before signing, ensuring identical bytes across platforms and preventing signature bypass via key reordering.

Replay prevention. The 128-bit random nonce is committed into the signed envelope. The same nonce cannot be used to construct a different payment.

Defense-in-depth. Even if an agent process is fully compromised, it cannot spend beyond maxAmount, pay a recipient not in allowedRecipients, or use an expired envelope — all enforced by the SDK before any rail call.

Quantum threat context. NIST issued final PQ standards (FIPS 203/204) in August 2024. ECDSA and RSA are broken by Shor's algorithm on a sufficiently large quantum computer. ML-DSA-65 is the NIST-standardized replacement for digital signatures.

API reference

createEnvelope(params): SpendEnvelope

Build a new unsigned SpendEnvelope. Nonce is auto-generated with crypto.getRandomValues.

ParamTypeDescription
issuerstringPQSafe address (pq1…)
agentstringAgent identifier, 1–128 chars
maxAmountnumberSpend ceiling (positive)
currencystringISO 4217, e.g. USD
allowedRecipientsstring[]Non-empty allowlist
ttlSeconds?numberDefault 3600
startsInSeconds?numberDefault 0 (immediate)
rail?RailOptional rail constraint
signEnvelope(envelope, secretKey, publicKey): SignedEnvelope

Sign with issuer's ML-DSA-65 secret key. Returns { envelopeJson, signature, dsaPublicKey } — safe to pass to agent.

verifyEnvelope(signed, dsaPublicKey?): SpendEnvelope

Verify signature + schema + temporal validity. Throws on any failure. Returns parsed envelope.

executeAgentPayment(signed, request): Promise<PaymentResult>

Full pipeline: verify → allowlist → amount ceiling → route to rail.

Request fieldTypeRequired
recipientstringYes — must be in allowedRecipients
amountnumberYes — must be >0 and ≤maxAmount
memo?stringNo

Something missing? Open a GitHub issue — we fix same day.

Run live demo →