Docs/Build guides/Become a Relayer
Become a Relayer
Participate in the Loyalty Network by finding Created reward actions, checking customer USDC allowance, submitting valid transactions, and earning the relayer fee.
Relayers do not need special permission and do not front the protocol fee. The customer funds the USDC fee through allowance, while the brand signature and contract validation decide whether a Created action can execute.
Find actions#
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#
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);Risk checks#
- Read
feeToken()andrequiredFee()from the factory instead of hardcoding the fee token or amount. - Check the customer's USDC allowance before submitting.
- Estimate gas and compare it to the relayer fee.
- Skip expired buckets, expired action deadlines, rejected signatures, and already-submitted nonces.