Skip to main content

How does it work?

Gasless transactions on EVM are regular transactions where the transaction signer and the gas payer are two different addresses. Rather than signing with the same address and then submitting their transaction and paying gas, a user can simply sign a transaction payload, and other entities (relayers, solvers, etc.) can submit the transaction on their behalf and sponsor the gas costs. This is not natively possible, so there are custom transaction payloads to enable this: Permit2, EIP-3009, ERC-2771 payloads, ERC-4337 user operations, etc. The core concept is that as long as the transaction execution does not rely on tx.origin (the transaction submitter), any user can sign permit payloads or Smart Account actions (EIP-7702, ERC-4337 user operations etc.) and a relayer can post those payloads on chain by wrapping them in a transaction. This is where the /execute API comes in. The API allows users to submit raw calls (the partial payload of a regular EVM transaction). It includes the to, data, value and an optional EIP-7702 authorizationList.

How to use it?

This feature is only currently available to Enterprise Partners, learn more about the Enterprise Partner Program to get started. Once you’re an Enterprise Partner you should have an API key and a linked funding address. You’ll then need to fund your app balance, you can learn more about that here.

Executing a gasless quote

In this example use case you want to execute a gasless Relay quote with the execute API.
1

Get a quote

This flow assumes that the user is using a smart wallet. Use the quote endpoint to get a quote for the transaction you want to execute. The originGasOverhead indicates how much additional gas overhead will be necessary to include the user operation on the chain as compared to a normal EOA transaction.
 curl -X POST "https://api.relay.link/quote/v2" \
 -H "Content-Type: application/json" \
 -d '{
   "user": "YOUR_WALLET_ADDRESS",
   "originChainId": 8453,
   "destinationChainId": 42161,
   "originCurrency": "0x0000000000000000000000000000000000000000",
   "destinationCurrency": "0x0000000000000000000000000000000000000000",
   "amount": "100000000000000",
   "tradeType": "EXACT_INPUT",
   "originGasOverhead": "300000" //This is the overhead gas cost of the smart wallet, this will be used when simulating the transaction
 }'
2

Execute the transaction

With the quote in hand, you can now execute the transaction. You’ll need to pass the data from previous steps to the api.
  const options = {
    method: 'POST',
    headers: {'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},
    body: JSON.stringify({
      requestId: steps[0].requestId, //Quote requestId from previous step
      executionKind: 'rawCalls',
      data: {
        chainId: 8453,
        to: 'YOUR_WALLET_ADDRESS',
        data: steps[0].items[0].data.data, //Quote data from previous step
        value: steps[0].items[0].data.value, //Quote value from previous step
      },
      executionOptions: {
        //Optional execution options
      }
    })
  };

  fetch('https://api.relay.link/execute', options)
    .then(res => res.json())
    .then(res => console.log(res))
    .catch(err => console.error(err));
3

Monitor

Now that the transaction has been submitted, you can monitor the status of the transaction using the requestId from the quote data and the status endpoint.
  const response = await fetch(`https://api.relay.link/intents/status/v3?requestId=${requestId}`);
  const data = await response.json();
  console.log(data);
4
Refer to the quickstart guide for more information on how to monitor the status of a transaction.

More Use Cases

If the user has a Smart Account (such as an ERC-4337 Smart Account), any transaction can be made gasless for the Smart Account by signing an ERC-4337 user operation and submitting it to the /execute API.The only step is to create the user operation, get it signed by the user and then create the final call data of the handleOps call of the Entry Point before submitting to the /execute endpoint.Note: The maxFeePerGas and maxPriorityFeePerGas The ERC-4337 user operation should be 0; it will result in a double payment to the solver (The solver already has adjusted the cost in the quote, so no need for this payment)
1

Create the user operation

 import {
   createPublicClient,
   createWalletClient,
   createClient,
   http,
   encodeFunctionData,
   keccak256,
   encodeAbiParameters,
   toHex,
 } from "viem";
 import { base } from "viem/chains";
 import { UserOperation } from "./types.ts";
 import { entryPointAbi, smartAccountAbi } from "./abis.ts";

 // ---- Config ----
 const ENTRY_POINT = "0xEntryPointAddress" as const;
 const YOUR_WALLET_ADDRESS = "0xYourAddress" as const;
 const BENEFICIARY = "0xYourBeneficiary" as const;

 // ---- Clients ----
 const publicClient = createPublicClient({
   chain: base,
   transport: http(),
 });

 const ownerAccount = ... // Replace with your owner account

 const walletClient = createWalletClient({
   account: ownerAccount,
   chain: base,
   transport: http()
 });

 const userOpPackHash = keccak256(packUserOp(userOp));

 const encoded = encodeAbiParameters(
   [
     { type: "bytes32" }, // userOp hash
     { type: "address" }, // entryPoint
     { type: "uint256" }, // chainId
   ],
   [userOpPackHash, ENTRY_POINT, BigInt(base.id)],
 );

 const userOpHash = keccak256(encoded);

 const signature = await walletClient.signMessage({
   message: { raw: userOpHash },
 });

 const signedUserOp: UserOperation = { ...userOp, signature };

 const handleOpsData = encodeFunctionData({
   abi: entryPointAbi,
   functionName: "handleOps",
   args: [[signedUserOp], BENEFICIARY],
 });
2

Send the payload to the /execute endpoint

  const options = {
    method: 'POST',
    headers: {'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},
    body: JSON.stringify({
      executionKind: 'rawCalls',
      data: {
        chainId: 8453,
        to: 'YOUR_WALLET_ADDRESS',
        data: handleOpsData,
        value: "0"
      }
    })
  };

  fetch('https://api.relay.link/execute', options)
    .then(res => res.json())
    .then(res => console.log(res))
    .catch(err => console.error(err));
3

Monitor

Now that the transaction has been submitted, you can monitor the status of the transaction using the requestId returned in the api response and the status endpoint.
  const response = await fetch(`https://api.relay.link/intents/status/v3?requestId=${requestId}`);
  const data = await response.json();
  console.log(data);
Another example of a flow where the transaction execution does not depend on tx.origin is the action of upgrading a regular EOA to an EIP-7702 smart account. In this flow, any entity can execute an upgrade payload and a signed EIP-7702 authorizationList from the EOA.The client must pass the correct fields for the to (which is the EOA that is to be upgraded), data is the call data for the initialization of the EOA’s EIO-7702 Smart Account and the authorizationList which contains the signed information about the Smart Account to upgrade to.
1

Sign the EIP-7702 authorizationList

import {
  createWalletClient,
  http,
  encodeFunctionData,
  hexToSignature,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

// This flow requires access to the EOA private key
const walletClient = createWalletClient({
  account: privateKeyToAccount("0xYourEOAPrivateKey"),
  chain: base,
  transport: http()
});

// Build init calldata (replace ABI + args with your real upgrade initializer)
const initCalldata = encodeFunctionData({
  abi: [{
    type: "function",
    name: "initialize",
    inputs: [
      { name: "owner", type: "address" },
      { name: "config", type: "uint256" }
    ],
    outputs: []
  }],
  functionName: "initialize",
  args: ["0xEOA_TO_UPGRADE", 1n]
});


// --- Sign EIP-7702 Authorization (viem handles everything: r, s, yParity, nonce, chainId) ---
const authorization = await client.signAuthorization({
  account: client.account!,
  contractAddress: "0xSMART_ACCOUNT_CONTRACT" //This is the contract that will have authorization
});
2

Send the payload to the /execute endpoint

  const options = {
    method: 'POST',
    headers: {'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},
    body: JSON.stringify({
      executionKind: 'rawCalls',
      data: {
        chainId: 8453,
        to: '0xEOA_TO_UPGRADE',
        data: initCalldata,
        value: "0",
        authorizationList: [authorization]
      }
    })
  };

  fetch('https://api.relay.link/execute', options)
    .then(res => res.json())
    .then(res => console.log(res))
    .catch(err => console.error(err));
3

Monitor

Now that the transaction has been submitted, you can monitor the status of the transaction using the requestId returned in the api response and the status endpoint.
  const response = await fetch(`https://api.relay.link/intents/status/v3?requestId=${requestId}`);
  const data = await response.json();
  console.log(data);

Caveats

  • Only EVM chains are supported currently.
  • Partial sponsorship is not yet supported. Should the fees amount to more than the app balance, the transaction will not be sponsored.
  • If the transaction relies on msg.sender, the transaction signer must be a smart wallet (EIP-7702, ERC-4337, etc).
  • The API won’t work if the transaction relies on tx.origin.