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
- React Integration - Complete React example
- React Native - Mobile implementation
- iOS - Native iOS integration
- Android - Native Android integration
API Reference
For complete API documentation, see:
- Gas Relayer API - Full API reference
- REST API - General API docs
Support
- Examples: github.com/Alpha4-Labs/loyalteez-examples
- Email: [email protected]
- Docs: docs.loyalteez.app