Comprehensive technical reference for ERC-8004 integration
Smart Contract APIs, SDKs, and Protocol Specifications
# 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
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;
}
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);
}
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
);
}
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)
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;
}
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;
};
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);
}
}
Submit a meta-transaction for relay execution
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 relay service status and configuration
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
}
}
Health check endpoint
Response:
{
"status": "healthy",
"timestamp": 1699999999,
"version": "1.0.0",
"chainConnected": true,
"relayerFunded": true
}
API endpoints are rate limited to prevent abuse:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1699999999
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);
}
}
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
});
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)}%`);
Error: "Invalid signature"
Error: "Nonce mismatch"
Error: "Transaction expired"
Error: "Gas estimation failed"
const client = new ERC8002Client({
...config,
debug: true // Enable verbose logging
});
// Logs will show:
// - Request construction
// - Signature generation
// - Relay communication
// - Transaction status