|
| 1 | +--- |
| 2 | +caip: CAIP-358 |
| 3 | +title: Universal Payment Request Method |
| 4 | +author: Luka Isailovic (@lukaisailovic), Derek Rein (@arein) |
| 5 | +discussions-to: https://github.com/ChainAgnostic/CAIPs/pull/358 |
| 6 | +status: Draft |
| 7 | +type: Standard |
| 8 | +created: 2025-05-26 |
| 9 | +updated: 2025-05-26 |
| 10 | +requires: 2, 10, 19 |
| 11 | +replaces: |
| 12 | +--- |
| 13 | + |
| 14 | + |
| 15 | +## Simple Summary |
| 16 | + |
| 17 | +A standard for enabling one-interaction cryptocurrency payment experiences across wallets and dapps, allowing payment information to be transmitted in a single round-trip. |
| 18 | + |
| 19 | +## Abstract |
| 20 | + |
| 21 | +This CAIP standardizes a wallet <> dapp JSON-RPC method `wallet_pay` for more efficient communication about the purchase intent from the dapp to the wallet. |
| 22 | +The method allows merchants to specify payment requirements enabling wallets to handle payment execution with minimal user interaction. |
| 23 | + |
| 24 | +## Motivation |
| 25 | + |
| 26 | +Current cryptocurrency payment experiences are either error-prone (manual transfers, address QR codes) or suboptimal, requiring multiple interactions from the user. |
| 27 | +In addition to this, different payment providers implement different payment experiences, creating confusion. |
| 28 | + |
| 29 | +Solutions like EIP-681 or `bitcoin:` url are ecosystem-specific and have not historically gotten sufficient support from the wallets. They tend to rely on a QR code scan as well, which means that they can't be batched as part of a connection-flow using protocols like WalletConnect. |
| 30 | + |
| 31 | +By standardizing the payment experience on both the application side and the wallet side, we can reduce user errors during payment, providing the payment experience in as few clicks as possible and reducing the friction in crypto payments. |
| 32 | + |
| 33 | +The method transmits all the acceptable payment requests so the wallet can pick the most optimal one based on the assets that user has in the account and the wallet's capabilities. |
| 34 | + |
| 35 | +## Specification |
| 36 | + |
| 37 | +### Method: `wallet_pay` |
| 38 | + |
| 39 | +#### Request |
| 40 | + |
| 41 | +```typescript |
| 42 | +type Hex = `0x${string}`; |
| 43 | + |
| 44 | +type PaymentOption = { |
| 45 | + asset: string; |
| 46 | + amount: Hex; |
| 47 | + recipient: string; |
| 48 | +} |
| 49 | + |
| 50 | +// JSON-RPC Request |
| 51 | +type PayRequest = { |
| 52 | + version: string; |
| 53 | + orderId?: string; |
| 54 | + acceptedPayments: PaymentOption[]; |
| 55 | + expiry: number; |
| 56 | +} |
| 57 | + |
| 58 | +``` |
| 59 | + |
| 60 | +The application **MUST** include: |
| 61 | + |
| 62 | +- At least one entry in the `acceptedPayments` array |
| 63 | +- `expiry` timestamp for the payment request |
| 64 | + |
| 65 | +The application **MAY** include: |
| 66 | + |
| 67 | +- An `orderId` that uniquely identifies this payment request. If provided, `orderId` **MUST NOT** be longer than 128 characters. |
| 68 | + |
| 69 | + When `orderId` is provided, it **MUST** be a string and implementations **SHOULD** ensure this ID is unique across their system to prevent collisions. |
| 70 | + |
| 71 | +The `acceptedPayments` field **MUST** be an array of `PaymentOption` objects. Each element in the array represents a payment option that the wallet can choose from to complete the payment. |
| 72 | + |
| 73 | +For `PaymentOption` options: |
| 74 | + |
| 75 | +- The `recipient` field **MUST** be a valid [CAIP-10][] account ID. |
| 76 | +- The `asset` field **MUST** follow the [CAIP-19][] standard. |
| 77 | +- The `amount` field **MUST** be a hex-encoded string representing the amount of the asset to be transferred. |
| 78 | +- The [CAIP-2][] chainId component in the [CAIP-19][] `asset` field **MUST** match the [CAIP-2][] chainId component of the [CAIP-10][] `recipient` account ID. |
| 79 | + |
| 80 | +The `expiry` field **MUST** be a UNIX timestamp (in seconds) after which the payment request is considered expired. Wallets **SHOULD** check this timestamp before processing the payment. |
| 81 | + |
| 82 | +Request example: |
| 83 | + |
| 84 | +```json |
| 85 | +{ |
| 86 | + "version": "1.0.0", |
| 87 | + "orderId": "order-123456", |
| 88 | + "acceptedPayments": [ |
| 89 | + { |
| 90 | + "recipient": "eip155:1:0x71C7656EC7ab88b098defB751B7401B5f6d8976F", |
| 91 | + "asset": "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", |
| 92 | + "amount": "0x5F5E100" |
| 93 | + }, |
| 94 | + { |
| 95 | + "recipient": "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", |
| 96 | + "asset": "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ/slip44:501", |
| 97 | + "amount": "0x6F05B59D3B20000" |
| 98 | + } |
| 99 | + ], |
| 100 | + "expiry": 1709593200 |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +#### Response |
| 105 | + |
| 106 | +```typescript |
| 107 | +type PayResult = { |
| 108 | + version: string; |
| 109 | + orderId?: string; |
| 110 | + txid: string; |
| 111 | + recipient: string; |
| 112 | + asset: string; |
| 113 | + amount: Hex; |
| 114 | +} |
| 115 | +``` |
| 116 | +
|
| 117 | +The wallet's response MUST include: |
| 118 | +
|
| 119 | +- `txid` with the transaction identifier on the blockchain |
| 120 | +- `recipient` that received the payment. It **MUST** be a valid [CAIP-10][] account ID. |
| 121 | +- `asset` that was used for payment. It **MUST** follow the [CAIP-19][] standard. |
| 122 | +- `amount` that was paid. It **MUST** be represented in hex string |
| 123 | +
|
| 124 | +If an `orderId` was provided in the original request, the response **MUST** include the same `orderId`. |
| 125 | +
|
| 126 | +`txid` **MUST** be a valid transaction identifier on the blockchain network specified in the asset's chain ID. |
| 127 | +
|
| 128 | + `recipient`, `asset`, and `amount` **MUST** match those specified in the selected direct payment option in the `acceptedPayments` array. |
| 129 | +
|
| 130 | +
|
| 131 | +Example response: |
| 132 | +```json |
| 133 | +{ |
| 134 | + "version": "1.0.0", |
| 135 | + "orderId": "order-123456", |
| 136 | + "txid": "0x8a8c3e0b1b812182db4cabd81c9d6de78e549fa3bf3d505d6e1a2b25a15789ed", |
| 137 | + "recipient": "eip155:1:0x71C7656EC7ab88b098defB751B7401B5f6d8976F", |
| 138 | + "asset": "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", |
| 139 | + "amount": "0x5F5E100" |
| 140 | +} |
| 141 | +``` |
| 142 | +
|
| 143 | +#### Idempotency |
| 144 | +
|
| 145 | +The `wallet_pay` method **MUST** be idempotent for the same `orderId` when provided. This ensures robustness in case of connection failures or timeout scenarios. |
| 146 | +
|
| 147 | +Requirements when `orderId` is provided: |
| 148 | +
|
| 149 | +- If a payment with the same `orderId` has already been completed successfully, the wallet **MUST** return the original `PayResult` without executing a new payment |
| 150 | +- If a payment with the same `orderId` is currently pending, the wallet **SHOULD** return the result of the original payment attempt |
| 151 | +- If a payment with the same `orderId` has failed previously, the wallet **MAY** attempt the payment again or return the previous error |
| 152 | +- Wallets **SHOULD** maintain payment status for completed transactions for at least 24 hours after completion |
| 153 | +- If connection is lost during payment execution, dapps **MAY** retry the same request to query the payment status |
| 154 | +
|
| 155 | +When `orderId` is not provided: |
| 156 | +
|
| 157 | +- Each payment request **SHOULD** be treated as a new payment attempt |
| 158 | +- Wallets **MAY** implement their own deduplication logic based on other request parameters (recipient, asset, amount, expiry) |
| 159 | +- Dapps **SHOULD** include an `orderId` if they require guaranteed idempotency behavior |
| 160 | +
|
| 161 | +#### Error Handling |
| 162 | +
|
| 163 | +If the payment process fails, the wallet **MUST** return an appropriate error message: |
| 164 | +
|
| 165 | +```typescript |
| 166 | +type PayError = { |
| 167 | + code: number; |
| 168 | + message: string; |
| 169 | + data?: any; |
| 170 | +} |
| 171 | +``` |
| 172 | +
|
| 173 | +The wallet **MUST** use one of the following error codes when the pay request fails: |
| 174 | +
|
| 175 | +- When user rejects the payment |
| 176 | + - code = 8001 |
| 177 | + - message = "User rejected payment" |
| 178 | +- When no matching assets are available in user's wallet |
| 179 | + - code = 8002 |
| 180 | + - message = "No matching assets available" |
| 181 | +- When the payment request has expired |
| 182 | + - code = 8003 |
| 183 | + - message = "Payment request expired" |
| 184 | +- When there are insufficient funds for the payment |
| 185 | + - code = 8004 |
| 186 | + - message = "Insufficient funds" |
| 187 | +
|
| 188 | +If a wallet does not support the `wallet_pay` method, it **MUST** return an appropriate JSON-RPC error with code -32601 (Method not found). |
| 189 | +
|
| 190 | +Example error response: |
| 191 | +
|
| 192 | +```json |
| 193 | +{ |
| 194 | + "id": 1, |
| 195 | + "jsonrpc": "2.0", |
| 196 | + "error": { |
| 197 | + "code": 8001, |
| 198 | + "message": "User rejected payment" |
| 199 | + } |
| 200 | +} |
| 201 | +``` |
| 202 | +
|
| 203 | +## Rationale |
| 204 | +
|
| 205 | +This specification evolved through multiple iterations to address fundamental usability issues in cryptocurrency payment flows. Initial exploration began as a CAIP alternative to EIP-681/Solana Pay, but analysis of existing payment service provider (PSP) implementations revealed significant friction in current user experiences. |
| 206 | +
|
| 207 | +Existing cryptocurrency payment flows typically require users to: |
| 208 | +
|
| 209 | +- Select a token type |
| 210 | +- Choose a blockchain network |
| 211 | +- Wait for address/QR code generation |
| 212 | +- Complete the transfer manually |
| 213 | +
|
| 214 | +This multi-step process creates excessive friction, often requiring 4-6 user interactions for a simple payment. |
| 215 | +
|
| 216 | +The `wallet_pay` method addresses these limitations by: |
| 217 | +
|
| 218 | +- Moving choice to the wallet rather than forcing merchants to pre-select payment methods, wallets can filter available options based on user account balances and preferences |
| 219 | +- All payment options are transmitted in one request, eliminating the need for multiple user interactions |
| 220 | +- The response includes transaction ID and execution details, providing immediate confirmation |
| 221 | +- Can be batched with connection establishment, enabling "connect + pay" flows in protocols like WalletConnect |
| 222 | +
|
| 223 | +### Alternative Approaches Considered |
| 224 | +
|
| 225 | +An intermediate solution involved encoding multiple payment addresses in a single QR code, allowing merchants to present all payment options simultaneously. |
| 226 | +However, this approach proved impractical for dapp implementations because: |
| 227 | +
|
| 228 | +- PSPs cannot determine which payment option was selected |
| 229 | +- Monitoring requires polling up to 20+ addresses simultaneously |
| 230 | +- No confirmation mechanism exists for payment completion |
| 231 | +
|
| 232 | +## Test Cases |
| 233 | +
|
| 234 | +TODO |
| 235 | +
|
| 236 | +## Security Considerations |
| 237 | +
|
| 238 | +`wallet_pay` does not try to address various cases of merchant fraud that end-users are exposed to today. |
| 239 | +Specifically it does not try to tackle merchant fraud insurance in case the sold good is not delivered. |
| 240 | +It also does not attempt to provide dispute functionality. These present ideas for future work. |
| 241 | +
|
| 242 | +## Privacy Considerations |
| 243 | +
|
| 244 | +TODO |
| 245 | +
|
| 246 | +## Backwards Compatibility |
| 247 | +
|
| 248 | +TODO |
| 249 | +
|
| 250 | +<!-- All CAIPs that introduce backwards incompatibilities must include a section describing these incompatibilities and their severity. The CAIP must explain how the author proposes to deal with these incompatibilities. CAIP submissions without a sufficient backwards compatibility treatise may be rejected outright. --> |
| 251 | +
|
| 252 | +## References |
| 253 | +
|
| 254 | +- [CAIP-1] defines the CAIP document structure |
| 255 | +- [EIP-681] is ethereum-specific prior art that also includes gas information in the URI |
| 256 | +
|
| 257 | +[CAIP-1]: https://ChainAgnostic.org/CAIPs/caip-1 |
| 258 | +[CAIP-2]: https://ChainAgnostic.org/CAIPs/caip-2 |
| 259 | +[CAIP-10]: https://ChainAgnostic.org/CAIPs/caip-10 |
| 260 | +[CAIP-19]: https://ChainAgnostic.org/CAIPs/caip-19 |
| 261 | +[EIP-681]: https://eips.ethereum.org/EIPS/eip-681 |
| 262 | +
|
| 263 | +## Copyright |
| 264 | +
|
| 265 | +Copyright and related rights waived via [CC0](../LICENSE). |
0 commit comments