Non-Sponsored calls, also known as Sync Fee Calls, are the simplest way to pay for relay services. However, they delegate all security (reentrancy/replay protection etc.) and payment logic to the target smart contract. You can use ERC-2771 to achieve out-of-the-box security and authentication. Relay costs are covered in either native or ERC-20 tokens and they are paid synchronously during the relay call.

For ERC-20 tokens that implement the permit function (EIP-2612), you can enable gasless transactions by allowing users to authorize token spending through off-chain signatures instead of requiring an on-chain approval transaction.

Implementation Steps

1. Deploy a GelatoRelayContext compatible contract

Import GelatoRelayContext in your target contract to inherit callWithSyncFee functionalities:

import {
    GelatoRelayContext
} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol";

contract TargetContract is GelatoRelayContext {
    function example() external onlyGelatoRelay {
        // _yourAuthenticationLogic()

        // Payment to Gelato
        // NOTE: be very careful here!
        // if you do not use the onlyGelatoRelay modifier,
        // anyone could encode themselves as the fee collector
        // in the low-level data and drain tokens from this contract.
        _transferRelayFee();
    }

    function exampleFeeCapped(uint256 maxFee) external onlyGelatoRelay {
        // Remember to authenticate your call since you are not using ERC-2771
        // _yourAuthenticationLogic()

        // Payment to Gelato
        // NOTE: be very careful here!
        // if you do not use the onlyGelatoRelay modifier,
        // anyone could encode themselves as the fee collector
        // in the low-level data and drain tokens from this contract.
        _transferRelayFeeCapped(maxFee);
    }
}

Import GelatoRelayContextERC2771 in your target contract to inherit ERC2771 functionalities with callWithSyncFee:

import {
    GelatoRelayContextERC2771
} from "@gelatonetwork/relay-context/contracts/GelatoRelayContextERC2771.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TargetContractRelayContextERC2771 is GelatoRelayContextERC2771 {
    mapping (address=>bool) public caller;

    function increment() external onlyGelatoRelayERC2771 {
        // Payment to Gelato
        // NOTE: be very careful here!
        // if you do not use the onlyGelatoRelayERC2771 modifier,
        // anyone could encode themselves as the fee collector
        // in the low-level data and drain tokens from this contract.
        _transferRelayFee();
        // _getMsgSender() will fetch the original user who signed the relay request.
        caller[_getMsgSender()] = true;
    }
    
    function incrementFeeCapped(uint256 maxFee) external onlyGelatoRelayERC2771 {
        // Payment to Gelato
        // NOTE: be very careful here!
        // if you do not use the onlyGelatoRelayERC2771 modifier,
        // anyone could encode themselves as the fee collector
        // in the low-level data and drain tokens from this contract.
        _transferRelayFeeCapped(maxFee);
        // _getMsgSender() will fetch the original user who signed the relay request.
        caller[_getMsgSender()] = true;
    }

    // Enhanced functions with ERC-20 permit support for gasless payments
    function incrementWithPermit(
        address token,
        uint256 amount,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external onlyGelatoRelayERC2771 {
        address user = _getMsgSender();
        
        // Execute permit to allow contract to spend user's tokens
        // This eliminates the need for a separate approval transaction
        IERC20Permit(token).permit(user, address(this), amount, deadline, v, r, s);
        
        // Your business logic here
        IERC20(token).transferFrom(user, address(this), amount);
        
        // Payment to Gelato
        // NOTE: be very careful here!
        // if you do not use the onlyGelatoRelayERC2771 modifier,
        // anyone could encode themselves as the fee collector
        // in the low-level data and drain tokens from this contract.
        _transferRelayFee();
        // _getMsgSender() will fetch the original user who signed the relay request.
        caller[user] = true;
    }

    function incrementWithPermitFeeCapped(
        address token,
        uint256 amount,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s,
        uint256 maxFee
    ) external onlyGelatoRelayERC2771 {
        address user = _getMsgSender();
        
        // Execute permit to allow contract to spend user's tokens
        IERC20Permit(token).permit(user, address(this), amount, deadline, v, r, s);
        
        // Your business logic here
        IERC20(token).transferFrom(user, address(this), amount);
        
        // Payment to Gelato
        // NOTE: be very careful here!
        // if you do not use the onlyGelatoRelayERC2771 modifier,
        // anyone could encode themselves as the fee collector
        // in the low-level data and drain tokens from this contract.
        _transferRelayFeeCapped(maxFee);
        // _getMsgSender() will fetch the original user who signed the relay request.
        caller[user] = true;
    }
}

2. Import GelatoRelaySDK into your front-end .js project

import { GelatoRelay, CallWithSyncFeeRequest } from "@gelatonetwork/relay-sdk";
const relay = new GelatoRelay();

Or if you’re using the Viem library:

import { GelatoRelay, CallWithSyncFeeRequest } from "@gelatonetwork/relay-sdk-viem";
const relay = new GelatoRelay();

For ERC2771 Sync Fee Calls

import { GelatoRelay, CallWithSyncFeeERC2771Request } from "@gelatonetwork/relay-sdk-viem";

When using ERC2771 methods, initialize GelatoRelay with the appropriate trustedForwarder. The possible configurations are:

contract: {
    relay1BalanceERC2771: "trustedForwarder for method sponsoredCallERC2771",
    relayERC2771: "trustedForwarder for method callWithSyncFeeERC2771",
    relay1BalanceConcurrentERC2771: "trustedForwarder for method concurrent sponsoredCallERC2771",
    relayConcurrentERC2771: "trustedForwarder for method concurrent callWithSyncFeeERC2771",
    relay1BalanceConcurrentERC2771zkSync: "trustedForwarder for method concurrent sponsoredCallERC2771 on zkSync",
    relay1BalanceERC2771zkSync: "trustedForwarder for method sponsoredCallERC2771 on zkSync",
    relayConcurrentERC2771zkSync: "trustedForwarder for method concurrent callWithSyncFeeERC2771 on zkSync",
    relayERC2771zkSync: "trustedForwarder for method callWithSyncFeeERC2771 on zkSync",
}

Check the Supported Networks and contract addresses to identify the trustedForwarder associated with your method.

Example for Sepolia using callWithSyncFeeERC2771:

const relay = new GelatoRelay({
    contract: {
        relayERC2771: "0xb539068872230f20456CF38EC52EF2f91AF4AE49"
    }
});

3. Generate a payload for your target contract

// Set up target address and function signature abi
const targetContractAddress = "<your target contract address>"; 
const abi = [
    "function example()", 
    "function exampleFeeCapped(uint256)",
    "function increment()",
    "function incrementFeeCapped(uint256)",
    "function incrementWithPermit(address,uint256,uint256,uint8,bytes32,bytes32)",
    "function incrementWithPermitFeeCapped(address,uint256,uint256,uint8,bytes32,bytes32,uint256)"
];

const [address] = await window.ethereum!.request({
    method: "eth_requestAccounts",
});

// Generate payload using front-end provider such as MetaMask
const client = createWalletClient({
    account: address,
    chain,
    transport: custom(window.ethereum!),
});

// Address of the token to pay fees
const feeToken = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

const chainId = await client.getChainId();

// Helper function to check if token supports permit
async function checkPermitSupport(tokenContract) {
    try {
        const hasPermit = await tokenContract.permit.staticCall(
            ethers.ZeroAddress, ethers.ZeroAddress, 0, 0, 0, "0x00", "0x00"
        ).catch(() => false);
        return hasPermit !== false;
    } catch {
        return false;
    }
}

// Helper function to generate permit signature
async function signPermit(signer, token, amount, spender, deadline, chainId) {
    const domain = {
        name: await token.name(),
        version: "1",
        chainId: chainId,
        verifyingContract: token.target
    };
    
    const types = {
        Permit: [
            { name: "owner", type: "address" },
            { name: "spender", type: "address" },
            { name: "value", type: "uint256" },
            { name: "nonce", type: "uint256" },
            { name: "deadline", type: "uint256" }
        ]
    };
    
    const nonce = await token.nonces(signer.address);
    
    const values = {
        owner: signer.address,
        spender: spender,
        value: amount,
        nonce: nonce,
        deadline: deadline
    };
    
    const signature = await signer.signTypedData(domain, types, values);
    return ethers.Signature.from(signature);
}

// Encode function data
const data = encodeFunctionData({
    abi: abi,
    functionName: "example",
});

// -----------------------------------------------------------------
// Alternative example using Gelato Fee Oracle
// Retrieve the estimate fee from the Gelato 
const fee = await relay.getEstimatedFee(
    BigInt(chainId),
    feeToken,
    gasLimit,
    false,
);

const maxFee = fee * 2 // You can use 2x or 3x to set your maxFee

// Encode function data
const dataMaxFee = encodeFunctionData({
    abi: abi,
    functionName: "exampleFeeCapped",
    args: [maxFee]
});

// -----------------------------------------------------------------
// For ERC-20 tokens with permit support (enhanced user experience)
const tokenContract = new ethers.Contract(tokenAddress, tokenABI, provider);
const supportsPermit = await checkPermitSupport(tokenContract);

if (supportsPermit) {
    const signer = await provider.getSigner();
    const amount = ethers.parseUnits("100", 6); // 100 USDC
    const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
    
    // Generate permit signature
    const sig = await signPermit(
        signer,
        tokenContract,
        amount,
        targetContractAddress,
        deadline,
        chainId
    );
    
    const { v, r, s } = sig;
    
    // Encode function data with permit
    const dataWithPermit = encodeFunctionData({
        abi: abi,
        functionName: "incrementWithPermit",
        args: [tokenAddress, amount, deadline, v, r, s]
    });
    
    // With fee cap using permit
    const dataWithPermitFeeCapped = encodeFunctionData({
        abi: abi,
        functionName: "incrementWithPermitFeeCapped",
        args: [tokenAddress, amount, deadline, v, r, s, maxFee]
    });
}

4. Send payload to Gelato

// Populate the relay SDK request body
const request: CallWithSyncFeeRequest = {
    chainId: BigInt(chainId),
    target: targetContractAddress,
    data: data,
    feeToken: feeToken,
    isRelayContext: true,
};

// Send relayRequest to Gelato Relay API
const relayResponse = await relay.callWithSyncFee(request);

// -----------------------------------------------------------------
// Alternative example using Gelato Fee Oracle
const requestMaxFee: CallWithSyncFeeRequest = {
    chainId: BigInt(chainId),
    target: targetContractAddress,
    data: dataMaxFee,
    feeToken: feeToken,
    isRelayContext: true,
};

// Send relayRequest to Gelato Relay API
const relayResponse = await relay.callWithSyncFee(requestMaxFee);

For ERC2771 Sync Fee Calls

// Populate the relay SDK request body
const request: CallWithSyncFeeERC2771Request = {
    chainId: BigInt(chainId),
    target: targetContractAddress,
    data: data,
    user: address,
    feeToken: feeToken,
    isRelayContext: true,
};

// Send relayRequest to Gelato Relay API
const relayResponse = await relay.callWithSyncFeeERC2771(request, client as any);

// -----------------------------------------------------------------
// Alternative example using Gelato Fee Oracle
const requestMaxFee: CallWithSyncFeeERC2771Request = {
    chainId: BigInt(chainId),
    target: targetContractAddress,
    data: dataMaxFee,
    user: address,
    feeToken: feeToken,
    isRelayContext: true,
};

// Send relayRequest to Gelato Relay API
const relayResponseMaxFee = await relay.callWithSyncFeeERC2771(requestMaxFee, client as any);

// -----------------------------------------------------------------
// Example with permit support for enhanced UX
if (supportsPermit) {
    // Use permit-enabled functions for gasless experience
    const requestWithPermit: CallWithSyncFeeERC2771Request = {
        chainId: BigInt(chainId),
        target: targetContractAddress,
        data: dataWithPermit,
        user: signer.address,
        feeToken: tokenAddress,
        isRelayContext: true,
    };
    
    const relayResponseWithPermit = await relay.callWithSyncFeeERC2771(requestWithPermit, signer);
    
    // With fee cap
    const requestWithPermitFeeCapped: CallWithSyncFeeERC2771Request = {
        chainId: BigInt(chainId),
        target: targetContractAddress,
        data: dataWithPermitFeeCapped,
        user: signer.address,
        feeToken: tokenAddress,
        isRelayContext: true,
    };
    
    const relayResponseWithPermitFeeCapped = await relay.callWithSyncFeeERC2771(requestWithPermitFeeCapped, signer);
} else {
    // Fallback to standard implementation requiring prior approval
    console.warn("Token doesn't support permit. User will need to approve tokens first.");
    // Use standard increment() or incrementFeeCapped() functions shown above
}

Learn more about Implementation of Non ERC2771 SyncFee Calls and ERC2771 SyncFee Calls in our documentation.

Important Considerations for ERC-20 Permit

When implementing permit functionality, keep these points in mind:

  • Token Compatibility: Not all ERC-20 tokens support permit - always check compatibility first
  • Single Transaction Flow: Users can approve and execute in one transaction when permit is supported
  • Signature Expiration: Permit signatures have deadlines - ensure adequate time for transaction execution
  • Nonce Mechanism: Each permit signature can only be used once due to the nonce mechanism
  • Security: Always validate permit parameters in your smart contract to prevent replay attacks