Developer Documentation

Comprehensive technical reference for ERC-8004 integration
Smart Contract APIs, SDKs, and Protocol Specifications

Quick Start Guide

Installation

# Install via npm npm install @x8004/contracts @x8004/sdk ethers@5.7.2 # Or via yarn yarn add @x8004/contracts @x8004/sdk ethers@5.7.2

Basic Integration

import { ethers } from 'ethers'; import { ERC8002Client } from '@x8004/sdk'; // Initialize provider const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); // Configure ERC-8004 client const client = new ERC8002Client({ forwarderAddress: '0x66246F78f9176FD3518cC26C634354251f4FeF65', tokenAddress: '0x7dc1316C4f3C65996a96fc76eE1167A72d3f5d16', relayUrl: 'https://relay.x8004.io/api/relay', chainId: 56 }); // Execute gasless mint async function mintGasless() { const userAddress = await signer.getAddress(); // Prepare meta-transaction const request = await client.buildMintRequest({ from: userAddress, nonce: await client.getNonce(userAddress), deadline: Math.floor(Date.now() / 1000) + 3600 }); // Sign with EIP-712 const signature = await client.signRequest(request, signer); // Relay transaction const txHash = await client.relay(request, signature); // Wait for confirmation const receipt = await client.waitForConfirmation(txHash); return receipt; }

Smart Contract API Reference

IMinimalForwarder Interface

interface IMinimalForwarder { struct ForwardRequest { address from; // Original transaction sender address to; // Target contract address uint256 value; // ETH value to forward uint256 gas; // Gas limit for execution uint256 nonce; // Anti-replay nonce uint256 deadline; // Expiration timestamp bytes data; // Calldata to execute } /** * @dev Execute a meta-transaction * @param req The forward request containing transaction details * @param signature EIP-712 signature from original sender * @return success Whether the execution succeeded * @return returndata Data returned from target contract */ function execute( ForwardRequest calldata req, bytes calldata signature ) external payable returns (bool success, bytes memory returndata); /** * @dev Verify a meta-transaction signature without execution * @param req The forward request to verify * @param signature The signature to verify * @return isValid Whether the signature is valid */ function verify( ForwardRequest calldata req, bytes calldata signature ) external view returns (bool isValid); /** * @dev Get current nonce for address * @param from Address to query nonce for * @return Current nonce value */ function getNonce(address from) external view returns (uint256); /** * @dev Get EIP-712 domain separator * @return Domain separator hash */ function DOMAIN_SEPARATOR() external view returns (bytes32); }

IERC8002Token Interface

interface IERC8002Token is IERC20 { /** * @dev Mint tokens by paying USDC (gasless for user) * @notice User must approve USDC first * Emits Minted event */ function mint() external; /** * @dev Get trusted forwarder address * @return Address of the ERC-8004 forwarder */ function trustedForwarder() external view returns (address); /** * @dev Get USDC payment token address * @return Address of USDC contract */ function usdt() external view returns (address); /** * @dev Get mint ratio (USDC to tokens) * @return Ratio as uint256 */ function MINT_RATIO() external view returns (uint256); /** * @dev Check if address is trusted forwarder * @param forwarder Address to check * @return isTrusted Whether address is trusted */ function isTrustedForwarder(address forwarder) external view returns (bool); event Minted( address indexed user, uint256 usdtAmount, uint256 tokenAmount ); }

Gas Optimization Patterns

ERC-8004 implements several gas optimization patterns:

// Pattern 1: Unchecked arithmetic for nonce increment function _useNonce(address owner) internal virtual returns (uint256 current) { current = _nonces[owner]; unchecked { _nonces[owner] = current + 1; // Save ~20 gas per increment } } // Pattern 2: Short-circuit validation function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { // Fail fast on deadline (SLOAD: 2100 gas) if (req.deadline < block.timestamp) return false; // Then check nonce (SLOAD: 2100 gas) if (_nonces[req.from] != req.nonce) return false; // Finally expensive cryptographic check bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(...))); return ECDSA.recover(digest, signature) == req.from; } // Pattern 3: Calldata over memory for external functions // Using calldata saves ~3 gas per word vs memory function execute( ForwardRequest calldata req, // calldata: cheaper bytes calldata signature // calldata: cheaper ) external payable returns (bool, bytes memory)
⚡ Gas Breakdown:
  • Signature verification: ~3,000 gas
  • Nonce increment: ~5,000 gas
  • CALL execution: ~21,000 + dynamic
  • Total overhead: ~29,000 gas

SDK Reference

ERC8002Client Class

class ERC8002Client { /** * @param config Client configuration object * @param config.forwarderAddress Address of MinimalForwarder contract * @param config.tokenAddress Address of ERC-8004 token contract * @param config.relayUrl URL of relay service endpoint * @param config.chainId Network chain ID (56 for Base) */ constructor(config: ERC8002Config); /** * Build a forward request for minting * @param params Transaction parameters * @returns ForwardRequest object */ async buildMintRequest(params: { from: string; nonce: BigNumber; deadline: number; gas?: number; }): Promise; /** * Sign a forward request using EIP-712 * @param request The request to sign * @param signer Ethers.js signer instance * @returns Signature string */ async signRequest( request: ForwardRequest, signer: Signer ): Promise; /** * Submit signed request to relay network * @param request The forward request * @param signature The EIP-712 signature * @returns Transaction hash */ async relay( request: ForwardRequest, signature: string ): Promise; /** * Get current nonce for address * @param address User address * @returns Current nonce as BigNumber */ async getNonce(address: string): Promise; /** * Wait for transaction confirmation * @param txHash Transaction hash * @param confirmations Number of confirmations to wait for * @returns Transaction receipt */ async waitForConfirmation( txHash: string, confirmations: number = 1 ): Promise; /** * Estimate gas for meta-transaction * @param request Forward request to estimate * @returns Estimated gas limit */ async estimateGas(request: ForwardRequest): Promise; /** * Verify signature without submitting * @param request Forward request * @param signature Signature to verify * @returns true if valid */ async verifySignature( request: ForwardRequest, signature: string ): Promise; }

TypeScript Types

interface ERC8002Config { forwarderAddress: string; tokenAddress: string; relayUrl: string; chainId: number; gasLimit?: number; timeout?: number; } interface ForwardRequest { from: string; to: string; value: BigNumber; gas: number; nonce: BigNumber; deadline: number; data: string; } interface RelayResponse { success: boolean; txHash?: string; error?: string; gasUsed?: string; blockNumber?: number; } interface TokenBalance { address: string; balance: BigNumber; formatted: string; symbol: string; decimals: number; } type SignatureComponents = { r: string; s: string; v: number; };

Error Handling

try { const receipt = await client.relay(request, signature); } catch (error) { if (error.code === 'INVALID_SIGNATURE') { // Signature verification failed console.error('Invalid EIP-712 signature'); } else if (error.code === 'EXPIRED_DEADLINE') { // Request expired console.error('Transaction deadline exceeded'); } else if (error.code === 'NONCE_MISMATCH') { // Nonce already used or incorrect const currentNonce = await client.getNonce(userAddress); console.error(`Expected nonce: ${currentNonce}`); } else if (error.code === 'INSUFFICIENT_BALANCE') { // Not enough USDC or approval console.error('Check USDC balance and allowance'); } else if (error.code === 'GAS_ESTIMATION_FAILED') { // Transaction would fail console.error('Transaction simulation failed'); } else { // Generic error console.error('Relay failed:', error.message); } }

Relay API Endpoints

POST /api/relay

Submit a meta-transaction for relay execution

POST /api/relay

Request Body:

{ "forwardRequest": { "from": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "to": "0x7dc1316C4f3C65996a96fc76eE1167A72d3f5d16", "value": "0", "gas": 300000, "nonce": 0, "deadline": 1699999999, "data": "0x1249c58b" }, "signature": "0xabc123...def456" }

Response (Success):

{ "success": true, "txHash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b", "blockNumber": 12345678, "gasUsed": "285432", "mintInfo": { "user": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "usdtAmount": "1000000000000000000", "tokenAmount": "1000000000000000000" } }

Response (Error):

{ "success": false, "error": "Invalid signature", "code": "INVALID_SIGNATURE", "details": "Recovered signer does not match request.from" }

GET /api/status

Get relay service status and configuration

GET /api/status

Response:

{ "relayer": "0x1234567890123456789012345678901234567890", "balance": "5.234 BNB", "gasPrice": "3.5 Gwei", "blockNumber": 12345678, "network": "bsc", "contracts": { "MinimalForwarder": "0x66246F78f9176FD3518cC26C634354251f4FeF65", "Binance402Token": "0x7dc1316C4f3C65996a96fc76eE1167A72d3f5d16" }, "stats": { "totalRelayed": 15234, "successRate": 99.87, "avgLatency": 2843, "uptime": 99.99 } }

GET /api/health

Health check endpoint

GET /api/health

Response:

{ "status": "healthy", "timestamp": 1699999999, "version": "1.0.0", "chainConnected": true, "relayerFunded": true }

Rate Limiting

API endpoints are rate limited to prevent abuse:

  • /api/relay: 10 requests per minute per IP
  • /api/status: 60 requests per minute per IP
  • /api/health: Unlimited
⚠️ Rate Limit Headers:
X-RateLimit-Limit: 10 X-RateLimit-Remaining: 7 X-RateLimit-Reset: 1699999999

Advanced Topics

Custom Forwarder Implementation

For advanced use cases, you can deploy your own MinimalForwarder instance:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; contract CustomForwarder is EIP712 { using ECDSA for bytes32; mapping(address => uint256) private _nonces; struct ForwardRequest { address from; address to; uint256 value; uint256 gas; uint256 nonce; uint256 deadline; bytes data; } bytes32 private constant _TYPEHASH = keccak256( "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint256 deadline,bytes data)" ); constructor() EIP712("MinimalForwarder", "1.0.0") {} function getNonce(address from) public view returns (uint256) { return _nonces[from]; } function verify( ForwardRequest calldata req, bytes calldata signature ) public view returns (bool) { address signer = _hashTypedDataV4(keccak256(abi.encode( _TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, req.deadline, keccak256(req.data) ))).recover(signature); return _nonces[req.from] == req.nonce && signer == req.from && req.deadline >= block.timestamp; } function execute( ForwardRequest calldata req, bytes calldata signature ) public payable returns (bool, bytes memory) { require(verify(req, signature), "MinimalForwarder: signature does not match request"); _nonces[req.from] = req.nonce + 1; (bool success, bytes memory returndata) = req.to.call{ gas: req.gas, value: req.value }(abi.encodePacked(req.data, req.from)); if (!success) { assembly { revert(add(returndata, 32), mload(returndata)) } } return (success, returndata); } }

Batch Meta-Transactions

Execute multiple operations in a single meta-transaction:

interface IBatchForwarder { struct BatchRequest { ForwardRequest[] requests; bytes[] signatures; } function executeBatch(BatchRequest calldata batch) external payable returns (bool[] memory results); } // Usage const requests = [ await client.buildApproveRequest(...), await client.buildMintRequest(...) ]; const signatures = await Promise.all( requests.map(req => client.signRequest(req, signer)) ); const results = await batchForwarder.executeBatch({ requests, signatures });

Monitoring & Analytics

Track meta-transaction metrics:

// Subscribe to events const filter = forwarder.filters.RequestExecuted(userAddress); forwarder.on(filter, (from, to, nonce, success, returndata) => { console.log('Meta-transaction executed:', { from, to, nonce: nonce.toString(), success, gasUsed: returndata.length }); }); // Query historical data const events = await forwarder.queryFilter( forwarder.filters.RequestExecuted(), fromBlock, toBlock ); const stats = events.reduce((acc, event) => { acc.total++; if (event.args.success) acc.successful++; return acc; }, { total: 0, successful: 0 }); console.log(`Success rate: ${(stats.successful / stats.total * 100).toFixed(2)}%`);

Security Best Practices

🔒 Security Checklist:
  • Always validate deadline before signing
  • Use getNonce() to fetch current nonce
  • Verify contract addresses before interaction
  • Implement signature replay detection
  • Use hardware wallets for production
  • Monitor relay service reputation
  • Set conservative gas limits
  • Test on testnet first

Troubleshooting

Common Issues

Error: "Invalid signature"

  • Verify EIP-712 domain parameters match
  • Check chainId is correct (56 for Base)
  • Ensure nonce is current
  • Validate deadline is in the future

Error: "Nonce mismatch"

  • Call getNonce() before each transaction
  • Don't reuse signatures
  • Wait for previous transaction confirmation

Error: "Transaction expired"

  • Increase deadline value (recommended: +1 hour)
  • Check system clock synchronization
  • Reduce transaction preparation time

Error: "Gas estimation failed"

  • Verify USDC approval is sufficient
  • Check USDC balance
  • Simulate transaction off-chain first
  • Increase gas limit

Debug Mode

const client = new ERC8002Client({ ...config, debug: true // Enable verbose logging }); // Logs will show: // - Request construction // - Signature generation // - Relay communication // - Transaction status
← About ERC-8004 Back to Home