---
# System prepended metadata

title: Contactless Crypto Payment Standard

---

---
eip: ____
title: "Contactless Crypto Payment Standard"
description: Standard for initializing contactless payment transactions from EVM wallets
author: Amhed Herrera, Justin Lee, Arjun Dureja
discussions-to: 
status: Draft
type: Standards Track
category: ERC
created: 2024-10-25
requires: 20, 681, 712
---

## Abstract
This ERC defines a standard for contactless payment transactions that can allow for interoperable customer to merchant onchain payments, regardless of the wallets the customer and merchant are using.

## Motivation
Currently there is no standard mechanism in crypto to do contactless payment transactions via NFC. This ERC defines a standardized way of exchanging payment information between merchants and customers, or peer-to-peer individuals, so that efficient checkout can occur.

## Specification

Comprised of three parts:
- A new web3Provider method called `requestContactlessPayment`
- An agreement on the payload for data exchange between the parties
- An optional mechanism for relaying large JSON payloads using a backend relayer


### Use Cases

#### Use Case 1: Customer purchases from a merchant
1. A customer approaches a merchant and orders an item at their cash register (e.g. a cup of coffee).
1. The merchant is running an app on their point-of-sale (PoS) system that allows initiating a checkout flow. 
1. When the customer is done ordering, the merchant goes through the checkout flow up and triggers a payment request. 
1. The merchant app calls the `requestContactlessPayment` RPC method with the expected payload. 
1. The merchant's device emits an NFC signal via host card emulation ([HCE](https://developer.android.com/develop/connectivity/nfc/hce)) for the customer’s device to read.
1. The customer, running their wallet software of choice, reads the NFC signal. The customer’s wallet constructs the necessary transaction(s) to execute the onchain checkout. 
1. The wallet then signs and submits all transactions.
1. Upon successful onchain execution of these transactions, the checkout is complete.

#### Use Case 2: Peer-to-peer Sends
1. Alice requests a payment on her wallet app by emitting an NFC signal. The contents of the signal is an EIP-681 URI
> []will this only work with 681? 681 has limitations, like receiver intent which would be great to solve too.

EIP-681 URI1. Bob reads the NFC signal from Alice on his wallet app and constructs the transaction
2. Bob holds his phone close to Alice's which triggers the NFC flow.
1. Bob then signs and submits the transaction onchain.
1. Send flow is complete.

### requestContactlessPayment
The requestContactlessPayment method will be added as a standard method to the [EIP-1193 Ethereum Provider JavaScript API](https://eips.ethereum.org/EIPS/eip-1193). 

This method takes in two parameters: `type` and `uri`.

- `type` is an enum that represents the payload type:
    - 1 means the URI is an EIP-681-compliant payload.
    - 2 means the URI points to an HTTP relayer, where the response of the relayer is either a JSON object with the transaction data already encoded as a 0x string, or a message for the customer to sign. The customer wallet is responsible for constructing, signing and submitting the transaction/message.

- `uri` represents either the EIP-681-compliant payload or the relayer endpoint to query for the JSON payload. 

Example calls:

```js 
window.ethereum.request({ 
  method: 'requestContactlessPayment', 
  params: { 
    type: 1,
    uri: "ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0xf7d07Ee99095FDC09001710Ec232162a788BB989&uint256=1e5",
});

window.ethereum.request({ 
  method: 'requestContactlessPayment', 
  params: { 
    type: 2,
    uri: "https://nfcrelay.xyz/paymentTxParams?uuid=1234-abcd-56678",
    verificationCode: "1234567890"
}
});
```

### Type 1

The transaction is a regular crypto send. The receiving party transmits the EIP-681 URI to the sender’s wallet. 

Example code using an EIP-681 payload:

```ts
import { ethers } from 'ethers';

const provider = new   ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_KEY");

function handleRequestContactlessPaymentEIP681URI(url: string) {
  // Parse the EIP-681 URL 
  const params = url.split('@')[1].split('?')[-1].split('&');
  const to = params[0].split('=')[1];
  const value = parseInt(params[1].split('=')[1]);
  const chainId = url.split('@')[0]; 

  // Get wallet and connect to provider
  const wallet = new ethers.Wallet(privateKey, provider);

  // Build and sign transactions
  const tx = {
    to: to,
    value: ethers.utils.parseEther(value.toString()),
    gasLimit: 21000,
    gasPrice: ethers.utils.parseUnits('50', 'gwei')  
  };

  const signedTx = await wallet.signTransaction(tx);

  // Send transaction
  return await provider.sendTransaction({ signedTransaction: signedTx, chainId });
}
```

### Type 2

Handles cases where the payload necessary to construct, sign, and submit the transaction needed for payment is too large to emit over NFC. The intended flow is as follows: 

1. The merchant dapp makes a POST request to an NFC relayer endpoint (See [example implementation](https://github.com/base-org/nfc-relayer)) with the information needed for completing a sale. 
1. The merchant’s device then transmits the NFC relayer endpoint URI and verification code to the customer via any means
1. The customer’s device then makes a GET request to the endpoint URI

One of three payloads can be returned from the NFC relayer:

**Regular Send**
```ts
{
  payloadType: 'eip681';
  chainId: string;
  contractAddress: string;
  toAddress: string;
  value: string;
  dappUrl?: string;
  dappName?: string;
  rpcProxySubmissionParams?: {
    submissionUrl?: string; 
  }
}
```

**Contract Call**

```ts
{
  payloadType: 'contractCall';
  chainId: string;
  approveTxs?: {
    data: string; // this represents the approval call data
    toAddress: string; // the address to submit the approve transaction to 
  }[],
  paymentTx: {
    data: string; // call data of the transaction, only needed for 
    toAddress: string; // the address to submit the payment transaction to
    value: string; // the value of the blockchain's native token to transfer over  
  }, 
  rpcProxySubmissionParams?: {
    submissionUrl?: string; // optional endpoint to submit the tx hash to
  },
  dappUrl?: string;
  dappName?: string;
}
```

**[EIP-712 Message](https://eips.ethereum.org/EIPS/eip-712) (offline signature)**

```ts
{
  payloadType: 'eip712';
  chainId: string,
  rpcProxySubmissionParams: {
    submissionUrl: string; // endpoint to submit tx message + signature to 
    typedData: {
      types: {
        // any types can go here in accordance with EIP-712 and eth_signtypeddata_v4, it's recommended that every type needed for
        // generating the right transactions on the RPC proxy is here
      },
      primaryType?: string;
      domain: {
        // any domain fields go here, usually need name, version, chainId and verifyingContract
      },
      message: {
        // any fields can go here, it's recommended that every field needed for
        // generating the right transactions on the RPC proxy is here
      }
    },
  },
  dappUrl?: string;
  dappName?: string;
  additionalPayload?: {
    // ... any fields can go here, this is additional payload the RPC proxy may need but does not have to bee signed in the message
  }
}
```
Upon submitting the transaction for the EIP-681 and contract call cases, if `rpcProxySubmissionParams` is present, then the transaction hash can be optionally submitted via a POST request to the submission URL. The body of the POST would have the following structure:
```ts
{
  txHash: string; 
}
```

For EIP-712 messages, the signature is to be submitted alongside the typed data message to the provided `submissionUrl`. The body of the POST request would look something like this:


```ts
{
   typedData: {
    types?: {
      // define types here
    },
    primaryType?: string;
    domain?: {
      name?: string;
      version?: string;
      chainId?: string;
      verifyingContract?: string;
    },
    message?: {
      // define fields of the message needed to be signed here
    },
  },
  signature: string; // EIP-712 signature should be here
  additionalPayload?: {
    // ... any fields can go here, this is additional payload the RPC proxy may need but does not have to be signed in the message
  }
}
```

The RPC proxy URL is then expected to return the tx hash in a 200 response for both cases.

#### Example code for this scenario

##### EIP-681

```ts
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(INSERT_PRIVATE_KEY_HERE, INSERT_PROVIDER_HERE);

function handleRequestContactlessPaymentTxDataPayload(uri: string) {
  const relayedData = await fetch(uri).json();

  if (relayedData.payloadType !== 'eip681') return;

  const isERC20Transfer = !!relayedData.contractAddress;

  const paymentTx = {
    to: relayedData.toAddress,
    value: isERC20Transfer ? 0 : relayedData.value,
    data: isERC20Transfer ? encodeERC20Transfer(relayedData.toAddress, relayedData.value) : undefined, // pseudocode for actually encoding an ERC20 transfer here
    gasLimit: 21000,
    gasPrice: ethers.utils.parseUnits('50', 'gwei')  
  };

  // insert code here to show the transactions to the user
  // and allow them to sign and submit them 

  const txHash = await wallet.signTransaction(paymentTx);
  return fetch(relayerData.rpcProxySubmissionParams.submissionUrl, { 
    method: 'POST',
    'Content-Type': 'application/json',
    body: JSON.stringify({ txHash });
  });
}
```

##### Approve + payment transactions

```ts
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(INSERT_PRIVATE_KEY_HERE, INSERT_PROVIDER_HERE);

function handleRequestContactlessPaymentTxDataPayload(uri: string) {
  const relayedData = await fetch(uri).json();
  
  if (relayedData.payloadType !== 'contractCall') return;
  
  const approveTxs = relayedData.approveTxs.map((approveTx) => {
    return {
      to: approveTx.toAddress,
      data: approveTx.data
    };
  });
  const paymentTx = {
    to: relayedData.paymentTx.toAddress,
    data: relayedData.paymentTx.data,
    value: relayedData.paymentTx.value,
    gasLimit: 21000,
    gasPrice: ethers.utils.parseUnits('50', 'gwei')  
  };

  // insert code here to show the transactions to the user
  // and allow them to sign and submit them 

  const signedApproveTxs = await Promise.all(approveTxs.map((approveTx) => {
    return wallet.signTransaction(approveTx); 
  }));
  const signedTx = await wallet.signTransaction(paymentTx);
  return fetch(relayerData.rpcProxySubmissionParams.submissionUrl, { 
    method: 'POST',
    'Content-Type': 'application/json',
    body: JSON.stringify({ txHash: signedTx });
  });
}
```

##### EIP-712
```ts
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(INSERT_PRIVATE_KEY_HERE, INSERT_PROVIDER_HERE);

function handleRequestContactlessPaymentTxMessagePayload(uri: string) {
  const relayedData = await fetch(uri).json();

  if (relayedData.payloadType !== 'eip712') return;
  
  const message = relayedData.message;

  // insert code here to show the message to the user
  // and allow them to sign it

  const signature = await wallet._signTypedData(message.domain, message.types, message.message);

  return fetch(relayerData.rpcProxySubmissionParams.submissionUrl, { 
    method: 'POST',
    'Content-Type': 'application/json',
    body: JSON.stringify({ message: relayedData.message, signature, additionalPayload: relayedData.additionalPayload });
  });
}
```


## Rationale

The reason for having a relayer URI to pass data is due to limitations in the size of the data that can be transmitted wirelessly between devices directly. For example, QR codes have a maximum character size of 7,089 characters currently, but a user can attempt to buy an indefinite number of items in a single checkout transaction. By having a relayer URI, we can theoretically pass as much calldata as we desire to the customer’s wallet, as long as they fetch from the URI.

EIP-681 URIs were also permitted to enable possible direct peer to peer payments. It’s more likely that they’ll be occurring between two end user wallets rather than between a customer and a merchant, but it would be good to include in the event the dapp only requires a simple send transaction to execute the checkout.

## Security Considerations
In the case of an EIP-681 URI, the same security considerations apply here. The wallet should display the amount and asset being transferred as well as the recipient very clearly to the customer, and users should only execute transactions using EIP-681 URIs from trusted dapps.

With regards to the relayer URI, since it is a publicly facing URI, it’s possible that anyone can make a fetch to it, grab the data, and submit transactions of their own to execute a checkout on someone else’s behalf. To mitigate this, we made a verification code as an optional part of the standard. The verification code is only passed through via the RPC call and is expected to be passed onto the customer’s wallet via some sort of contactless communication mechanism. This way, the data is not accessible to a random person on the internet and can only be accessed from the intended customer’s wallet. 

