Integration Guide
What this page is for: integrating rFlow into your own protocol or frontend. Covers whitelisting a receipt token, the canonical create/buy/settle flow, listening for on-chain events, and running settlement keepers.
Overview#
There are four kinds of integrators rFlow is built for:
- Yield-source protocols — get your receipt token whitelisted so users can monetize positions without leaving your app.
- Frontends and aggregators — surface deals from the program directly via the SDK.
- Structured products — buy baskets of deals on behalf of users; settlement is permissionless so payouts are no-coordination.
- Keeper operators — settle expired deals to collect vault rent.
Install & setup#
1import { RFlowClient, PROGRAM_ID } from "@rflowdapp/rflow";2import { Connection } from "@solana/web3.js";34export function makeClient(walletLike?: AnchorWalletLike) {5 const connection = new Connection(6 process.env.NEXT_PUBLIC_RPC_URL ?? "https://api.mainnet-beta.solana.com",7 "confirmed"8 );9 return walletLike10 ? new RFlowClient({ connection, wallet: walletLike })11 : RFlowClient.readOnly(connection);12}1314export { PROGRAM_ID };Whitelist a receipt token mint#
The program will reject create_deal for any mint not in config.allowed_mints. The whitelist is admin-only and capped at 10 entries.
Request criteria
Before requesting a mint be added, your token should ideally:
- Be an SPL Token with a documented exchange-rate or fee-accrual mechanism
- Have non-trivial TVL and a public RPC-readable exchange rate
- For LSTs / receipt tokens with strong USD coupling, ideally have a Pyth feed — otherwise the deal will use the bounded caller-value path
- Pass a documented integration test on devnet first
How requests are processed
update_config with add_mint once the integration is verified. The change takes effect at the next block.Sanity-check before depositing
1import { RFlowClient } from "@rflowdapp/rflow";23const config = await client.getConfig();4const isAllowed = config?.allowedMints.some((m) => m.equals(myReceiptMint));5if (!isAllowed) throw new Error("Mint not whitelisted yet");67const paymentAllowed = config?.allowedPaymentMints.some((m) => m.equals(usdcMint));8if (!paymentAllowed) throw new Error("Payment mint not whitelisted");9if (config?.isPaused) throw new Error("Protocol is paused");Reference flow#
The canonical flow is implemented in the rFlow app at src/services/blockchain.service.ts. Here is the minimal version using the SDK:
1import {2 RFlowClient,3 SourceProtocol,4 KNOWN_MINTS,5} from "@rflowdapp/rflow";6import {7 Connection,8 Transaction,9 sendAndConfirmTransaction,10} from "@solana/web3.js";11import { getAssociatedTokenAddress } from "@solana/spl-token";1213const connection = new Connection(process.env.RPC_URL!);14const client = new RFlowClient({ connection, wallet });1516// 1. Create17const createIxs = await client.yieldDeals.createDeal({18 receiptTokenMint: KNOWN_MINTS.MSOL,19 receiptTokensAmount: 10_000_000_000n,20 principalValueAtLock: 1_180_000_000n,21 expectedYield: 17_000_000n,22 sellingPrice: 14_000_000n,23 durationDays: 90,24 sourceProtocol: SourceProtocol.Marinade,25 exchangeRateAtLock: 1_180_000n,26 // priceUpdate: msolUsdPriceUpdate, // mainnet only27});28await send(createIxs);2930// 2. Buy31const dealId = (await client.getConfig())!.dealCounter - 1n;32const deal = await client.yieldDeals.getDeal(dealId);33const seller = await getAssociatedTokenAddress(deal!.paymentMint, deal!.seller);34const treas = await getAssociatedTokenAddress(35 deal!.paymentMint,36 (await client.getConfig())!.treasury37);38await send(await client.yieldDeals.buyDeal(dealId, seller, treas));3940// 3. Settle (after ends_at, anyone)41await send(42 await client.yieldDeals.settleDeal(dealId, currentTokenValue, priceUpdate)43);4445async function send(ixs: TransactionInstruction[]) {46 const tx = new Transaction().add(...ixs);47 return sendAndConfirmTransaction(connection, tx, [signerKeypair]);48}Listening for events#
rFlow does not emit Anchor events directly — every state change is driven by the deal account's status field and related timestamps. Three patterns work for indexing:
1. Account subscriptions (in-process)
1import { Connection, PublicKey } from "@solana/web3.js";2import { PROGRAM_ID } from "@rflowdapp/rflow";34const connection = new Connection("wss://your-rpc", "confirmed");56// All program accounts — for full indexing on startup7const accounts = await connection.getProgramAccounts(PROGRAM_ID);89// Stream live updates on the program logs10const subId = connection.onLogs(PROGRAM_ID, (logInfo) => {11 // logInfo.signature -> fetch the parsed tx, sync the touched deal12});1314// Or subscribe to a specific deal PDA15const dealSubId = connection.onAccountChange(dealPda, (info) => {16 // info.data is the YieldDeal — decode with the SDK's IDL17});2. Helius webhooks (recommended for production)
The rFlow reference app uses Helius enhanced webhooks pointed at the program ID. The webhook handler parses each transaction, identifies the touched deal PDA, refetches it on-chain, and writes the updated state to your indexer. The full handler lives at src/app/api/webhooks/helius/route.ts.
1// 1. Configure a Helius enhanced webhook2// Trigger: program ID 2woLsnG7zvKdyd7geH9GAFgKSt6NLrnLDDMmFBUdDjFU3// Type: enhanced transactions4// Auth: Bearer secret in the Authorization header56// 2. POST handler7export async function POST(req: Request) {8 const auth = req.headers.get("authorization");9 if (auth !== `Bearer ${process.env.HELIUS_WEBHOOK_SECRET}`) {10 return new Response("Unauthorized", { status: 401 });11 }1213 const transactions = await req.json();1415 for (const tx of transactions) {16 const instructions = parseRFlowInstructions(tx); // your own parser17 for (const ix of instructions) {18 switch (ix.action) {19 case "create_deal": // refetch + upsert20 case "create_meteora_deal":21 case "update_to_active": // -> status: 'active'22 case "update_to_settled": // -> status: 'settled'23 case "update_to_cancelled": // -> status: 'cancelled'24 case "update_to_bought_back": // -> status: 'bought_back'25 }26 }27 }2829 return Response.json({ ok: true });30}Reference parser
src/lib/helius/parser.ts which maps discriminators to actions and pulls deal PDAs from instruction accounts. Copy it directly — it's open source.3. Yellowstone gRPC (advanced)
For sub-second indexing latency, subscribe to program-account updates via a Yellowstone gRPC plugin (Triton, Helius gRPC, or your own validator). Filter on the rFlow program ID and your deals are pushed as they change.
Settlement keepers#
Both settle_deal and settle_meteora_lp_deal are permissionless. The signer receives the vault rent — a small but real incentive. A bare-bones keeper:
1import { RFlowClient, DealStatus } from "@rflowdapp/rflow";23async function tick() {4 const active = (await client.yieldDeals.getAllDeals({ status: DealStatus.Active }))5 .filter((d) => d.endsAt && d.endsAt.getTime() <= Date.now());67 for (const deal of active) {8 try {9 const currentValue = await fetchOracleValue(deal); // your code10 const ixs = await client.yieldDeals.settleDeal(11 deal.dealId,12 currentValue,13 getPythPriceUpdate(deal.receiptTokenMint)14 );15 const tx = new Transaction().add(...ixs);16 await sendAndConfirmTransaction(connection, tx, [keeperKeypair]);17 console.log("Settled deal", deal.dealId);18 } catch (err) {19 console.warn("Settle failed for", deal.dealId, err);20 }21 }22}2324setInterval(tick, 60_000);Mainnet LSTs require Pyth
price_update account. Without it the program returns OracleRequired. You can either crank a Pyth update on-the-fly with pyth-solana-receiver-sdk or use a hosted relayer.Production checklist#
- Using a dedicated RPC (Helius / Triton / QuickNode), not a public endpoint
- Caching
configwith a short TTL (30s) — it rarely changes - Checking
config.isPausedbefore every write - Validating
allowed_mintsandallowed_payment_mintsbefore constructing a deal - Estimating priority fees and adding
ComputeBudgetPrograminstructions where needed - Handling
InvalidTokenValueat settlement — your cached price may be stale - Subscribing to deal-account changes (not just transaction logs) for correct state recovery
- Confirming Pyth feed IDs and the 60-second freshness rule for every mainnet LST you support