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#

Terminal
$npm install @rflowdapp/rflow @coral-xyz/anchor @solana/web3.js @solana/spl-token
lib/rflow.ts
1import { RFlowClient, PROGRAM_ID } from "@rflowdapp/rflow";
2import { Connection } from "@solana/web3.js";
3
4export 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 walletLike
10 ? new RFlowClient({ connection, wallet: walletLike })
11 : RFlowClient.readOnly(connection);
12}
13
14export { 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
Reach the team in Discord or on GitHub. The authority calls update_config with add_mint once the integration is verified. The change takes effect at the next block.

Sanity-check before depositing

typescript
1import { RFlowClient } from "@rflowdapp/rflow";
2
3const config = await client.getConfig();
4const isAllowed = config?.allowedMints.some((m) => m.equals(myReceiptMint));
5if (!isAllowed) throw new Error("Mint not whitelisted yet");
6
7const 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:

flows.ts
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";
12
13const connection = new Connection(process.env.RPC_URL!);
14const client = new RFlowClient({ connection, wallet });
15
16// 1. Create
17const 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 only
27});
28await send(createIxs);
29
30// 2. Buy
31const 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())!.treasury
37);
38await send(await client.yieldDeals.buyDeal(dealId, seller, treas));
39
40// 3. Settle (after ends_at, anyone)
41await send(
42 await client.yieldDeals.settleDeal(dealId, currentTokenValue, priceUpdate)
43);
44
45async 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)

typescript
1import { Connection, PublicKey } from "@solana/web3.js";
2import { PROGRAM_ID } from "@rflowdapp/rflow";
3
4const connection = new Connection("wss://your-rpc", "confirmed");
5
6// All program accounts — for full indexing on startup
7const accounts = await connection.getProgramAccounts(PROGRAM_ID);
8
9// Stream live updates on the program logs
10const subId = connection.onLogs(PROGRAM_ID, (logInfo) => {
11 // logInfo.signature -> fetch the parsed tx, sync the touched deal
12});
13
14// Or subscribe to a specific deal PDA
15const dealSubId = connection.onAccountChange(dealPda, (info) => {
16 // info.data is the YieldDeal — decode with the SDK's IDL
17});

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.

app/api/webhooks/rflow/route.ts (shape)
1// 1. Configure a Helius enhanced webhook
2// Trigger: program ID 2woLsnG7zvKdyd7geH9GAFgKSt6NLrnLDDMmFBUdDjFU
3// Type: enhanced transactions
4// Auth: Bearer secret in the Authorization header
5
6// 2. POST handler
7export 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 }
12
13 const transactions = await req.json();
14
15 for (const tx of transactions) {
16 const instructions = parseRFlowInstructions(tx); // your own parser
17 for (const ix of instructions) {
18 switch (ix.action) {
19 case "create_deal": // refetch + upsert
20 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 }
28
29 return Response.json({ ok: true });
30}
Reference parser
The rFlow app ships 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:

keeper.ts
1import { RFlowClient, DealStatus } from "@rflowdapp/rflow";
2
3async function tick() {
4 const active = (await client.yieldDeals.getAllDeals({ status: DealStatus.Active }))
5 .filter((d) => d.endsAt && d.endsAt.getTime() <= Date.now());
6
7 for (const deal of active) {
8 try {
9 const currentValue = await fetchOracleValue(deal); // your code
10 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}
23
24setInterval(tick, 60_000);
Mainnet LSTs require Pyth
When you settle a mainnet LST deal, you must pass the 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 config with a short TTL (30s) — it rarely changes
  • Checking config.isPaused before every write
  • Validating allowed_mints and allowed_payment_mints before constructing a deal
  • Estimating priority fees and adding ComputeBudgetProgram instructions where needed
  • Handling InvalidTokenValue at 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