Architecture
System overview
PQSafe AgentPay has four primary layers:
- SDK (
@pqsafe/agent-pay) — the agent-side library that creates, signs, verifies, and dispatches payments - SpendEnvelope — the signed authorization document, passed between issuer and agent
- Rail connectors — thin adapters to Airwallex, Wise, Stripe, USDC Base, and x402
- Ledger — the append-only audit log (local or hosted)
Optionally:
- On-chain registry (Arbitrum) — cryptographic anchoring for high-value or regulated payments
Architecture diagram
graph TD A[Human Operator] -->|signs| B[SpendEnvelope\nML-DSA-65 signature] B -->|passed to| C[AI Agent\n@pqsafe/agent-pay SDK]
C -->|executeAgentPayment| D{Envelope validation\n8 checks} D -->|all pass| E[Rail dispatcher] D -->|any fail| F[PQSafeError\nno payment made]
E --> G[Airwallex\nACH · wire · cross-border] E --> H[Wise\nInternational wire · 80+ currencies] E --> I[Stripe\nCard · invoice · ACP] E --> J[USDC on Base\nOn-chain · 2s settlement] E --> K[x402\nHTTP 402 micropayments]
G & H & I & J & K -->|PaymentResult| L[Ledger\nappend-only]
L -->|optional| M[Arbitrum Registry\nOn-chain proof]
C -->|approval gate| N[Telegram / Slack / Webhook] N -->|approved| E N -->|rejected| FEnvelope validation (8 checks)
Before any rail receives a payment request, executeAgentPayment() runs these checks in order. All eight must pass:
| # | Check | Error if failed |
|---|---|---|
| 1 | ML-DSA-65 signature valid | ENVELOPE_SIGNATURE_INVALID |
| 2 | Zod schema validation | ENVELOPE_SCHEMA_INVALID |
| 3 | validFrom reached | ENVELOPE_NOT_YET_ACTIVE |
| 4 | validUntil not exceeded | ENVELOPE_EXPIRED |
| 5 | recipient in allowedRecipients | RECIPIENT_NOT_ALLOWED |
| 5b | amount ≤ maxAmount | AMOUNT_EXCEEDS_CEILING |
| 6 | Human approval granted (if requireApproval) | APPROVAL_REQUIRED |
| 7 | Route to rail connector | RAIL_ERROR |
There is no way to bypass these checks. The rail connector never sees a request unless all prior checks have passed.
Envelope lifecycle
Created (createSpendEnvelope) ↓Signed (createSignedEnvelope) ← ML-DSA-65 over canonical JSON ↓[Optional: Approval gate — Telegram / Slack / webhook] ↓Dispatched (executeAgentPayment) ↓Settled (rail-specific tx ID returned) ↓Ledger entry appended (buildLedgerRecord + submitToLedger) ↓[Optional: On-chain anchor — commitEnvelopeToArbitrum]Single-use semantics: once a SignedEnvelope is dispatched, its envelopeId is recorded in the ledger. A second dispatch with the same ID fails with ENVELOPE_ALREADY_USED.
Key hierarchy
PQSafe supports a two-level key hierarchy for enterprise deployments:
Root Issuer Key (ML-DSA-65) ↓ createIssuerHierarchy() ├── Agent Subkey A (capped: $500, expires: 7d) ├── Agent Subkey B (capped: $100, expires: 24h) └── Agent Subkey C (capped: $5000, expires: 30d)Each subkey is derived from the root issuer key and carries its own spending cap and expiry. A compromised subkey cannot sign envelopes above its cap, even if the attacker holds the subkey material.
Ledger hash chain
The ledger is an append-only hash chain. Each entry includes the SHA-256 hash of the previous entry:
entry[n].hash = SHA-256(entry[n].content + entry[n-1].hash)Tampering with any entry invalidates all subsequent hashes. Use verifyLedgerChain() to verify integrity:
import { verifyLedgerChain, getLedgerEntries } from '@pqsafe/agent-pay'
const entries = await getLedgerEntries({ agentId: 'my-agent' })const intact = verifyLedgerChain(entries)On-chain anchoring (optional)
For payments above $1,000 or in regulated contexts (HKMA QPI, EU DORA), PQSafe can anchor the envelopeId and ledger hash on Arbitrum:
import { commitEnvelopeToArbitrum } from '@pqsafe/agent-pay'
const { txHash } = await commitEnvelopeToArbitrum(signedEnvelope, { rpcUrl: process.env.ARBITRUM_RPC_URL!, privateKey: process.env.WALLET_PRIVATE_KEY!,})The on-chain record provides a tamper-evident, third-party-auditable proof of authorization that cannot be altered even if the PQSafe ledger is compromised.
Multi-framework support
The SDK ships adapter packages for the most popular agentic frameworks. All adapters share the same envelope/rail/ledger stack — only the tool-calling interface differs:
| Framework | Adapter | Language |
|---|---|---|
| LangChain | DynamicStructuredTool | TypeScript / Python |
| CrewAI | BaseTool subclass | Python |
| Mastra | createTool() | TypeScript |
| MCP | pqsafe_pay tool | Any MCP host |