- A vault program that mints positions on its users’ behalf.
- A structured product that bundles L and S tokens across several markets.
- A rebalancer that reads NAV and acts on it.
Before you start
Most integrations don’t need CPI. Bots and frontends sign transactions directly; CPI is for protocols that run on chain themselves. Only the public instructions are open to other programs.mint_paired and redeem_paired need no signer beyond the owner of the tokens. pool_swap and commit_swap in the CLP program are public too, as are cancel_swap once an order has expired and claim for a held order. settle_swap is not: only the keeper can send it. Committed orders are enabled on every live market, and pool_swap fills only while the market’s price is stale. The rest check fixed signers stored on chain:
keeper_mint_single,keeper_redeem_singleand the other keeper instructions requiremarket.keeper_authority.- CLP book and inventory instructions (
set_pool_shape,pool_sweep_synth,pool_sweep_usdc,vault_mint_pairs) require the CLP or pool authority. - Admin instructions such as
update_market(mint-redeem) andadmin_fund(CLP) require the market or CLP authority.
declare_program! macro, available since Anchor 0.30. The programs are built with Anchor 0.32.1.
Setup
declare_program!(mint_redeem) reads idls/mint_redeem.json from your crate’s directory, or from the nearest parent directory that has an idls folder. Note the plural: the docs repository serves the files from idl/, and the macro reads idls/.
Declare the program
check_fresh repeats mint-redeem’s own gate (MAX_TWAP_AGE_SECS = 300), so your instruction fails with your own error before it pays for the CPI. mint_redeem::accounts::Market is the complete account type, so Account<'info, Market> checks the owner and the 8-byte discriminator for you.
Accounts
In this example a PDA of your program ([b"vault"]) owns the cUSDC, L and S token accounts and signs the CPI. mint-redeem checks every account against the Market, so your program can pass most of them through as UncheckedAccount. That also keeps the instruction’s stack frame small.
redeem_paired’s last slot, collateral_authority. Since the upgrade of 11 Oct 2026 the program never reads that slot and accepts any account, because each market’s own Market PDA signs its payouts. Pass None, as below: Anchor then puts mint-redeem’s program ID in the slot. mint_paired doesn’t have the slot.
Mint and redeem
redeem_paired requires both amounts to be above 0 (there is no single-leg redeem). The amounts are maxima: it burns the largest value-balanced pair inside them at the worst-case redeem NAVs, and the rest of the larger leg stays in the caller’s account. If the balanced pair has a zero leg it fails with InvalidAmount, and the pair it burns must be worth at least 10 cUSDC (BelowMinimum). Read the amounts actually burned from the PairedActionExecuted event, not from the arguments. It takes no exposure remaining accounts. By default both revert in Stress, with MintNotAllowedInStress or RedeemNotAllowedInStress. redeem_paired also reverts with InsufficientLiquidCollateral (6022) when the payout is more than the market’s liquid collateral; route a large redemption to request_redemption instead, which escrows the tokens and lets the keeper pay it in parts. After a CPI, call ctx.accounts.market.reload()? before you read market again: the deserialized struct still holds the values from before the call.
When the user signs
If the user signs for their own tokens, makeuser a Signer<'info> in your accounts struct and call without seeds. The user’s signature carries through the CPI:
Reading Market from a raw account
If you receive the market as an AccountInfo, deserialize the whole buffer:
try_deserialize(&mut &data[8..]) fails with a discriminator mismatch. Check the owner yourself, because a raw AccountInfo carries no owner check. Fields after the variable-length collateral_vaults Vec have no fixed byte offsets, so read Market through the type rather than at offsets.
How the CLP program does it
The CLP program callsmint_paired with its CLP PDA as user and signs with the PDA’s seeds. It builds the instruction by hand and calls invoke_signed directly, because a CpiContext for this many accounts would push its stack frame past Solana’s 4,096-byte limit. If your instruction hits the same limit, box your typed accounts (Box<Account<...>>) or pass them as UncheckedAccount.
Gotchas
Account order
The generated CPI structs put accounts in IDL order. If you build an instruction by hand, follow the order inmint_redeem.json exactly, with collateral_authority last on redeem_paired. Put mint-redeem’s program ID in that slot; the program ignores it.
Writable accounts
mint-redeem writesmarket, user, the three user token accounts, both mints, collateral_vault and dev_token_account. Mark each of them mut in your accounts struct; passing one read-only fails the CPI with a privilege escalation error.
Compute budget
Each Continuum CPI adds its compute to your instruction. If you bundle several, raise the limit in the transaction:Account limits
Aredeem_paired CPI brings 13 accounts, counting the program. Bundling several pushes a legacy transaction past its size limit, so use a versioned transaction with an address lookup table.
See also
Composability
Off-chain integration patterns: wrappers, bots, structured products.
Programs overview
Authority model: who can call what.

