Docs/Build guides/Become a Relayer

Become a Relayer

Participate in the Loyalty Network by finding Created reward actions, reading the required native fee, submitting valid transactions with exact msg.value, and earning the relayer fee.

GoalSubmit Created actions and earn the relayer fee.

Relayers do not need special permission. The submitting wallet supplies requiredFee() in native ETH, while the brand signature and contract validation decide whether a Created action can execute.

Find actions#

List pending issuancesbash
curl "https://api.loyfin.com/operations?kind=issuance&status=pending&loyaltyId=0x4242424242424242424242424242424242424242424242424242424242424242&limit=25"

Use the wire status pending to find actions the UI calls Created. Brands only need to publish valid EIP-712 signatures; relayers compete on speed, reliability, and gas discipline to execute those actions and participate in the Loyalty Network.

Submit#

Reconstruct and submittypescript
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const abi = parseAbi([
  "function requiredFee() view returns (uint256)",
  "function issue((address issuer,address to,bytes32 loyaltyId,uint256 amount,uint256 expiresAt,uint256 deadline,bytes32 nonce,uint256 chainId,address verifyingContract,bytes32 operationHash,(bytes32 loyaltyId,string name,string symbol,string media,string description,string contractURI,string tokenURI) metadata,bytes data),bytes) payable returns (address)",
  "function redeem((address issuer,address from,bytes32 loyaltyId,uint256 amount,uint256 expiresAt,uint256 deadline,bytes32 nonce,uint256 chainId,address verifyingContract,bytes32 operationHash,bytes data),bytes) payable",
  "function issueClaimable((address issuer,bytes32 loyaltyId,uint256 amount,uint256 expiresAt,uint256 deadline,bytes32 nonce,uint256 chainId,address verifyingContract,bytes32 operationHash,(bytes32 loyaltyId,string name,string symbol,string media,string description,string contractURI,string tokenURI) metadata,bytes data),bytes,(address to,bytes32 claimHash,uint256 chainId,address verifyingContract,uint256 deadline),bytes) payable returns (address)",
  "function redeemClaimable((address issuer,bytes32 loyaltyId,uint256 amount,uint256 expiresAt,uint256 deadline,bytes32 nonce,uint256 chainId,address verifyingContract,bytes32 operationHash,bytes data),bytes,(address from,bytes32 claimHash,uint256 chainId,address verifyingContract,uint256 deadline),bytes) payable"
]);

const privateKey = process.env.LOYFIN_RELAYER_PRIVATE_KEY;
if (!privateKey) throw new Error("Missing LOYFIN_RELAYER_PRIVATE_KEY");
const account = privateKeyToAccount(privateKey);
const publicClient = createPublicClient({ chain: base, transport: http() });
const walletClient = createWalletClient({ account, chain: base, transport: http() });

async function relayOne() {
  const response = await fetch(
    "https://api.loyfin.com/operations?status=pending&chainId=8453&limit=1"
  );
  if (!response.ok) throw new Error(await response.text());
  const operation = (await response.json()).items[0];
  if (!operation) return null;

  const requiredFee = await publicClient.readContract({
    address: operation.verifyingContract,
    abi,
    functionName: "requiredFee"
  });
  if (await publicClient.getBalance({ address: account.address }) <= requiredFee) {
    throw new Error("Relayer wallet needs the required fee plus ETH for gas.");
  }

  const action = {
    issuer: operation.issuer,
    loyaltyId: operation.loyaltyId,
    amount: BigInt(operation.amount),
    expiresAt: BigInt(operation.expiresAt),
    deadline: BigInt(operation.deadline),
    nonce: operation.nonce,
    chainId: BigInt(operation.chainId),
    verifyingContract: operation.verifyingContract,
    operationHash: operation.operationHash,
    data: operation.data
  };

  let functionName;
  let args;
  if (operation.kind === "issuance") {
    const issuance = { ...action, metadata: operation.metadata };
    if (operation.signatureScheme === "dual") {
      if (!operation.holderSignature || !operation.claimHash) return null;
      functionName = "issueClaimable";
      args = [issuance, operation.signature, {
        to: operation.holder,
        claimHash: operation.claimHash,
        chainId: BigInt(operation.chainId),
        verifyingContract: operation.verifyingContract,
        deadline: BigInt(operation.deadline)
      }, operation.holderSignature];
    } else {
      functionName = "issue";
      args = [{ ...issuance, to: operation.holder }, operation.signature];
    }
  } else if (operation.signatureScheme === "dual") {
    if (!operation.holderSignature || !operation.claimHash) return null;
    functionName = "redeemClaimable";
    args = [action, operation.signature, {
      from: operation.holder,
      claimHash: operation.claimHash,
      chainId: BigInt(operation.chainId),
      verifyingContract: operation.verifyingContract,
      deadline: BigInt(operation.deadline)
    }, operation.holderSignature];
  } else {
    functionName = "redeem";
    args = [{ ...action, from: operation.holder }, operation.signature];
  }

  const { request } = await publicClient.simulateContract({
    account,
    address: operation.verifyingContract,
    abi,
    functionName,
    args,
    value: requiredFee
  });
  return walletClient.writeContract(request);
}

relayOne().then((hash) => console.log(hash ?? "no ready operation"));

Risk checks#

  • Read requiredFee() from the factory immediately before submitting instead of hardcoding the amount.
  • Send that exact amount as msg.value and keep enough ETH for gas.
  • Estimate gas and compare it to the relayer fee.
  • Skip expired buckets, expired action deadlines, rejected signatures, and already-submitted nonces.
Loyfin is built onBase