Docs/Reference/Relayers
Relayers
How independent relayers discover prepared reward actions, supply the required native fee, 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, read requiredFee(), then decide whether the relayer fee makes execution worthwhile.
Execution#
Relayers submit from their own wallet with exact native msg.value. If the transaction succeeds, the contract distributes the issuer and protocol shares and returns the relayer portion to the submitting address.
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"));