Skip to main content

Gas Relayer Integration Guide

Learn how to integrate gasless transactions into your application using the Loyalteez Gas Relayer.

What is the Gas Relayer?

The Gas Relayer enables gasless transactions for your users. They can interact with blockchain features (claiming perks, redeeming rewards) without needing cryptocurrency for gas fees. Your brand wallet pays all gas costs.

Benefits:

  • ✅ Zero friction for users - no ETH needed
  • ✅ Web2-like UX - feels like a normal app
  • ✅ Predictable costs - you control gas budget
  • ✅ Higher conversion - no wallet setup barrier

How It Works

Quick Start

1. Authenticate with Privy

import { usePrivy } from '@privy-io/react-auth';

function MyComponent() {
const { getAccessToken } = usePrivy();

// Get token for authentication
const token = await getAccessToken();
}

2. Encode Your Transaction

import { ethers } from 'ethers';

// Define your contract ABI
const abi = ['function claimPerk(uint256 perkId) external'];

// Encode the function call
const iface = new ethers.Interface(abi);
const data = iface.encodeFunctionData('claimPerk', [perkId]);

3. Call the Gas Relayer

const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: contractAddress,
data: data,
userAddress: userWalletAddress,
metadata: {
action: 'claim_perk',
perkId: perkId
}
})
});

const result = await response.json();

if (result.success) {
console.log('Transaction hash:', result.transactionHash);
}

Complete Integration

React Hook

// useGasRelayer.ts
import { usePrivy } from '@privy-io/react-auth';

export function useGasRelayer() {
const { getAccessToken, user } = usePrivy();

const relayTransaction = async (params: {
to: string;
data: string;
value?: string;
gasLimit?: number;
metadata?: any;
}) => {
const token = await getAccessToken();

const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...params,
userAddress: user?.wallet?.address
})
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Transaction failed');
}

return await response.json();
};

return { relayTransaction };
}

Usage Example

function ClaimButton({ perkId }: { perkId: number }) {
const { relayTransaction } = useGasRelayer();
const [claiming, setClaiming] = useState(false);

const handleClaim = async () => {
setClaiming(true);
try {
// Encode transaction
const iface = new ethers.Interface([
'function claimPerk(uint256 perkId) external'
]);
const data = iface.encodeFunctionData('claimPerk', [perkId]);

// Relay transaction
const result = await relayTransaction({
to: PERK_NFT_ADDRESS,
data: data,
metadata: { action: 'claim_perk', perkId }
});

alert(`Success! Tx: ${result.transactionHash}`);
} catch (error) {
console.error('Claim failed:', error);
alert('Failed to claim perk');
} finally {
setClaiming(false);
}
};

return (
<button onClick={handleClaim} disabled={claiming}>
{claiming ? 'Claiming...' : 'Claim Perk (No Gas!)'}
</button>
);
}

EIP-2612 Permit Integration

Enable gasless approvals using EIP-2612 permit signatures:

Sign Permit

async function signPermit(params: {
token: string;
spender: string;
value: string;
deadline: number;
}) {
const domain = {
name: 'Loyalteez',
version: '1',
chainId: 1868,
verifyingContract: params.token
};

const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
};

const message = {
owner: userAddress,
spender: params.spender,
value: params.value,
nonce: await getNonce(userAddress),
deadline: params.deadline
};

const signature = await signer.signTypedData(domain, types, message);
const sig = ethers.Signature.from(signature);

return {
owner: userAddress,
spender: params.spender,
value: params.value,
deadline: params.deadline,
v: sig.v,
r: sig.r,
s: sig.s
};
}

Use Permit with Gas Relayer

async function claimWithPermit(perkId: number, cost: string) {
// 1. Sign permit (gasless approval)
const permit = await signPermit({
token: LTZ_TOKEN_ADDRESS,
spender: PERK_NFT_ADDRESS,
value: ethers.parseUnits(cost, 18).toString(),
deadline: Math.floor(Date.now() / 1000) + 3600
});

// 2. Encode claim function
const iface = new ethers.Interface([
'function claimPerk(uint256 perkId) external'
]);
const data = iface.encodeFunctionData('claimPerk', [perkId]);

// 3. Relay with permit
const result = await relayTransaction({
to: PERK_NFT_ADDRESS,
data: data,
metadata: {
action: 'claim_perk',
perkId: perkId,
permit: permit // Gas relayer handles permit execution
}
});

return result;
}

Error Handling

Handle All Error Codes

async function relayWithErrorHandling(params: any) {
try {
return await relayTransaction(params);
} catch (error: any) {
switch (error.code) {
case 'RATE_LIMIT_EXCEEDED':
alert('Too many transactions. Please wait a few minutes.');
break;
case 'VALIDATION_FAILED':
alert('Transaction validation failed. Please check your inputs.');
break;
case 'CONTRACT_NOT_WHITELISTED':
alert('This contract is not authorized for gasless transactions.');
break;
case 'UNAUTHORIZED':
alert('Please sign in again.');
break;
case 'TRANSACTION_FAILED':
alert('Transaction failed on-chain. Please try again.');
break;
default:
alert('An unexpected error occurred.');
}
throw error;
}
}

Security & Limits

Whitelisted Contracts

Only these contracts are allowed:

  • PerkNFT: 0x6ae30d6Dcf3e75456B6582b057f1Bf98A90F2CA0
  • Loyalteez: 0x5242b6DB88A72752ac5a54cFe6A7DB8244d743c9
  • PointsSale: 0x5269B83F6A4E31bEdFDf5329DC052FBb661e3c72

Transactions to other contracts will be rejected.

Rate Limits

  • 35 transactions per hour per user
  • 1,000,000 gas maximum per transaction
  • 100 Gwei maximum gas price

Authentication

  • All requests require valid Privy access token
  • Token must not be expired
  • User must be authenticated

Monitoring Gas Usage

Track your gas spending in the Partner Portal:

Dashboard → Gas Usage
  • Total ETH spent
  • Transactions per day
  • Average gas cost
  • Monthly projections

Set up alerts when spending exceeds thresholds.

Best Practices

1. Estimate Gas Before Relaying

// Estimate gas first
const provider = new ethers.JsonRpcProvider(RPC_URL);
const contract = new ethers.Contract(address, abi, provider);

const estimated = await contract.claimPerk.estimateGas(perkId);
const withBuffer = estimated * 120n / 100n; // +20% buffer

// Use in relay
await relayTransaction({
to: address,
data: data,
gasLimit: Number(withBuffer)
});

2. Show Clear Status Updates

// Before
showLoading('Processing your claim...');

// During
showProgress('Transaction submitted. Waiting for confirmation...');

// After success
showSuccess(`Claimed! View transaction: ${txHash}`);

// After error
showError('Claim failed. Please try again.');

3. Cache Permit Signatures

// Permit valid for 1 hour - reuse it!
const cacheKey = `permit_${userAddress}_${spender}`;
const cached = localStorage.getItem(cacheKey);

if (cached && JSON.parse(cached).deadline > Date.now() / 1000) {
return JSON.parse(cached); // Reuse
} else {
const newPermit = await signPermit(...);
localStorage.setItem(cacheKey, JSON.stringify(newPermit));
return newPermit;
}

4. Implement Retry Logic

async function relayWithRetry(params: any, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await relayTransaction(params);
} catch (error: any) {
if (error.code === 'RATE_LIMIT_EXCEEDED') {
throw error; // Don't retry rate limits
}

if (attempt === maxRetries) {
throw error;
}

// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, attempt - 1))
);
}
}
}

5. Validate Before Submitting

async function validateAndRelay(params: any) {
// Check user has enough LTZ
const balance = await getLTZBalance(userAddress);
const required = await getPerkCost(perkId);

if (balance < required) {
throw new Error('Insufficient LTZ balance');
}

// Check perk is available
const perkStatus = await getPerkStatus(perkId);
if (!perkStatus.available) {
throw new Error('Perk is not available');
}

// All good - relay transaction
return await relayTransaction(params);
}

Testing

Test in Development

// Use Soneium Minato testnet for testing
const TEST_RELAYER_URL = 'https://relayer-test.loyalteez.app/relay';

// Test transaction
const result = await fetch(TEST_RELAYER_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${testToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: TEST_CONTRACT_ADDRESS,
data: testData,
userAddress: testUserAddress
})
});

Unit Tests

import { renderHook } from '@testing-library/react';
import { useGasRelayer } from './useGasRelayer';

describe('Gas Relayer', () => {
it('successfully relays transaction', async () => {
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true, transactionHash: '0xabc' })
});

const { result } = renderHook(() => useGasRelayer());
const txResult = await result.current.relayTransaction({
to: '0x123',
data: '0xabcd'
});

expect(txResult.success).toBe(true);
expect(txResult.transactionHash).toBe('0xabc');
});
});

Troubleshooting

Transaction Fails Silently

// Enable detailed logging
const result = await relayTransaction({
...params,
metadata: {
...params.metadata,
debug: true
}
});

console.log('Full result:', result);

Rate Limit Hit

// Implement cooldown tracking
const lastTx = localStorage.getItem('last_tx_time');
const now = Date.now();

if (lastTx && now - Number(lastTx) < 103000) { // ~103 seconds
alert('Please wait before making another transaction');
return;
}

await relayTransaction(params);
localStorage.setItem('last_tx_time', now.toString());

Gas Estimation Fails

// Use fixed gas limit
await relayTransaction({
to: address,
data: data,
gasLimit: 500000 // Fixed limit
});

Platform-Specific Guides

API Reference

For complete API documentation, see:

Support