Skip to content

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:

{
  "error": "IP rate limit exceeded (20/hour)"
}

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 > 0 are 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 with 401.
  • 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
curl https://relay.theagentregistry.org/nonce/0xAgentAddress

Response:

{
  "address": "0xAgentAddress",
  "nonce": "3"
}

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.

curl https://relay.theagentregistry.org/domain

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.

curl https://relay.theagentregistry.org/status

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']}")