RFC-005 v4: Nexus Payment Core Specification (Gasless Escrow Settlement)
| Metadata | Value |
|---|---|
| Title | Nexus Payment Core Specification |
| Version | 4.0.0 |
| Status | Standards Track (Draft) |
| Supersedes | RFC-005 v3.0.0 (Escrow Settlement) |
| Author | Cipher & Nexus Architect Team |
| Created | 2026-02-24 |
| Updated | 2026-04-01 |
| Scope | Orchestration, State Management, Gasless Escrow, Group Payments, AP2 Protocol, Cancel/Dispute, Multi-Token |
| Chain | PlatON Devnet (chain_id: 20250407) |
| Token | ERC-3009 compatible stablecoins (XSGD, USDC) |
1. Abstract
This RFC defines the complete implementation specification for Nexus Core. v4 builds on v3's Escrow Settlement foundation with the addition of Gasless Deposit, Cancel mechanism, AP2 Credential Provider, and Multi-Token support.
Key changes (v3 -> v4):
- Gasless Deposit: Users only sign an EIP-3009 authorization; the Relayer pays gas and submits
batchDepositGasless() - Cancel mechanism: Merchants/operators can cancel payments in any non-terminal state; on-chain ESCROWED payments are immediately refunded via
cancelByOperator() - AP2 Protocol: Acts as a Credential Provider for Agent Protocol 2, issuing PaymentMandate Verifiable Credentials
- Multi-Token: Supports multiple ERC-3009 compatible tokens via
setToken()(current: XSGD) - 13-State Machine: Adds the CANCELLED state
- Auto-Refund Late Deposits: Automatically triggers a refund when an expired payment receives a late on-chain deposit
2. Design Principles
- Escrow Settlement: Funds are guaranteed through a smart contract, released after merchant fulfillment, with automatic refund on timeout
- Gasless UX: Users only need to sign an EIP-3009 authorization and do not need to hold any native token; the Relayer pays all gas
- Batch Efficiency: Multiple payments are aggregated into a group, sharing a single on-chain transaction
- MCP-First: All capabilities are exposed through the MCP Protocol, with REST API also available
- Event Sourcing: Each state change generates an immutable event record
- Anti-MITM: Core Operator signs Group instructions, verified on-chain to prevent tampering
- Protocol Agnostic: Supports both the NUPS native protocol and the AP2 Agent Protocol
3. Architecture
UA --[MCP/REST]--> Nexus Core --[Webhook]--> MA
|
+-- Security Module (EIP-712, DID Resolver, Group Sig)
+-- Order State Machine (13 states)
+-- Group Manager (batch orchestration)
+-- AP2 Protocol Router (NUPS / AP2 detection)
+-- AP2 Credential Provider (PaymentMandate VC issuance)
+-- Chain Watcher (Escrow events: Deposited/Released/Refunded)
+-- Relayer (gasless deposit / release / refund / cancel tx submission)
+-- Webhook Notifier (HMAC signed, RFC-009)
+-- PostgreSQL (payments, payment_groups, events, merchants,
| webhook_logs, mandate_evidence)
|
PlatON Devnet (chain_id: 20250407)
XSGD/USDC (ERC-3009) + NexusEscrow (UUPS Proxy v4.3.0)4. Payment Flow
4.1 Happy Path (Gasless)
Step 1: UA obtains merchant Quote (NUPS v1.5, EIP-712 signed)
Step 2: UA calls Core nexus_orchestrate_payment(quotes[], payer_wallet)
Core verifies sigs -> DID resolves -> creates payment group
-> returns 402 with BatchDepositInstruction (user_action: "SIGN_ONLY")
Step 3: User signs EIP-3009 TypedData (via MetaMask / wallet) — signature only, no transaction sent
Step 4: Checkout page POST /api/checkout/:groupId/relay-deposit { payer_address, v, r, s }
Core Relayer calls batchDepositGasless() on-chain — zero gas for user
Step 5: Core Chain Watcher detects Deposited events -> status = ESCROWED
Step 6: Core sends Webhook to MA: payment.escrowed
Step 7: MA confirms fulfillment -> Core Relayer calls release() on escrow
Step 8: Chain Watcher detects Released event -> status = SETTLED
Step 9: Core sends Webhook to MA: payment.settled
Step 10: MA confirms completion -> status = COMPLETED4.2 Checkout Page Flow (Browser Gasless)
Step 1: UA receives 402 response with checkout_url
Step 2: User opens checkout_url in browser with MetaMask
Step 3: Checkout page displays payment summary (SSE real-time updates)
Step 4: User clicks "Pay" -> MetaMask prompts EIP-3009 signature (signature only, not a transaction)
Step 5: Checkout page POST relay-deposit with signature components
Step 6: Relayer submits batchDepositGasless() on-chain
Step 7: Page polls /api/checkout/:token until receipt verified
Step 8: Receipt verified -> ESCROWED transition4.3 AP2 Protocol Flow
When an AP2 protocol request is detected (body.protocol === "ap2" or contains cart_mandates):
Step 1: UA sends orchestrate request with CartMandate VCs
Step 2: Core validates CartMandate signatures, extracts payment intent
Step 3: Core creates payment group, returns 402 (same as NUPS)
Step 4: User completes gasless deposit (same as 4.1/4.2)
Step 5: On ESCROWED transition -> Core issues PaymentMandate VC
(W3C Verifiable Credential, signed with ECDSA P-256 / JCS)
Step 6: PaymentMandate stored in mandate_evidence table
Step 7: Webhook includes mandate_vc in payload for AP2 orders4.4 Cancel Flow
Pre-Escrow (CREATED / AWAITING_TX):
-> nexus_cancel_payment -> database status = CANCELLED
-> Webhook: payment.cancelled
ESCROWED:
-> nexus_cancel_payment -> Relayer calls cancelByOperator() on-chain
-> Chain Watcher detects Refunded -> status = CANCELLED
-> Webhook: payment.cancelled
Group-level:
-> nexus_cancel_order -> cancels all child payments in group
-> Group status -> GROUP_CANCELLED4.5 HTTP 402 Payment Required Response
interface PaymentRequired402 {
readonly nexus_version: string;
readonly group_id: string;
readonly status: "PAYMENT_REQUIRED";
readonly checkout_url: string;
readonly instruction: BatchDepositInstruction;
readonly nexus_group_sig: Hex;
readonly core_operator_address: Address;
}4.6 BatchDepositInstruction Schema
interface BatchDepositInstruction {
readonly group_id: string;
readonly chain_id: 20250407;
readonly chain_name: "PlatON Devnet";
readonly rpc_url: string;
readonly payment_method: "ESCROW_CONTRACT";
readonly escrow_contract: Address; // UUPS proxy address
readonly token_address: Address; // ERC-3009 token contract
readonly token_symbol: string; // "XSGD" | "USDC"
readonly token_decimals: 6;
readonly total_amount_uint256: string;
readonly total_amount_display: string;
readonly payments: readonly GroupPaymentDetail[];
readonly eip3009_sign_data: EIP3009SignData; // EIP-3009 TypedData for signing
readonly user_action: "SIGN_ONLY"; // v4: user only signs, relayer submits
readonly gas_paid_by: "RELAYER"; // v4: relayer pays all gas
readonly nexus_group_sig: Hex;
readonly core_operator_address: Address;
}Key changes from v3 to v4:
user_action:"SIGN_AND_SEND"->"SIGN_ONLY"gas_paid_by:"USER"->"RELAYER"token_symbol: fixed"USDC"-> dynamic (currently"XSGD")- Removed
deposit_txfield (users no longer submit transactions directly)
4.7 GroupPaymentDetail Schema
interface GroupPaymentDetail {
readonly nexus_payment_id: string;
readonly merchant_did: string;
readonly merchant_order_ref: string;
readonly merchant_address: Address;
readonly amount_uint256: string;
readonly amount_display: string;
readonly summary: string;
readonly payment_id_bytes32: Hex; // keccak256(nexus_payment_id)
readonly order_ref_bytes32: Hex; // keccak256(merchant_order_ref)
readonly merchant_did_bytes32: Hex; // keccak256(merchant_did)
readonly context_hash: Hex; // keccak256(JSON.stringify(context))
}5. State Machine
5.1 States (13-state payment machine)
| Status | Description | Trigger |
|---|---|---|
| CREATED | Quote verified, payment created | orchestrate_payment success |
| AWAITING_TX | Legacy: UA has PaymentInstruction | Direct Transfer mode (deprecated) |
| BROADCASTED | Legacy: UA submitted tx_hash | Direct Transfer mode (deprecated) |
| ESCROWED | Funds deposited in escrow contract | Chain Watcher (Deposited event) |
| SETTLED | Escrow released to merchant | Chain Watcher (Released event) |
| COMPLETED | Merchant confirmed fulfillment | confirm_fulfillment call |
| EXPIRED | Payment timed out | Timeout Handler |
| TX_FAILED | On-chain transaction reverted | Chain Watcher |
| RISK_REJECTED | Security check failed | Security Module (future) |
| REFUNDED | Escrow refunded to payer | Chain Watcher (Refunded event) |
| DISPUTE_OPEN | Payer opened dispute | Chain Watcher (Disputed event) |
| DISPUTE_RESOLVED | Arbiter resolved dispute | Chain Watcher (Resolved event) |
| CANCELLED | Payment cancelled by merchant/operator | nexus_cancel_payment (new in v4) |
5.2 Transition Rules
(none) -> CREATED [valid quote + signature verified]
CREATED -> ESCROWED [Chain Watcher: Deposited event]
CREATED -> EXPIRED [timeout]
CREATED -> RISK_REJECTED [security check failed]
CREATED -> CANCELLED [merchant/operator cancel]
ESCROWED -> SETTLED [Chain Watcher: Released event]
ESCROWED -> REFUNDED [Chain Watcher: Refunded event (timeout)]
ESCROWED -> DISPUTE_OPEN [Chain Watcher: Disputed event]
ESCROWED -> CANCELLED [cancelByOperator -> Refunded on-chain]
SETTLED -> COMPLETED [merchant confirms fulfillment]
DISPUTE_OPEN -> DISPUTE_RESOLVED [Chain Watcher: Resolved event]
EXPIRED -> REFUNDED [auto-refund late deposits]
Legacy (Direct Transfer, deprecated):
CREATED -> AWAITING_TX [UA requests PaymentInstruction]
AWAITING_TX -> BROADCASTED [UA submits tx_hash]
AWAITING_TX -> CANCELLED [cancel]
BROADCASTED -> SETTLED [on-chain Transfer confirmed]
BROADCASTED -> TX_FAILED [on-chain revert]Terminal statuses: COMPLETED, EXPIRED, TX_FAILED, RISK_REJECTED, REFUNDED, DISPUTE_RESOLVED, CANCELLED
5.3 Group Statuses
| Group Status | Description |
|---|---|
| GROUP_CREATED | Group created, payments pending |
| GROUP_AWAITING_TX | 402 returned, waiting for on-chain tx |
| GROUP_DEPOSITED | On-chain deposit confirmed (receipt verified) |
| GROUP_ESCROWED | All child payments transitioned to ESCROWED |
| GROUP_SETTLED | All child payments released |
| GROUP_COMPLETED | All child payments completed |
| GROUP_EXPIRED | Group timed out |
| GROUP_PARTIAL | Mixed states (some settled, some disputed) |
| GROUP_CANCELLED | All child payments cancelled (new in v4) |
5.4 Timeout Rules
| Scenario | Timeout | Action |
|---|---|---|
| Quote expiry | quote.expiry timestamp | CREATED -> EXPIRED |
| Awaiting deposit | 30 minutes | CREATED -> EXPIRED |
| Escrow release deadline | 24 hours (on-chain, ms) | refund() callable by anyone |
| Dispute window | 72 hours (on-chain, ms) | dispute() no longer callable |
| Arbitration timeout | 7 days (on-chain, ms) | refundUnresolvedDispute() callable |
PlatON Devnet note:
block.timestampreturns milliseconds rather than seconds. All on-chain timeout parameters are configured in milliseconds accordingly.
5.5 Auto-Refund Late Deposits
When a payment in the EXPIRED state receives a late on-chain Deposited event:
- Chain Watcher detects the Deposited event
- Automatically calls Relayer.submitRefund() to trigger an on-chain refund
- Status remains unchanged (stays EXPIRED), but the Refunded event is recorded
- Prevents user funds from being locked in an expired order
6. Security Specification
6.1 EIP-712 Quote Verification
const NEXUS_QUOTE_DOMAIN = {
name: "Nexus",
version: "1",
chainId: 20250407,
verifyingContract: "0x0000000000000000000000000000000000000000",
} as const;
const NEXUS_QUOTE_TYPES = {
NexusQuote: [
{ name: "merchant_did", type: "string" },
{ name: "merchant_order_ref", type: "string" },
{ name: "amount", type: "uint256" },
{ name: "currency", type: "string" },
{ name: "chain_id", type: "uint256" },
{ name: "expiry", type: "uint256" },
{ name: "context_hash", type: "bytes32" },
],
} as const;Verification steps:
- Reconstruct EIP-712 TypedData from quote fields
- Recover signer address from signature
- Resolve merchant_did via merchant_registry to obtain the registered signer
- Compare recovered address with registered signer
- Verify quote has not expired
6.2 Group Signature (Anti-MITM)
After Core generates the BatchDepositInstruction, it signs an EIP-712 GroupApproval using the Core Operator key:
const GROUP_APPROVAL_TYPES = {
NexusGroupApproval: [
{ name: "groupId", type: "bytes32" },
{ name: "entriesHash", type: "bytes32" },
{ name: "totalAmount", type: "uint256" },
],
} as const;The on-chain batchDepositGasless() verifies this signature, ensuring that the transaction parameters have not been tampered with.
6.3 Receipt Verification
When the Checkout confirms, Core verifies the on-chain transaction receipt:
- HTTP 200: receipt success -> transition to ESCROWED
- HTTP 202: receipt not available yet -> frontend polls (5s interval, max 24 attempts = 120s)
- HTTP 422: receipt reverted -> error, no state change
6.4 DID Resolution (MVP)
MVP uses a local merchant_registry table:
merchant_did TEXT PRIMARY KEY
signer_address TEXT NOT NULL -- signing key address
payment_address TEXT NOT NULL -- receiving address (escrow release target)
webhook_url TEXT -- callback URL
webhook_secret TEXT -- HMAC key
ap2_did TEXT -- AP2 agent DID (optional)6.5 Payment Address Trust
CRITICAL: Core MUST resolve payment_address from the merchant_did registry. It MUST NOT trust any address passed in the quote or by the UA. This prevents payment redirection attacks.
7. Interface Specification
7.1 MCP Tools (11 tools)
| Tool | Caller | Description |
|---|---|---|
| nexus_orchestrate_payment | UA | Verify quotes, create payment group, return 402 with BatchDepositInstruction |
| nexus_get_payment_status | UA/MA | Query payment or group status |
| nexus_confirm_fulfillment | MA | Confirm merchant delivery, trigger escrow release |
| nexus_release_payment | MA/Core | Explicit escrow release |
| nexus_dispute_payment | Payer | Open dispute within dispute window |
| nexus_resolve_dispute | Arbiter | Resolve dispute with bps split |
| nexus_cancel_payment | MA | Cancel single payment (pre-escrow or on-chain) |
| nexus_cancel_order | MA | Cancel all payments in a group |
| nexus_get_merchant | UA | Get merchant profile + marketplace metadata |
| nexus_discover_agents | UA | Search agent catalog by keyword |
| nexus_get_agent_skill | UA | Fetch agent's skill.md |
7.2 REST API
| Method | Path | Description |
|---|---|---|
| POST | /api/orchestrate | Verify quotes, return 402 with payment instruction |
| GET | /api/checkout/:token | Get group instruction JSON |
| POST | /api/checkout/:token/relay-deposit | Gasless deposit (new in v4): submit EIP-3009 sig, relayer submits on-chain |
| POST | /api/checkout/:token/confirm | Legacy: confirm user-submitted tx_hash |
| GET | /api/payments | Query payments (by nexus_payment_id, merchant_order_ref, group_id) |
| GET | /api/payments/:id | Get payment details |
| GET | /api/merchant/payments | Merchant portal: list payments (filter by did, status, since) |
| POST | /api/merchant/confirm-fulfillment | Merchant confirms fulfillment (triggers release) |
| POST | /api/merchant/cancel-payment | Cancel single payment |
| POST | /api/merchant/cancel-order | Cancel all payments in group |
| GET | /api/agents | List all registered agents |
| GET | /api/agents/:did/skill | Fetch agent's skill.md |
7.3 Checkout Page
| Path | Description |
|---|---|
| /checkout/:groupId | Browser checkout page (MetaMask integration, Gasless) |
| /portal | Core management portal |
7.4 Rate Limiting
- 30 requests per minute per IP (token bucket)
- Headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset
8. AP2 Protocol Integration
8.1 Protocol Detection
function detectProtocol(body): "ap2" | "nups" {
if (body.protocol === "ap2") return "ap2";
if (body.cart_mandates) return "ap2";
return "nups";
}8.2 Verifiable Credential Types
As an AP2 Credential Provider, Nexus handles the following three W3C Verifiable Credential types:
CartMandate VC (issued by Merchant Agent)
interface CartMandateSubject {
merchant_did: string;
cart_id: string;
total: string;
currency: string;
line_items: CartLineItem[];
expires_at: string;
}IntentMandate VC (issued by User Agent)
interface IntentMandateSubject {
agent_did: string;
budget: string;
currency: string;
merchant_whitelist: string[];
ttl: number; // seconds
}PaymentMandate VC (issued by Nexus Credential Provider)
When a payment transitions to ESCROWED status, Core issues a PaymentMandate as proof of payment:
interface PaymentMandateSubject {
credential_provider_did: string;
nexus_payment_id: string;
group_id: string;
payer: Address;
total_amount: string;
currency: string;
chain_id: number;
escrow_contract: Address;
deposit_tx_hash: Hex;
}8.3 Proof Structure (Data Integrity)
interface JCSProof {
type: "DataIntegrityProof";
cryptosuite: "ecdsa-jcs-2022";
verificationMethod: string; // DID URL
proofPurpose: "assertionMethod";
created: string; // ISO 8601
proofValue: string; // base64url-encoded r||s
}Signature algorithm: ECDSA P-256, JSON Canonicalization Scheme (JCS)
8.4 Mandate Storage
CREATE TABLE mandate_evidence (
evidence_id TEXT PRIMARY KEY,
nexus_payment_id TEXT NOT NULL,
group_id TEXT NOT NULL,
mandate_type TEXT NOT NULL, -- 'cart_mandate' | 'intent_mandate' | 'payment_mandate'
mandate_vc JSONB NOT NULL, -- complete W3C VC JSON
issuer_did TEXT NOT NULL,
subject_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);9. Chain Watcher Specification
9.1 Polling Strategy
- Poll PlatON RPC every 3 seconds for new blocks
- Filter NexusEscrow contract logs for:
Deposited(paymentId, payer, merchant, amount, orderRef)BatchDeposited(groupId, payer, totalAmount, count)Released(paymentId, merchant, merchantAmount, feeAmount)Refunded(paymentId, payer, amount)Disputed(paymentId, payer, reason)Resolved(paymentId, merchantBps, merchantAmount, payerAmount)
9.2 Event Processing
On Deposited event:
1. Match paymentId to payment record (payment_id_bytes32)
2. Check if payment is EXPIRED -> if so, auto-refund (submit refund tx)
3. Transition payment: CREATED -> ESCROWED
4. Record deposit_tx_hash, release_deadline, dispute_deadline
5. If protocol === "ap2": issue PaymentMandate VC
6. Send webhook: payment.escrowed
On Released event:
1. Match paymentId to payment record
2. Transition payment: ESCROWED -> SETTLED
3. Record release_tx_hash, protocol_fee
4. Send webhook: payment.settled
On Refunded event:
1. Match paymentId to payment record
2. Check cancel context -> CANCELLED or REFUNDED
3. Record refund_tx_hash
4. Send webhook: payment.refunded or payment.cancelled10. Relayer Service
The Relayer is a server-side service that submits transactions on behalf of the Core:
| Operation | Trigger | Gas Payer | v3 | v4 |
|---|---|---|---|---|
batchDepositGasless(...) | User signs EIP-3009 | Relayer | N/A | NEW |
release(paymentId) | Merchant confirms fulfillment | Relayer | Yes | Yes |
refund(paymentId) | Timeout auto-refund | Relayer | Yes | Yes |
cancelByOperator(paymentId) | Merchant/operator cancel | Relayer | N/A | NEW |
resolve(paymentId, bps) | Arbiter resolves dispute | Relayer | N/A | NEW |
Relayer wallet: 0xf7EA5d3f0Bf8185c4f3C2F405D9a71009CF4D920 (also coreOperator on contract)
Retry logic: exponential backoff (1s, 3s, 9s), max timeout 120s per transaction.
11. Webhook Notification
See RFC-009 v1.2 for full specification. Key points:
11.1 Event Types (11)
| Event | Trigger |
|---|---|
| payment.created | Payment created |
| payment.escrowed | Funds deposited in escrow |
| payment.settled | Escrow released to merchant |
| payment.completed | Merchant confirmed fulfillment |
| payment.expired | Payment timed out |
| payment.failed | On-chain transaction reverted |
| payment.refunded | Escrow refunded to payer (timeout/dispute) |
| payment.cancelled | Payment cancelled by merchant/operator |
| dispute.opened | Payer opened dispute |
| dispute.resolved | Arbiter resolved dispute |
11.2 Security
- HMAC-SHA256 signature in
X-Nexus-Signatureheader - Exponential backoff retry (6 attempts: 10s, 30s, 2min, 10min, 30min)
- Idempotent via event_id
- Delivery logged in
webhook_delivery_logstable
12. Database Schema
Core tables:
- payments: Payment order records (13-state machine, escrow fields, protocol field: 'nups' | 'ap2')
- payment_groups: Group aggregation records (batch deposits)
- payment_events: Event sourcing (append-only, 15 event types)
- merchant_registry: Merchant identity + marketplace metadata + ap2_did
- webhook_delivery_logs: Webhook delivery tracking with retry state
- mandate_evidence: AP2 Verifiable Credential storage (new in v4)
13. Deployed Addresses
| Contract | Address | Type |
|---|---|---|
| NexusEscrow (Proxy) | 0xeB33a9C2b4c7D3F44Fd5514F90C355AF6bb79236 | UUPS Proxy |
| NexusEscrow (Impl v4.3.0) | 0xF6ED311f8ea594572872E78DB945277ab37ECE37 | Implementation |
| XSGD | 0x0Fd437613dE3d14F4dDaB8331DC0f2C0C543BdD0 | ERC-3009 Token |
| USDC (legacy) | 0xFF8dEe9983768D0399673014cf77826896F97e4d | ERC-3009 Token |
| Relayer / Core Operator | 0xf7EA5d3f0Bf8185c4f3C2F405D9a71009CF4D920 | EOA |
14. Contract Version History
| Version | Date | Key Changes |
|---|---|---|
| v2.0.0 | 2026-02-24 | Initial escrow (depositWithAuthorization, release, refund, dispute, resolve) |
| v3.0.0 | 2026-02-24 | Batch deposits, RESOLVED_SPLIT, non-upgradeable |
| v4.0.0 | 2026-02-26 | UUPS proxy, batchDepositWithGroupApproval, refundUnresolvedDispute, MAX_BATCH_SIZE=20, feeBps snapshot |
| v4.1.0 | 2026-03-12 | cancelByOperator() — immediate refund by operator/merchant |
| v4.3.0 | 2026-04-01 | batchDepositGasless() — relayer-submitted gasless deposits, setToken() for multi-token |
15. Upgrade Path
| Current (this RFC) | Future |
|---|---|
| Gasless Escrow (single chain) | Hub-Spoke cross-chain (PlatON + Base + ETH) |
| Local merchant_registry | On-chain NexusMerchantRegistry |
| Basic signature verification | Full RiskGatekeeper with Permit (RFC-006) |
| PlatON Devnet only | Multi-chain production |
| XSGD/USDC stablecoins | Multi-asset (any ERC-3009 token) |
| AP2 Credential Provider | Full AP2 ecosystem participation |
| Browser checkout + MCP | @nexus/buyer-skills SDK (RFC-011) |
16. Copyright
Copyright (c) 2026 Nexus Protocol. All Rights Reserved.