Skip to content

Architecture

System overview

PQSafe AgentPay has four primary layers:

  1. SDK (@pqsafe/agent-pay) — the agent-side library that creates, signs, verifies, and dispatches payments
  2. SpendEnvelope — the signed authorization document, passed between issuer and agent
  3. Rail connectors — thin adapters to Airwallex, Wise, Stripe, USDC Base, and x402
  4. Ledger — the append-only audit log (local or hosted)

Optionally:

  1. 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| F

Envelope validation (8 checks)

Before any rail receives a payment request, executeAgentPayment() runs these checks in order. All eight must pass:

#CheckError if failed
1ML-DSA-65 signature validENVELOPE_SIGNATURE_INVALID
2Zod schema validationENVELOPE_SCHEMA_INVALID
3validFrom reachedENVELOPE_NOT_YET_ACTIVE
4validUntil not exceededENVELOPE_EXPIRED
5recipient in allowedRecipientsRECIPIENT_NOT_ALLOWED
5bamount ≤ maxAmountAMOUNT_EXCEEDS_CEILING
6Human approval granted (if requireApproval)APPROVAL_REQUIRED
7Route to rail connectorRAIL_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:

FrameworkAdapterLanguage
LangChainDynamicStructuredToolTypeScript / Python
CrewAIBaseTool subclassPython
MastracreateTool()TypeScript
MCPpqsafe_pay toolAny MCP host