Skip to content
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions CAIPs/caip-358.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
---
caip: CAIP-358
title: Universal Payment Request Method
author: Luka Isailovic (@lukaisailovic), Derek Rein (@arein)
discussions-to: []
status: Draft
type: Standard
created: 2025-05-26
updated: 2025-05-26
requires: 2, 10, 19
replaces:
---


## Simple Summary
<!--"If you can't explain it simply, you don't understand it well enough." Provide a simplified and layman-accessible explanation of the CAIP.-->
A standard for enabling one-interaction cryptocurrency payment experiences across wallets and dapps, allowing payment information to be transmitted in a single round-trip.

## Abstract
<!--A short (~200 word) description of the technical issue being addressed.-->
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.
The method allows merchants to specify payment requirements enabling wallets to handle payment execution with minimal user interaction.

## Motivation
<!--The motivation is critical for CAIP. It should clearly explain why the state of the art is inadequate to address the problem that the CAIP solves. CAIP submissions without sufficient motivation may be rejected outright.-->
Current cryptocurrency payment experiences are either error-prone (manual transfers, address QR codes) or suboptimal, requiring multiple interactions from the user.
In addition to this, different payment providers implement different payment experiences, creating confusion.

Solutions like EIP-681 or `bitcoin:` url are ecosystem specific and didn't get much support from the wallets. They rely on the QR code scan as well, which means that they can't be batched during connection with protocols like WalletConnect.

By standardizing the payment experience both dapp side and wallet side, we can reduce user errors during payment, provide the payment experience in as few clicks as possible reducing the friction in crypto payments.

The method transmits all the possible payment requests so the wallet can pick the most optimal one based on the assets that user has in the account.

## Specification
<!--The technical specification should describe the standard in detail. The specification should be detailed enough to allow competing, interoperable implementations. -->
### Method: `wallet_pay`
#### Request

```typescript
type Hex = `0x${string}`;

type PaymentOption = {
asset: string;
amount: Hex;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am rusty on this, but does amount need to be a Hex, or can it be a simpler string like "100.0"? Is it common for non-evm wallets to use hex's here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think hex is a pretty good convention. If you put "100.0" then everyone has to do the conversion. Pretty much all downstream RPCs and SDKs accept hex

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw hex is extremely not standard in Solana

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can consider uint

Copy link
Copy Markdown

@jxom jxom Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need to overthink it. Hex is a universal convention beyond crypto applications (ie. JS 0x69 resolves to 105 as a number, and we are referring to a JSON-RPC interface here) for numbers exceeding Number.MAX_SAFE_INTEGER.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is something that's going to be interpreted by the wallet is there a reason to not just go with JSON number? Seems like then it just becomes the wallets responsibility to format it correctly for the particular chain in the transaction. In this way language specific aspects around types (e.g. some need doubles, floats, uints, etc) also become simpler too.

recipient: string;
}

// JSON-RPC Request
type PayRequest = {
orderId: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if this should by inside of a good old context field. curious if you think this could be used for p2p use cases too, where there might not really be a meaningful orderId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ to making this optional. One of the cases I think we may be interested in this for eventually would be for P2P creator tipping in Brave Rewards.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we can make it optional.

@lukasrosario currently its the only property, but if we wanted to future proof it it might make sense

acceptedPayments: PaymentOption[];
expiry: number;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to also add an optional, human readable identifier like label / description here? Something similar to Solana Pay to help user identify payment request. Perhaps multiple of these can come through when they open up a wallet or come back online etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Solana Pay includes the following which are not specified here but have had proven use:

  • Label: UTF-8 string that describes the source of the transfer request (ex: a brand, store, app etc)
  • Message: UTF-8 string that is user facing to describe the nature of the request. This can help add context to wallets who display this request.
  • Memo: similar to the orderId above but more broad in specification

Additionally, on Solana you can embed arbitrary public keys in the message to allow for easy tracking or reference. This is the "reference" field and is extremely helpful for wallets/apps to be able to track client IDs

Copy link
Copy Markdown
Contributor

@kdenhartog kdenhartog Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have there been instances of this data being fraudulently submitted by a scam site for the purposes of tricking the user? It seems like due to instant settlements of crypto payments, there's more risk to submitting these payments to untrusted sites that have no intention of shipping goods or services once they receive payment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure there have been instances.. it's crypto so scams are bound to happen. I think we can fix this by having some sort of tls-like auth mechanism, or perhaps an integrity signature. I'm not sure if this would be defined in this caip or at a protocol level or as an additional capability to be adopted by teams where it makes sense

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My $0.02: there's not just scams in the form of "this data being fraudulently submitted by a scam site for the purposes of tricking the user" but there's also the problem of "fake stores" selling you iPhones that don't exist.

I believe trying to accomplish this with wallet_pay in its initial form is too much. But in a future iteration or addendum the problem of merchant fraud insurance must be covered.

FWIW: if wallet_pay is transmitted via WalletConnect then the transport provides some scam protection via WalletConnects "Verify API"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @arein
This should definitely be left out. By default wallet would be unable to display untrusted information, or at least it should never do that.

}

```

The application **MUST** include:
- An `orderId` that uniquely identifies this payment request. `orderId` **MUST NOT** be longer than 128 characters.
- At least one entry in the `acceptedPayments` array
- `expiry` timestamp for the payment request


The `orderId` field **MUST** be a string that uniquely identifies the payment request. Implementations **SHOULD** ensure this ID is unique across their system to prevent collisions.

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.

For `PaymentOption` options:
- The `recipient` field **MUST** be a valid [CAIP-10][] account ID.
- The `asset` field **MUST** follow the [CAIP-19][] standard.
- The `amount` field **MUST** be a hex-encoded string representing the amount of the asset to be transferred.
- 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the [CAIP-107][] namespace component? 😏

Copy link
Copy Markdown
Contributor Author

@lukaisailovic lukaisailovic Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdym? chainId is namespace + reference (blockchain Id). Do you mean asset namespace in caip-19? That can't be used for anything, not sure why should we compare it to CAIP-2


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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
The `expiry` field **MUST** be a UNIX timestamp (in seconds) after which the payment request is considered expired. Wallets **SHOULD** check this timestamp is not in the past before proposing or submitting a payment.



Request example:
```json
{
"orderId": "order-123456",
"acceptedPayments": [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be for another CAIP or even controversial, but with stablecoin proliferation, merchants will soon accept dozens of different stablecoins while only caring about the underlying value (e.g., USD). Moreover, users often hold multiple USD stablecoins across different chains and should be able to use any of them when paying in USD, rather than having to choose a specific implementation. This also applies to other asset equivalents like BTC and wrapped BTC variants.
Adding a grouping or equivalency concept to the payment options where assets with the same underlying value can be grouped together would let wallets show "Pay $100 in USD" where users can either pick a specific asset or let the wallet automatically select the best option (lowest fees, available balance) instead of forcing them to pick between USDC on Ethereum vs Polygon vs Solana when they just want to pay in dollars.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems worth exploring what works better here through implementation experimentation a bit more. When I was thinking about this, I was somewhat expecting that would be the case for the most part anyways, but through implementation specific wallet UX, so that we can optimize on this overtime. What I originally had in mind was that the wallet operates on the following heuristic:

  1. Check which currencies the recipient wants and which are available without a swap/bridge
  2. If payment can be handled with currently available funds, offer that first. It's almost certainly lowest in terms of fees, but this is an assumption that may be invalidated as networks and swap/bridge protocols compete for lower fees.
  3. If swap/bridge is necessary, check which current currencies the user holds can cover funds. Also, determine which of the currencies the recipient wants to have the most liquidity.
  4. Then get a swap/bridge quote from users current funds that can cover for each pair of users funds and the most liquid stablecoin chosen.
  5. Display these options to the user from least to most fees, with the default being least.

Additionally, allow the user to override as necessary in optional transaction panel, in case they don't want to use a particular fund. I think Users will demand what sort of UX they want here too, so we can optimize a bit further as the feature becomes available, without needing to change the RPC data model. That should remain our top priority here IMO.

From the merchant's perspective, one of the issues we've found with spending/receiving funds today with BAT and rewards ads is that there's a constant need to bridge/swap funds and in some cases off-ramp back to fiat (hopefully that's not a problem of the future). This can be a bit of a headache for accounting/tax purposes on the merchant side. So, part of the advantage to the recipient specifying network/currency exactly is that it makes merchant onboarding a bit easier in terms of fund management which makes it more likely that Metcalfe's law kicks in here. E.g. they can always have USDC on their network of choice and easily offramp that as needed to pay their invoices/employees. Additionally, since most wallets universally have some form of swap/bridge functionality I don't think it's going to be overly difficult for us to swap/bridge as needed and handle the complexity for the user. This also means we'll be able to easily handle currency conversion via stablecoins using the same heuristic too.

I may be making some incorrect assumptions here though, so thank you for calling this out. Ultimately, whatever the implementation feedback and early testing with users/merchants tells us here is going to be the decider IMO.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a grouping or equivalency concept to the payment options where assets with the same underlying value can be grouped together would let wallets show "Pay $100 in USD"

@argjv Agreed that this requires orthogonal/separate specification, whether as another CAIP or outside of CASA. Still going to chime in here because I used to work at Circle and am fascinated personally by the topic-- tag me if this is proposed somewhere else as a spec!

Upfront I would just say that $100 in USDC (on any chain/in any formfactor) is an abstraction that makes sense to me (cf. Layer Zero and Circle's devrel strategy), but $100 in "any stablecoin" is a non-starter IMHO: not just because some people have hard regulatory requirements to use, e.g. Circle but not Tether, or Paxos but not Luna/GUSD/etc etc, but ALSO because their "semantic equivalence" is just not deliverable in practice given the way DEXs and solvers and bridges are incentivized by their contracts, API billing structures, and the terms of their integrations deals to favor one over the other and devoid "de minimis" monthly transaction volumes in any other stables...

What I could see happening is USDC and comparable-regulated stablecoins federating to create one broader meta-coin, e.g. "USDC+", which would abstract over and translate between USDC on any chain, Paxos on any chain, etc etc.

{
"recipient": "eip155:1:0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"asset": "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "0x5F5E100"
},
{
"recipient": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: historically we have recommended using the solana genesis hash as the "chain id". As is this would not allow for disambiguation between mainnet and devnet for example

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know this exists but yes

"asset": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501",
"amount": "0x6F05B59D3B20000"
}
],
"expiry": 1709593200
}
```

#### Response
```typescript
type PayResult = {
orderId: string;
txid: string;
recipient: string;
asset: string;
amount: Hex;
}
```

The wallet's response MUST include:
- `orderId` that matches the original request
- `txid` with the transaction identifier on the blockchain
- `recipient` that received the payment. It **MUST** be a valid [CAIP-10][] account ID.
- `asset` that was used for payment. It **MUST** follow the [CAIP-19][] standard.
- `amount` that was paid. It **MUST** be represented in hex string

`txid` **MUST** be a valid transaction identifier on the blockchain network specified in the asset's chain ID.

`recipient`, `asset`, and `amount` **MUST** match those specified in the selected direct payment option in the `acceptedPayments` array.


Example response:
```json
{
"orderId": "order-123456",
"txid": "0x8a8c3e0b1b812182db4cabd81c9d6de78e549fa3bf3d505d6e1a2b25a15789ed",
"recipient": "eip155:1:0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"asset": "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "0x5F5E100"
}
```

#### Idempotency
The `wallet_pay` method **MUST** be idempotent for the same `orderId`. This ensures robustness in case of connection failures or timeout scenarios.

Requirements:
- If a payment with the same `orderId` has already been completed successfully, the wallet **MUST** return the original `PayResult` without executing a new payment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add more details as to the unique constraints for the orderId? is this per merchant app? Many chains don't have the ability to embed orderIds onchain so this information is likely held offchain.

On Solana, it's common to use the memo + reference key to uniquely identify the transaction as this data is onchain

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orderId doesn't have to be stored onchain. It should be unique for each order. Merchant generated UUID should work just fine

- If a payment with the same `orderId` is currently pending, the wallet **SHOULD** return the result of the original payment attempt
Copy link
Copy Markdown

@jakubuid jakubuid Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify. What's the result of the original payment attempt? If the payment is currently pending the result is not yet available

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the wallet returned txid and you submit the payment with the same orderId, the same txid will be returned. The result doesn't specify the tx state (pending/success)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With that in mind, it would likely be useful to the site to be able to reliably use this. Otherwise it could potentially lead to a UX divergence where a wallet that doesn't support this re-prompts for a new payment where as a wallet that does respond automatically with the TxID without a user interaction.

Therefore, I think we should upgrade this to a MUST rather than a SHOULD. Is there some reason you went with SHOULD here instead?

- If a payment with the same `orderId` has failed previously, the wallet **MAY** attempt the payment again or return the previous error
- Wallets **SHOULD** maintain payment status for completed transactions for at least 24 hours after completion
- If connection is lost during payment execution, dapps **MAY** retry the same request to query the payment status

#### Error Handling
If the payment process fails, the wallet **MUST** return an appropriate error message:
```typescript
type PayError = {
code: number;
message: string;
data?: any;
}
```
The wallet **MUST** use one of the following error codes when the pay request fails:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might it be more future-proof to require an error message 800X or 80XX, or is the idea specifically that these are the only 4 valid errors to throw?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this version, there are. If there are other errors, or different use cases someone can write a new CAIP


- When user rejects the payment
- code = 8001
- message = "User rejected payment"
- When no matching assets are available in user's wallet
- code = 8002
- message = "No matching assets available"
- When the payment request has expired
- code = 8003
- message = "Payment request expired"
- When there are insufficient funds for the payment
- code = 8004
- message = "Insufficient funds"


If a wallet does not support the `wallet_pay` method, it **MUST** return an appropriate JSON-RPC error with code -32601 (Method not found).

Example error response:
```json
{
"id": 1,
"jsonrpc": "2.0",
"error": {
"code": 8001,
"message": "User rejected payment"
}
}
```

## Rationale
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should something about supporting Smart Contract payment flavors like Coinbase Commerce be added here?

Also should the be a discussion somewhere in the CAIP on how developers can reconcile payments? What happens when the wallet can send the transaction but then the internet cuts off and the user can no longer switch back to the dapp?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually removed smart contract interactions. It adds too much complexity for not a lot of benefits.

Currently the only reason its used is to identify payments, which can be done differently in a more efficient way. Also some implementations still use contracts with direct deposit into them


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.

Existing cryptocurrency payment flows typically require users to:
- Select a token type
- Choose a blockchain network
- Wait for address/QR code generation
- Complete the transfer manually

This multi-step process creates excessive friction, often requiring 4-6 user interactions for a simple payment.

The `wallet_pay` method addresses these limitations by:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consideration: EIP-681 and Solana Pay are similar but chain specific. In order to remove chain selection on the PSP level it has to be chain agnostic by design

- 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
- All payment options are transmitted in one request, eliminating the need for multiple user interactions
- The response includes transaction ID and execution details, providing immediate confirmation
- Can be batched with connection establishment, enabling "connect + pay" flows in protocols like WalletConnect

### Alternative Approaches Considered

An intermediate solution involved encoding multiple payment addresses in a single QR code, allowing merchants to present all payment options simultaneously. However, this approach proved impractical for dapp implementations because:

- PSPs cannot determine which payment option was selected
- Monitoring requires polling up to 20+ addresses simultaneously
- No confirmation mechanism exists for payment completion

## Test Cases
<!--Please add diverse test cases here if applicable. Any normative definition of an interface requires test cases to be implementable. -->
TODO
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend adding both positive and negative cases here, it never hurts! it might also be a good place to stick end-to-end examples showing how it can be combined with x402, caip-25, wallet-standard, etc (i.e. end-to-end examples showing other layers, not just payment requests).

## Security Considerations
<!--Please add an explicit list of intra-actor assumptions and known risk factors if applicable. Any normative definition of an interface requires these to be implementable; assumptions and risks should be at both individual interaction/use-case scale and systemically, should the interface specified gain ecosystem-namespace adoption. -->
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, should provide a verifiable source of payment request i.e dapp domain

TODO
Comment thread
lukaisailovic marked this conversation as resolved.
Outdated

`wallet_pay` does not try to address various cases of merchant fraud that end-users are exposed to today. Specifically it does not try to tackle merchant fraud insurance in case the sold good is not delivered. It also does not attempt to provide dispute functionality. These present ideas for future work.
## Privacy Considerations
<!--Please add an explicit list of intra-actor assumptions and known risk factors if applicable. Any normative definition of an interface requires these to be implementable; assumptions and risks should be at both individual interaction/use-case scale and systemically, should the interface specified gain ecosystem-namespace adoption. -->
TODO
## Backwards Compatibility
<!--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.-->
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.

## References
<!--Links to external resources that help understanding the CAIP better. This can e.g. be links to existing implementations. See CONTRIBUTING.md#style-guide . -->

- [CAIP-1] defines the CAIP document structure
- [EIP-681] is ethereum-specific prior art that also includes gas information in the URI

[CAIP-1]: https://ChainAgnostic.org/CAIPs/caip-1
[EIP-681]: https://eips.ethereum.org/EIPS/eip-681

## Copyright
Copyright and related rights waived via [CC0](../LICENSE).