Payment Request v1
Signed terms, QR transport and the checks a compatible payer must implement.
This manual targets 0.1.0-beta.3. Read the source and availability policy before installing. Historical reports retain their original scope.
The existing request envelope is the interoperability boundary for a compatible payer. This specification documents beta.3; the QR and readiness UX are additions since beta.2. There is no registered ppops:// scheme and no claim that an ordinary wallet can scan and execute it.
Transport
GET /pay/:opaqueIntentId/request.json returns the public JSON request. The checkout QR encodes its absolute URL, not a bare receiving address. Copy and download expose the same request. Production uses HTTPS; loopback HTTP is for local development only and cannot be reached by a wallet on another device.
Do not append order IDs, customer information, credentials or trust pins. The reference payer refuses redirects and bounds URL downloads. A local request file can be inspected/prepared, but submission requires the live request to recheck current state. Treat the URL/file as private: holders can inspect payment terms and status. No merchant API authorization token belongs in a payer request.
Signed descriptor
EIP-712 domain: name “PPOps Payment Descriptor”, version “1”, chainId matching the descriptor. The PPOpsPaymentDescriptorV1 type has these ordered fields:
| Field | EIP-712 type |
|---|---|
| version | uint8 |
| chainId | uint256 |
| rail | string |
| tokenAddress | address |
| decimals | uint8 |
| amountAtomic | uint256 |
| recipient0zk | string |
| reference | bytes32 |
| expiresAt | uint64 |
| nonce | bytes32 |
| merchantSigner | address |
See src/security/descriptor-format.ts in the source checkout and the HTTP schema. Amounts are decimal integer strings, never floating point. Expiry is Unix seconds. Reference is preserved as the transfer’s encrypted memo: ppops:v1: followed by the complete 0x-prefixed reference.
The public envelope repeats payment terms and adds id, amountFormatted, expectedMerchantSigner, status, receivedAmountAtomic, pendingAmountAtomic and reconciliationReady. Current source also adds paymentStage and overpaymentAmountAtomic; the local demo marks simulated: true. These outer presentation/status fields are not covered by the descriptor signature. amountFormatted is a convenience, not authoritative arithmetic. Consumers must tolerate additive outer fields while rejecting inconsistent signed terms.
Required payer checks
- Obtain the expected merchant public signer through a separate trusted channel. The request’s expectedMerchantSigner is not independent identity evidence.
- Recover the signature against the exact domain/type. Match the recovered, descriptor and independently pinned signer addresses.
- Require version 1, rail railgun, Arbitrum One chain 42161, native USDC address 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 and 6 decimals for this profile.
- Bind every repeated term to the descriptor. Decode/validate the private receiver with the wallet’s RAILGUN address implementation. Check positive amount, exact reference/memo and unexpired request. Reject simulated requests.
- Require an OPEN request with no received or pending payment and no explicit merchant reconciliation-unavailable flag. Never infer safe retry from a timeout.
- Check local readiness against amount plus fee budget. Obtain a current quote, show amount/fee/total, then require explicit payer authorization.
- Re-read the live request and compare its identity/descriptor before submission; preserve the payment reference and existing submission recovery journal.
- Observe the merchant’s acceptance state. A submitted transaction or signed descriptor is not a receipt. Only the merchant reconciler can produce its payment.confirmed event after FINALIZED + SPENDABLE + MATCHED.
The checkout verifies signed fields locally but is not a replacement for a payer’s independent verification, recipient decoding or authorization. The reference implementation is documented in payer integration.