Skip to content

Commit 439bd19

Browse files
authored
Merge pull request #358 from lukaisailovic/wallet-pay-caip
CAIP-358: Universal Payment Request Method
2 parents 66fdf0d + bd1209b commit 439bd19

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed

CAIPs/caip-358.md

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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

Comments
 (0)