TypeScript SDK for the rFlow Solana Protocol — yield discounting + Meteora LP fee deals on Solana mainnet.
rFlow lets users sell their future yield from DeFi protocols (lending, liquid staking, Meteora DAMM v2 LP fees) for immediate USDC. Sellers lock receipt tokens / LP NFTs, buyers pay upfront for the right to collect the yield at maturity.
| Network | Program ID |
|---|---|
| Mainnet | 2woLsnG7zvKdyd7geH9GAFgKSt6NLrnLDDMmFBUdDjFU |
npm install @rflowdapp/rflow @coral-xyz/anchor @solana/web3.js @solana/spl-tokenThe Pyth oracle helper is optional. Install it only if you trade LST deals (mSOL / jitoSOL / bSOL) on mainnet:
npm install @pythnetwork/hermes-client @pythnetwork/pyth-solana-receiverimport { RFlowClient, SourceProtocol, KNOWN_MINTS } from "@rflowdapp/rflow";
import { Connection, Transaction, sendAndConfirmTransaction, Keypair } from "@solana/web3.js";
import { Wallet } from "@coral-xyz/anchor";
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const wallet = new Wallet(Keypair.fromSecretKey(/* your secret */));
const client = new RFlowClient({ connection, wallet });
const ixs = await client.yieldDeals.createDeal({
receiptTokenMint: KNOWN_MINTS.MSOL, // mSOL (9 decimals)
receiptTokensAmount: 10_000_000_000, // 10 mSOL locked
principalValueAtLock: 2_000_000_000, // $2000 in USDC units (6 dp)
expectedYield: 30_000_000, // 30 USDC of yield
sellingPrice: 25_000_000, // sell for 25 USDC (17% discount)
durationDays: 90,
sourceProtocol: SourceProtocol.Marinade,
exchangeRateAtLock: 200_000_000, // 1 mSOL ≈ 200 USDC, scaled 1e6
});
const tx = new Transaction().add(...ixs);
await sendAndConfirmTransaction(connection, tx, [wallet.payer]);import { RFlowClient, KNOWN_MINTS } from "@rflowdapp/rflow";
import { PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
const ixs = await client.meteoraDeals.createDeal({
positionNftMint: new PublicKey("..."), // Position NFT mint (Token-2022)
positionAccount: new PublicKey("..."), // Meteora Position account
pool: new PublicKey("..."), // Meteora DAMM v2 pool
tokenAMint: new PublicKey("So111..."), // wSOL
tokenBMint: KNOWN_MINTS.USDC,
feeAAtLock: 0, // snapshot fee_a_pending
feeBAtLock: 0, // snapshot fee_b_pending
expectedFeeA: 250_000_000, // estimated SOL fees over duration
expectedFeeB: 75_000_000, // estimated USDC fees over duration
expectedFeeValueUsdc: 100_000_000, // 100 USDC combined estimate
sellingPrice: 80_000_000, // sell for 80 USDC (20% discount)
durationDays: 60,
});
const tx = new Transaction().add(...ixs);
await sendAndConfirmTransaction(connection, tx, [wallet.payer]);See examples/ for fully-runnable scripts (npx tsx examples/<name>.ts).
After a deal's endsAt is in the past, anyone can call settleDeal (it's permissionless). The SDK does three things for you that the on-chain program won't:
- Defaults
currentTokenValuetoprincipalValueAtLock + expectedYieldread straight off the on-chain deal account (both raw u64 BNs). That value always falls inside the program'sexchangeRateAtLock ± 10%tolerance band — passing a UI-units number (e.g.632.81instead of632_808_370) returnsInvalidTokenValue(Custom 6027). - Adds idempotent
createAssociatedTokenAccountinstructions for the buyer's and seller's receipt-token ATAs when missing. The buyer not having an ATA is what blocked settlement in production before v0.2.0. - Auto-fetches the Pyth price update for LST receipt tokens when
config.use_oracleis true on-chain (mSOL, jitoSOL, bSOL). This sends a separate transaction to the Pyth Solana receiver before settle.
const ixs = await client.yieldDeals.settleDeal(dealId);
const tx = new Transaction().add(...ixs);
await sendAndConfirmTransaction(connection, tx, [wallet.payer]);For non-LST tokens or devnet, no Pyth update is needed and the SDK skips that step automatically.
// Seller pays selling_price + yield + dynamic penalty (3% → 1%) to the buyer
const ixs = await client.yieldDeals.buybackDeal(dealId);Same defaults: currentTokenValue auto-resolved, Pyth auto-fetched, ATAs auto-created.
import { BN } from "@coral-xyz/anchor";
const ixs = await client.yieldDeals.settleDeal(dealId, {
currentTokenValue: new BN("640123456"), // explicit override (raw u64)
priceUpdate: customPythAccount, // bypass auto-fetch
skipPythAutoFetch: true, // never fetch from Hermes
});Settling a Meteora LP deal auto-claims any pending fees to the buyer's Token A/B accounts and returns the Position NFT to the seller. Because of the embedded CPI, the SDK needs the full Meteora account set:
import { PublicKey } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@rflowdapp/rflow";
const METEORA_CP_AMM_PROGRAM = new PublicKey("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG");
const METEORA_POOL_AUTHORITY = new PublicKey("HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC");
// event authority PDA = ["__event_authority"] on METEORA_CP_AMM_PROGRAM
const ixs = await client.meteoraDeals.settleDeal({
dealId,
meteoraProgram: METEORA_CP_AMM_PROGRAM,
poolAuthority: METEORA_POOL_AUTHORITY,
eventAuthority: eventAuthorityPda,
poolTokenAVault,
poolTokenBVault,
nftTokenProgram: TOKEN_2022_PROGRAM_ID, // Meteora positions are Token-2022
tokenAProgram: TOKEN_PROGRAM_ID,
tokenBProgram: TOKEN_PROGRAM_ID,
});The on-chain program embeds Pyth feed IDs for these three LSTs at programs/payflow/src/constants.rs. The SDK exposes the same constants:
| Symbol | Mint | Pyth Feed ID (hex) |
|---|---|---|
| mSOL | mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So |
0xc2289a6a43d2ce91c6f55caec370f4acc38a2ed477f58813334c6d03749ff2a4 |
| jitoSOL | J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn |
0x67be9f519b95cf24338801051f9a808eff0a578ccb388db73b7f6fe1de019ffb |
| bSOL | bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1 |
0x89875379e70f8fbadc17aef315adf3a8d5d160b811435537e03c97e8aac97d9c |
import { isLSTToken, getPythFeedForMint, PYTH_PRICE_FEEDS, KNOWN_MINTS } from "@rflowdapp/rflow";
isLSTToken(KNOWN_MINTS.MSOL); // true
getPythFeedForMint(KNOWN_MINTS.JITO_SOL); // "0x67be9f...."
PYTH_PRICE_FEEDS.bSOL; // same hex feedNeed the priceUpdate PublicKey on demand? Use the helper directly:
import { getPythPriceUpdate } from "@rflowdapp/rflow";
const priceUpdate = await getPythPriceUpdate(connection, wallet, KNOWN_MINTS.MSOL);
// or, equivalently:
const priceUpdate2 = await client.fetchPythPriceUpdate(KNOWN_MINTS.MSOL);It posts the latest Pyth price update on-chain (one extra transaction) and returns the priceUpdate account address ready to be passed as priceUpdate to createDeal / settleDeal / buybackDeal.
import { RFlowClient } from "@rflowdapp/rflow";
const client = RFlowClient.readOnly(connection);
await client.yieldDeals.getAvailableDeals();
await client.yieldDeals.getDealsBySeller(wallet.publicKey);
await client.yieldDeals.getDealsByBuyer(wallet.publicKey);
await client.yieldDeals.getDeal(42);
await client.meteoraDeals.getAvailableDeals();
await client.meteoraDeals.getDeal(7);
await client.getConfig();
await client.isPaused();import {
findYieldDealPDA,
findVaultPDA,
findProtocolConfigPDA,
findMeteoraLpDealPDA,
findMeteoraVaultPDA,
} from "@rflowdapp/rflow";
const [dealPda, bump] = findYieldDealPDA(42);
const [vaultPda] = findVaultPDA(dealPda);
const [configPda] = findProtocolConfigPDA();interface YieldDeal {
dealId: number;
pda: PublicKey;
seller: PublicKey;
buyer: PublicKey | null;
receiptTokenMint: PublicKey;
receiptTokenVault: PublicKey;
receiptTokensAmount: BN;
principalValueAtLock: BN;
expectedYield: BN;
sellingPrice: BN;
paymentMint: PublicKey;
durationDays: 30 | 60 | 90 | 180 | 365;
createdAt: Date;
purchasedAt: Date | null;
endsAt: Date | null;
status: DealStatus; // "created" | "active" | "settled" | "cancelled" | "bought_back"
sourceProtocol: SourceProtocol;
isAvailable: boolean;
isExpired: boolean;
}
enum SourceProtocol {
Kamino = "kamino",
Solend = "solend",
Save = "save",
Marinade = "marinade",
Jito = "jito",
Blaze = "blaze",
Sanctum = "sanctum",
RaydiumLp = "raydium_lp",
MeteoraLp = "meteora_lp",
OrcaLp = "orca_lp",
FeeStream = "fee_stream",
}MeteoraLpDeal and ProtocolConfig are exported from the same entry. See src/types/sdk.ts for the complete list.
import {
RFlowError,
ProtocolPausedError,
DealNotFoundError,
InvalidDurationError,
parseAnchorError,
} from "@rflowdapp/rflow";
try {
await client.yieldDeals.createDeal(input);
} catch (err) {
const parsed = parseAnchorError(err);
if (parsed instanceof ProtocolPausedError) console.error("Protocol is paused");
else if (parsed instanceof DealNotFoundError) console.error("Deal not found");
else if (parsed instanceof RFlowError) console.error(parsed.code, parsed.message);
else throw err;
}The most common on-chain error you'll see is InvalidTokenValue (Custom 6027) at settle/buyback — caused by passing UI-units (e.g. 632.81) instead of raw u64 (632_808_370). The SDK's default behaviour prevents this; use it.
- Mainnet-only. The default
PROGRAM_IDis the deployed mainnet program. There is currently no devnet deployment of v0.2 contracts. Pass a customprogramIdtoRFlowClientif you've deployed your own. config.use_oracleis set on-chain. The SDK auto-fetches Pyth updates only when bothconfig.useOracle === trueAND the receipt mint is in the LST whitelist. If you operate a fork withuse_oracle = false, the SDK skips Pyth even for LSTs.- Meteora positions are Token-2022. Pass
nftTokenProgram: TOKEN_2022_PROGRAM_IDwhen callingcreateDeal/cancelDeal/settleDealon Meteora LP deals. The SDK exportsTOKEN_2022_PROGRAM_IDfor convenience. Pythhelper requires extra deps.getPythPriceUpdateuses dynamicimport()for@pythnetwork/hermes-clientand@pythnetwork/pyth-solana-receiver. They're declared asoptionalDependencies— install them if you trade LSTs on mainnet, skip them otherwise.- No bundled wallet adapter. Bring your own
@coral-xyz/anchorWallet. For browser apps, use@solana/wallet-adapter.
MIT