Docs/Reference/Relayers

Relayers

How independent relayers discover prepared reward actions, check customer USDC allowance, submit them, and earn the relayer fee.

Opportunities#

Relayers look for Created issuances and redemptions.

The coordination API exposes signed actions that have been created but not completed. A relayer can filter by kind, status, issuer, and program, check customer USDC allowance, then decide whether the relayer fee makes execution worthwhile.

Execution#

Relayers submit from their own wallet. If the transaction succeeds, the customer pays the USDC fee through transferFrom, and the submitting address receives the relayer portion.

Relayer looptypescript
import { createPublicClient, createWalletClient, http } from "viem";
import { base } from "viem/chains";

const page = await fetch(
  "https://api.loyfin.com/operations?kind=issuance&status=pending&limit=1"
).then((response) => response.json());

const operation = page.items[0];
if (!operation) return;

const publicClient = createPublicClient({ chain: base, transport: http() });
const [feeToken, requiredFee] = await Promise.all([
  publicClient.readContract({
    address: operation.verifyingContract,
    abi: loyfinAbi,
    functionName: "feeToken"
  }),
  publicClient.readContract({
    address: operation.verifyingContract,
    abi: loyfinAbi,
    functionName: "requiredFee"
  })
]);

const customer = operation.holder;
const allowance = await publicClient.readContract({
  address: feeToken,
  abi: erc20Abi,
  functionName: "allowance",
  args: [customer, operation.verifyingContract]
});

if (allowance < requiredFee) {
  throw new Error("Customer must approve USDC before relayer submission.");
}

const metadata = operation.metadata;
const hash = await walletClient.writeContract({
  address: operation.verifyingContract,
  abi: loyfinAbi,
  functionName: operation.kind === "issuance" ? "issue" : "redeem",
  args: operation.kind === "issuance"
    ? [{
        issuer: operation.issuer,
        to: operation.holder,
        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,
        metadata: {
          loyaltyId: metadata.loyaltyId,
          name: metadata.name,
          symbol: metadata.symbol,
          media: metadata.media,
          description: metadata.description ?? "",
          contractURI: metadata.contractURI ?? "",
          tokenURI: metadata.tokenURI ?? ""
        },
        data: operation.data
      }, operation.signature]
    : [{
        issuer: operation.issuer,
        from: operation.holder,
        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
      }, operation.signature]
});

console.log("submitted", hash);