Skip to main content
Your on-chain program can mint and redeem Continuum pairs through cross-program invocation (CPI). Typical callers:
  • 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_single and the other keeper instructions require market.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) and admin_fund (CLP) require the market or CLP authority.
No Continuum crate is published, and the program source is not public. Generate the CPI client from the published IDL with Anchor’s 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.
The struct has no account for 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, make user 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:
Don’t slice off the discriminator: 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 calls mint_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 in mint_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 writes market, 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

A redeem_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.