Skip to content

RFC-005 v4: Nexus Payment Core Specification (Gasless Escrow Settlement)

MetadataValue
TitleNexus Payment Core Specification
Version4.0.0
StatusStandards Track (Draft)
SupersedesRFC-005 v3.0.0 (Escrow Settlement)
AuthorCipher & Nexus Architect Team
Created2026-02-24
Updated2026-04-01
ScopeOrchestration, State Management, Gasless Escrow, Group Payments, AP2 Protocol, Cancel/Dispute, Multi-Token
ChainPlatON Devnet (chain_id: 20250407)
TokenERC-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):

  1. Gasless Deposit: Users only sign an EIP-3009 authorization; the Relayer pays gas and submits batchDepositGasless()
  2. Cancel mechanism: Merchants/operators can cancel payments in any non-terminal state; on-chain ESCROWED payments are immediately refunded via cancelByOperator()
  3. AP2 Protocol: Acts as a Credential Provider for Agent Protocol 2, issuing PaymentMandate Verifiable Credentials
  4. Multi-Token: Supports multiple ERC-3009 compatible tokens via setToken() (current: XSGD)
  5. 13-State Machine: Adds the CANCELLED state
  6. Auto-Refund Late Deposits: Automatically triggers a refund when an expired payment receives a late on-chain deposit

2. Design Principles

  1. Escrow Settlement: Funds are guaranteed through a smart contract, released after merchant fulfillment, with automatic refund on timeout
  2. 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
  3. Batch Efficiency: Multiple payments are aggregated into a group, sharing a single on-chain transaction
  4. MCP-First: All capabilities are exposed through the MCP Protocol, with REST API also available
  5. Event Sourcing: Each state change generates an immutable event record
  6. Anti-MITM: Core Operator signs Group instructions, verified on-chain to prevent tampering
  7. 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 = COMPLETED

4.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 transition

4.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 orders

4.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_CANCELLED

4.5 HTTP 402 Payment Required Response

typescript
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

typescript
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_tx field (users no longer submit transactions directly)

4.7 GroupPaymentDetail Schema

typescript
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)

StatusDescriptionTrigger
CREATEDQuote verified, payment createdorchestrate_payment success
AWAITING_TXLegacy: UA has PaymentInstructionDirect Transfer mode (deprecated)
BROADCASTEDLegacy: UA submitted tx_hashDirect Transfer mode (deprecated)
ESCROWEDFunds deposited in escrow contractChain Watcher (Deposited event)
SETTLEDEscrow released to merchantChain Watcher (Released event)
COMPLETEDMerchant confirmed fulfillmentconfirm_fulfillment call
EXPIREDPayment timed outTimeout Handler
TX_FAILEDOn-chain transaction revertedChain Watcher
RISK_REJECTEDSecurity check failedSecurity Module (future)
REFUNDEDEscrow refunded to payerChain Watcher (Refunded event)
DISPUTE_OPENPayer opened disputeChain Watcher (Disputed event)
DISPUTE_RESOLVEDArbiter resolved disputeChain Watcher (Resolved event)
CANCELLEDPayment cancelled by merchant/operatornexus_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 StatusDescription
GROUP_CREATEDGroup created, payments pending
GROUP_AWAITING_TX402 returned, waiting for on-chain tx
GROUP_DEPOSITEDOn-chain deposit confirmed (receipt verified)
GROUP_ESCROWEDAll child payments transitioned to ESCROWED
GROUP_SETTLEDAll child payments released
GROUP_COMPLETEDAll child payments completed
GROUP_EXPIREDGroup timed out
GROUP_PARTIALMixed states (some settled, some disputed)
GROUP_CANCELLEDAll child payments cancelled (new in v4)

5.4 Timeout Rules

ScenarioTimeoutAction
Quote expiryquote.expiry timestampCREATED -> EXPIRED
Awaiting deposit30 minutesCREATED -> EXPIRED
Escrow release deadline24 hours (on-chain, ms)refund() callable by anyone
Dispute window72 hours (on-chain, ms)dispute() no longer callable
Arbitration timeout7 days (on-chain, ms)refundUnresolvedDispute() callable

PlatON Devnet note: block.timestamp returns 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:

  1. Chain Watcher detects the Deposited event
  2. Automatically calls Relayer.submitRefund() to trigger an on-chain refund
  3. Status remains unchanged (stays EXPIRED), but the Refunded event is recorded
  4. Prevents user funds from being locked in an expired order

6. Security Specification

6.1 EIP-712 Quote Verification

typescript
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:

  1. Reconstruct EIP-712 TypedData from quote fields
  2. Recover signer address from signature
  3. Resolve merchant_did via merchant_registry to obtain the registered signer
  4. Compare recovered address with registered signer
  5. 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:

typescript
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:

sql
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)

ToolCallerDescription
nexus_orchestrate_paymentUAVerify quotes, create payment group, return 402 with BatchDepositInstruction
nexus_get_payment_statusUA/MAQuery payment or group status
nexus_confirm_fulfillmentMAConfirm merchant delivery, trigger escrow release
nexus_release_paymentMA/CoreExplicit escrow release
nexus_dispute_paymentPayerOpen dispute within dispute window
nexus_resolve_disputeArbiterResolve dispute with bps split
nexus_cancel_paymentMACancel single payment (pre-escrow or on-chain)
nexus_cancel_orderMACancel all payments in a group
nexus_get_merchantUAGet merchant profile + marketplace metadata
nexus_discover_agentsUASearch agent catalog by keyword
nexus_get_agent_skillUAFetch agent's skill.md

7.2 REST API

MethodPathDescription
POST/api/orchestrateVerify quotes, return 402 with payment instruction
GET/api/checkout/:tokenGet group instruction JSON
POST/api/checkout/:token/relay-depositGasless deposit (new in v4): submit EIP-3009 sig, relayer submits on-chain
POST/api/checkout/:token/confirmLegacy: confirm user-submitted tx_hash
GET/api/paymentsQuery payments (by nexus_payment_id, merchant_order_ref, group_id)
GET/api/payments/:idGet payment details
GET/api/merchant/paymentsMerchant portal: list payments (filter by did, status, since)
POST/api/merchant/confirm-fulfillmentMerchant confirms fulfillment (triggers release)
POST/api/merchant/cancel-paymentCancel single payment
POST/api/merchant/cancel-orderCancel all payments in group
GET/api/agentsList all registered agents
GET/api/agents/:did/skillFetch agent's skill.md

7.3 Checkout Page

PathDescription
/checkout/:groupIdBrowser checkout page (MetaMask integration, Gasless)
/portalCore 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

typescript
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)

typescript
interface CartMandateSubject {
  merchant_did: string;
  cart_id: string;
  total: string;
  currency: string;
  line_items: CartLineItem[];
  expires_at: string;
}

IntentMandate VC (issued by User Agent)

typescript
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:

typescript
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)

typescript
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

sql
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.cancelled

10. Relayer Service

The Relayer is a server-side service that submits transactions on behalf of the Core:

OperationTriggerGas Payerv3v4
batchDepositGasless(...)User signs EIP-3009RelayerN/ANEW
release(paymentId)Merchant confirms fulfillmentRelayerYesYes
refund(paymentId)Timeout auto-refundRelayerYesYes
cancelByOperator(paymentId)Merchant/operator cancelRelayerN/ANEW
resolve(paymentId, bps)Arbiter resolves disputeRelayerN/ANEW

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)

EventTrigger
payment.createdPayment created
payment.escrowedFunds deposited in escrow
payment.settledEscrow released to merchant
payment.completedMerchant confirmed fulfillment
payment.expiredPayment timed out
payment.failedOn-chain transaction reverted
payment.refundedEscrow refunded to payer (timeout/dispute)
payment.cancelledPayment cancelled by merchant/operator
dispute.openedPayer opened dispute
dispute.resolvedArbiter resolved dispute

11.2 Security

  • HMAC-SHA256 signature in X-Nexus-Signature header
  • Exponential backoff retry (6 attempts: 10s, 30s, 2min, 10min, 30min)
  • Idempotent via event_id
  • Delivery logged in webhook_delivery_logs table

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

ContractAddressType
NexusEscrow (Proxy)0xeB33a9C2b4c7D3F44Fd5514F90C355AF6bb79236UUPS Proxy
NexusEscrow (Impl v4.3.0)0xF6ED311f8ea594572872E78DB945277ab37ECE37Implementation
XSGD0x0Fd437613dE3d14F4dDaB8331DC0f2C0C543BdD0ERC-3009 Token
USDC (legacy)0xFF8dEe9983768D0399673014cf77826896F97e4dERC-3009 Token
Relayer / Core Operator0xf7EA5d3f0Bf8185c4f3C2F405D9a71009CF4D920EOA

14. Contract Version History

VersionDateKey Changes
v2.0.02026-02-24Initial escrow (depositWithAuthorization, release, refund, dispute, resolve)
v3.0.02026-02-24Batch deposits, RESOLVED_SPLIT, non-upgradeable
v4.0.02026-02-26UUPS proxy, batchDepositWithGroupApproval, refundUnresolvedDispute, MAX_BATCH_SIZE=20, feeBps snapshot
v4.1.02026-03-12cancelByOperator() — immediate refund by operator/merchant
v4.3.02026-04-01batchDepositGasless() — 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_registryOn-chain NexusMerchantRegistry
Basic signature verificationFull RiskGatekeeper with Permit (RFC-006)
PlatON Devnet onlyMulti-chain production
XSGD/USDC stablecoinsMulti-asset (any ERC-3009 token)
AP2 Credential ProviderFull AP2 ecosystem participation
Browser checkout + MCP@nexus/buyer-skills SDK (RFC-011)

Copyright (c) 2026 Nexus Protocol. All Rights Reserved.