Key Management
Generate and rotate ML-DSA-65 key pairs. Issue agent subkeys with narrower permissions.
The @pqsafe/agent-pay package exports a small, focused API surface organized into five functional areas. This page provides a navigable overview; auto-generated TypeDoc documentation for every exported symbol lives in the API Reference section.
npm install @pqsafe/agent-payKey Management
Generate and rotate ML-DSA-65 key pairs. Issue agent subkeys with narrower permissions.
Envelope Lifecycle
Create, sign, verify, and revoke spend envelopes.
Payment Execution
Dispatch payments to five rails: Airwallex, Wise, Stripe, USDC Base, x402.
Approval Gates
Request and resolve human-in-the-loop approvals via Telegram, Slack, or webhook.
Ledger
Build and submit tamper-evident ledger records for every payment attempt.
generateKeyPair()Generate a fresh ML-DSA-65 key pair. Uses a cryptographically secure 32-byte seed.
import { generateKeyPair } from '@pqsafe/agent-pay'
const { publicKey, secretKey } = await generateKeyPair()// publicKey: hex string, 3904 chars (1952 bytes)// secretKey: hex string, 8064 chars (4032 bytes)Key sizes (ML-DSA-65 / FIPS 204):
| Key | Bytes | Hex chars |
|---|---|---|
| Secret key | 4,032 | 8,064 |
| Public key | 1,952 | 3,904 |
| Signature | 3,293 | 6,586 |
createAgentSubkey()Derive a spend key with reduced permissions from an issuer root key.
import { createAgentSubkey } from '@pqsafe/agent-pay'
const subkey = await createAgentSubkey(rootKey, { agentId: 'procurement-agent-v2', maxAmountCap: 500, // Sub-key cannot sign envelopes above $500 expiresAt: new Date(Date.now() + 7 * 86_400_000), // 7-day key})rotateSpendKey()Replace a spend key while preserving the issuer hierarchy. Old key is revoked.
import { rotateSpendKey } from '@pqsafe/agent-pay'
const { newPublicKey, newSecretKey } = await rotateSpendKey(oldSecretKey, { reason: 'scheduled-rotation',})createSpendEnvelope()Build an unsigned SpendEnvelope object from a set of authorization parameters.
import { createSpendEnvelope } from '@pqsafe/agent-pay'
const envelope = createSpendEnvelope({ agentId: 'my-agent', maxAmount: 100, currency: 'USD', allowedRails: ['airwallex'], allowedRecipients: ['vendor.com'], validUntil: new Date(Date.now() + 3_600_000),})SpendEnvelope type:
type SpendEnvelope = { envelopeId: string // SHA-256 of canonical JSON (auto-computed) agentId: string issuedBy?: string maxAmount: number currency: string // ISO 4217 or 'USDC' validFrom?: Date validUntil: Date allowedRecipients: string[] allowedRails: Rail[] requireApproval?: boolean approvalThreshold?: number memo?: string}createSignedEnvelope()Sign an envelope with ML-DSA-65. Returns a SignedEnvelope with the hex-encoded signature attached.
import { createSignedEnvelope } from '@pqsafe/agent-pay'
const signed = createSignedEnvelope(envelope, secretKey)// signed.signature: 6586-char hex string// signed.envelopeId: SHA-256 of JCS canonical JSONThe signature is computed over the JCS (RFC 8785) canonical JSON of the envelope — deterministic key ordering, no whitespace, no trailing commas. Any modification to the envelope after signing invalidates the signature.
verifyEnvelope()Verify an ML-DSA-65 signature. Returns true if valid.
import { verifyEnvelope } from '@pqsafe/agent-pay'
const valid = verifyEnvelope(signed, publicKey)// true | falserevoke()Mark an envelope as revoked in the PQSafe revocation service. Revoked envelopes cannot be dispatched.
import { revoke } from '@pqsafe/agent-pay'
await revoke(signed.envelopeId, { reason: 'key-compromise', revokedBy: 'security-team',})isRevoked()Check whether an envelope has been revoked.
import { isRevoked } from '@pqsafe/agent-pay'
const revoked = await isRevoked(signed.envelopeId)executeAgentPayment()The primary payment dispatch function. Validates the envelope, checks revocation, enforces all invariants, dispatches to the appropriate rail, and returns a PaymentResult.
import { executeAgentPayment } from '@pqsafe/agent-pay'
const result = await executeAgentPayment(signed, { recipient: 'vendor.com/billing', amount: 50, memo: 'Invoice #INV-2026-042',})
// result.status: 'settled' | 'pending' | 'failed'// result.txId: rail-specific transaction ID// result.rail: 'airwallex' | 'wise' | 'stripe' | 'usdc-base' | 'x402'// result.settledAt: Date (if settled)// result.errorCode: PQSafeErrorCode (if failed)Invariants checked before dispatch (in order):
validUntil not exceededvalidFrom reached (if set)amount ≤ maxAmountrecipient in allowedRecipientsrail in allowedRailsrequireApproval: true)All eight checks must pass. There is no bypass mechanism.
| Rail | Rail value | Currency | Typical use |
|---|---|---|---|
| Airwallex | 'airwallex' | USD, EUR, GBP, HKD, AUD | Cross-border, ACH, wire |
| Wise | 'wise' | 40+ currencies | International wire |
| Stripe | 'stripe' | USD, EUR, GBP | Card-on-file, ACP |
| USDC Base | 'usdc-base' | USDC | On-chain, crypto-native |
| x402 | 'x402' | USDC, ETH | HTTP 402 micropayments |
requestApproval()Initiate a human-in-the-loop approval request for a pending payment.
import { requestApproval } from '@pqsafe/agent-pay'
const request = await requestApproval(signed, { channel: 'telegram', chatId: process.env.TELEGRAM_CHAT_ID!, message: `Payment of $${amount} to ${recipient} requires your approval.`, expiresIn: 3_600_000, // 1 hour (must be < envelope validUntil)})
// request.approvalId: string// request.status: 'pending'getApprovalStatus()Poll or check the current status of an approval request.
import { getApprovalStatus } from '@pqsafe/agent-pay'
const status = await getApprovalStatus(request.approvalId)// status.state: 'pending' | 'approved' | 'rejected' | 'expired'executeWithApproval()High-level helper that combines payment execution with approval gating — blocks until approved or times out.
import { executeWithApproval } from '@pqsafe/agent-pay'
const result = await executeWithApproval(signed, { recipient: 'vendor.com/billing', amount: 250, memo: 'Q2 services invoice', approval: { channel: 'telegram', chatId: process.env.TELEGRAM_CHAT_ID!, timeoutMs: 3_600_000, },})buildLedgerRecord()Construct a LedgerRecord from a signed envelope and payment result.
import { buildLedgerRecord } from '@pqsafe/agent-pay'
const record = buildLedgerRecord(signed, result)// record.envelopeId// record.agentId// record.recipient// record.amount// record.currency// record.rail// record.status// record.txId// record.timestamp// record.signatureFingerprint ← first 16 hex chars of signaturesubmitToLedger()Append a ledger record to the tamper-evident ledger. Returns the hash and sequence number.
import { submitToLedger } from '@pqsafe/agent-pay'
const entry = await submitToLedger(record)// entry.hash: SHA-256 of record + previous hash (chain integrity)// entry.sequence: monotonically increasing integer// entry.timestamp: server-side timestampsetAgentPayConfig()Set global SDK configuration. Call once at application startup.
import { setAgentPayConfig } from '@pqsafe/agent-pay'
setAgentPayConfig({ issuerKey: process.env.PQSAFE_ISSUER_KEY, defaultRail: 'airwallex', logLevel: 'info', revocationService: { url: 'https://revoke.pqsafe.xyz', timeoutMs: 2_000, }, webhooks: { envelopeConsumed: { threshold: 0.8, // Alert at 80% of maxAmount consumed url: 'https://your-server.com/webhooks/pqsafe', }, },})All PQSafe errors extend PQSafeError and include a code property.
import { PQSafeError } from '@pqsafe/agent-pay'
try { await executeAgentPayment(signed, params)} catch (err) { if (err instanceof PQSafeError) { console.error(err.code) // e.g. 'ENVELOPE_EXPIRED' console.error(err.message) // human-readable description }}| Error code | Meaning |
|---|---|
ENVELOPE_SIGNATURE_INVALID | ML-DSA-65 verification failed — envelope was tampered |
ENVELOPE_EXPIRED | validUntil has passed |
ENVELOPE_NOT_YET_ACTIVE | validFrom is in the future |
AMOUNT_EXCEEDS_CEILING | amount > maxAmount |
RECIPIENT_NOT_ALLOWED | recipient not in allowedRecipients |
RAIL_NOT_ALLOWED | Rail not in allowedRails |
ENVELOPE_REVOKED | Envelope was revoked via revoke() |
APPROVAL_REQUIRED | requireApproval is true and no approval token provided |
APPROVAL_REJECTED | Human explicitly rejected the payment |
APPROVAL_TIMEOUT | Approval request expired before human responded |
RAIL_ERROR | Payment rail returned an error (see err.railCode) |
RATE_LIMIT | Too many requests — back off and retry |
Full TypeDoc-generated documentation for every exported symbol, interface, type alias, and error class:
→ API Reference (auto-generated)
The API reference is regenerated on every SDK release from JSDoc comments in agent-pay/src/. If you find a discrepancy between this overview and the auto-generated docs, the auto-generated docs are authoritative.