Relayer API Reference¶
The Agent Registry Relayer is a gasless transaction relay service. It receives EIP-712 signed meta-transaction requests from agents (who hold no ETH) and submits them to the MinimalForwarder contract, paying gas on the agent's behalf.
Source: relayer/relayer.js
Default port: 3001
Configuration¶
| Environment Variable | Default | Description |
|---|---|---|
RELAYER_PORT |
3001 |
HTTP server port |
RPC_URL |
https://sepolia.base.org |
Blockchain RPC endpoint |
RELAYER_KEY |
-- (required) | Private key for the relayer wallet (pays gas) |
FORWARDER_ADDRESS |
-- (required) | Deployed MinimalForwarder contract address |
REGISTRY_ADDRESS |
-- (required) | Deployed Agent Registry contract address |
DAILY_GAS_BUDGET |
0.05 |
Daily gas budget in ETH |
RATE_LIMIT_PER_IP |
20 |
Max requests per IP per hour |
RATE_LIMIT_PER_SIGNER |
10 |
Max requests per signer per hour |
RELAYER_KEY=0x... \
FORWARDER_ADDRESS=0x70c2fdD0CDada6b43195981928D76f5D32AE29e5 \
REGISTRY_ADDRESS=0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23 \
node relayer/relayer.js
Rate Limiting¶
The relayer enforces rate limits at two levels plus a daily gas budget cap:
| Limit | Default | Window |
|---|---|---|
| Per IP | 20 requests | 1 hour |
| Per signer address | 10 requests | 1 hour |
| Daily gas budget | 0.05 ETH | 24 hours (rolling) |
When any limit is exceeded, the relayer returns:
HTTP status: 429 Too Many Requests
Security¶
The relayer enforces several security constraints:
- Target restriction: Only forwards transactions to the Agentenregister contract address. Attempts to relay to other addresses are rejected with
403. - No value transfers: Transactions with
value > 0are rejected. Agents cannot use the relayer to send ETH. - Signature verification: The MinimalForwarder's
verify()function is called on-chain before execution. Invalid signatures are rejected with401. - Balance check: If the relayer wallet balance drops below 0.001 ETH, new requests are rejected with
503. - Budget cap: Once the daily gas budget is exhausted, further requests are rejected until the next day.
Endpoints Overview¶
| Method | Endpoint | Description |
|---|---|---|
| POST | /relay |
Submit a signed meta-transaction |
| GET | /nonce/:address |
Get current forwarder nonce for an address |
| GET | /domain |
Get EIP-712 domain parameters for signing |
| GET | /status |
Relayer status, balance, and budget |
Submit Meta-Transaction¶
POST /relay¶
Submit a signed ERC-2771 meta-transaction for execution. The relayer verifies the signature, then submits the transaction to the MinimalForwarder and pays gas.
Request Body:
{
"request": {
"from": "0xAgentOrCreatorAddress",
"to": "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
"value": "0",
"gas": "800000",
"nonce": "0",
"deadline": "0",
"data": "0x..."
},
"signature": "0x..."
}
Request fields:
| Field | Type | Description |
|---|---|---|
request.from |
string |
Signer's address (agent or creator) |
request.to |
string |
Must be the Agentenregister contract address |
request.value |
string |
ETH value to forward. Must be "0" |
request.gas |
string |
Gas limit for the inner call (e.g. "800000" for registration) |
request.nonce |
string |
Current forwarder nonce for the signer (from GET /nonce/:address) |
request.deadline |
string |
Expiry timestamp. "0" means no expiry |
request.data |
string |
ABI-encoded function call data (hex) |
signature |
string |
EIP-712 signature over the ForwardRequest (hex) |
curl -X POST https://relay.theagentregistry.org/relay \
-H "Content-Type: application/json" \
-d '{
"request": {
"from": "0xAgentAddress",
"to": "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
"value": "0",
"gas": "800000",
"nonce": "0",
"deadline": "0",
"data": "0x..."
},
"signature": "0x..."
}'
Success Response (200):
{
"success": true,
"transactionHash": "0xabc123...",
"blockNumber": 12345678,
"gasUsed": "156789",
"gasPaidByRelayer": "0.000023456"
}
Error Responses:
| Status | Error | Description |
|---|---|---|
400 |
"Missing request or signature" |
Request body is incomplete |
401 |
"Invalid signature or nonce mismatch" |
Signature verification failed |
403 |
"Relayer only forwards to the Agentenregister contract" |
Target address is not the registry |
403 |
"Value transfers not supported" |
Request has value > 0 |
429 |
"IP rate limit exceeded (20/hour)" |
IP rate limit reached |
429 |
"Signer rate limit exceeded (10/hour)" |
Per-signer rate limit reached |
429 |
"Daily gas budget exhausted. Try again tomorrow." |
Daily budget cap reached |
500 |
(varies) | Transaction reverted or RPC error |
503 |
"Relayer balance too low..." |
Relayer wallet needs funding |
Get Nonce¶
GET /nonce/:address¶
Get the current forwarder nonce for an address. This nonce must be included in the ForwardRequest to prevent replay attacks.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address |
string |
Ethereum address of the signer |
Response:
Nonce handling
Always fetch a fresh nonce before building a relay request. The nonce increments after each successful relay. Using a stale nonce will cause signature verification to fail.
Get EIP-712 Domain¶
GET /domain¶
Get the EIP-712 domain parameters needed for signing ForwardRequest messages on the client side.
Response:
{
"name": "MinimalForwarder",
"version": "1",
"chainId": "84532",
"verifyingContract": "0x70c2fdD0CDada6b43195981928D76f5D32AE29e5"
}
| Field | Description |
|---|---|
name |
EIP-712 domain name (always "MinimalForwarder") |
version |
EIP-712 domain version (always "1") |
chainId |
The chain ID as a string |
verifyingContract |
The MinimalForwarder contract address |
Get Status¶
GET /status¶
Get the relayer's operational status, wallet balance, and gas budget usage.
Response:
{
"relayer": "0xRelayerWalletAddress",
"balance": "0.048523",
"forwarder": "0x70c2fdD0CDada6b43195981928D76f5D32AE29e5",
"registry": "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
"rpc": "https://sepolia.base.org",
"dailyBudget": "0.05 ETH",
"dailyGasUsed": "0.001477 ETH",
"budgetRemaining": "0.048523 ETH"
}
| Field | Description |
|---|---|
relayer |
Relayer wallet address |
balance |
Current ETH balance of the relayer wallet |
forwarder |
MinimalForwarder contract address |
registry |
Agent Registry contract address |
rpc |
RPC endpoint URL |
dailyBudget |
Configured daily gas budget |
dailyGasUsed |
Gas spent today |
budgetRemaining |
Remaining gas budget for today |
EIP-712 Signing Flow¶
The gasless meta-transaction flow uses EIP-712 typed data signing. Here is the complete flow for building and submitting a relay request:
1. Agent encodes the target function call as ABI calldata
2. Agent fetches the current nonce from GET /nonce/:address
3. Agent fetches the EIP-712 domain from GET /domain
4. Agent builds a ForwardRequest struct
5. Agent signs the ForwardRequest using EIP-712 (signTypedData)
6. Agent POSTs { request, signature } to POST /relay
7. Relayer verifies signature on-chain via forwarder.verify()
8. Relayer executes via forwarder.execute() and pays gas
9. MinimalForwarder appends the real sender to calldata (ERC-2771)
10. Agentenregister sees _msgSender() = agent, not relayer
ForwardRequest Type Definition¶
The EIP-712 ForwardRequest type used for signing:
{
"ForwardRequest": [
{ "name": "from", "type": "address" },
{ "name": "to", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "gas", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint48" },
{ "name": "data", "type": "bytes" }
]
}
Signing Example¶
const { ethers } = require("ethers");
const wallet = new ethers.Wallet("0xAGENT_PRIVATE_KEY");
// 1. Encode function call
const iface = new ethers.Interface([
"function attestCompliance(uint256 agentId)",
]);
const calldata = iface.encodeFunctionData("attestCompliance", [1]);
// 2. Fetch nonce
const nonceRes = await fetch("https://relay.theagentregistry.org/nonce/" + wallet.address);
const { nonce } = await nonceRes.json();
// 3. Build request
const request = {
from: wallet.address,
to: "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
value: "0",
gas: "200000",
nonce: nonce,
deadline: "0",
data: calldata,
};
// 4. Sign EIP-712 (free -- no gas)
const signature = await wallet.signTypedData(
{
name: "MinimalForwarder",
version: "1",
chainId: 84532n,
verifyingContract: "0x70c2fdD0CDada6b43195981928D76f5D32AE29e5",
},
{
ForwardRequest: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint48" },
{ name: "data", type: "bytes" },
],
},
request,
);
// 5. Submit to relayer
const res = await fetch("https://relay.theagentregistry.org/relay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request, signature }),
});
const result = await res.json();
console.log("TX:", result.transactionHash);
from eth_account import Account
from eth_account.messages import encode_structured_data
import requests
import json
account = Account.from_key("0xAGENT_PRIVATE_KEY")
# 1. Encode function call (use web3.py or manual ABI encoding)
# calldata = contract.functions.attestCompliance(1)._encode_transaction_data()
# 2. Fetch nonce
nonce_resp = requests.get(f"https://relay.theagentregistry.org/nonce/{account.address}")
nonce = int(nonce_resp.json()["nonce"])
# 3. Build EIP-712 typed data
typed_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"ForwardRequest": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "gas", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint48"},
{"name": "data", "type": "bytes"},
],
},
"primaryType": "ForwardRequest",
"domain": {
"name": "MinimalForwarder",
"version": "1",
"chainId": 84532,
"verifyingContract": "0x70c2fdD0CDada6b43195981928D76f5D32AE29e5",
},
"message": {
"from": account.address,
"to": "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
"value": 0,
"gas": 200000,
"nonce": nonce,
"deadline": 0,
"data": calldata,
},
}
# 4. Sign (free -- no gas)
encoded = encode_structured_data(typed_data)
signed = account.sign_message(encoded)
# 5. Submit to relayer
resp = requests.post("https://relay.theagentregistry.org/relay", json={
"request": {
"from": account.address,
"to": "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
"value": "0",
"gas": "200000",
"nonce": str(nonce),
"deadline": "0",
"data": calldata,
},
"signature": signed.signature.hex(),
})
result = resp.json()
print(f"TX: {result['transactionHash']}")