# Composability
Source: https://docs.continuum.markets/build/composability
What you can build on Continuum's L and S tokens, and the protocol limits each design has to respect.
L and S are plain SPL tokens, and the instructions users call (`mint_paired`, `redeem_paired`, `pool_swap`, `commit_swap`) are public. Here are concrete things you can build on them.
## Token facts
Every L and S mint is a classic SPL Token mint (program `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`) with no Token-2022 extensions. Each has 6 decimals and no freeze authority, so no one can freeze a holder's tokens. The mint authority is the market's Market PDA, so only mint-redeem can create new tokens. All 28 mints of the 14 markets then listed matched this on chain on 10 Oct 2026 (MSTR has since closed, leaving 26); for QQQ, the L mint `E4xJxBPyDXXJK2uqF17cq9kch1xHCTBZ6vjMxKWKANgZ` and the S mint `BisvPibkUJqix3ZzaeHCxJ2W7KqoroFpnyf55nzFoicG` both have mint authority `F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5`.
These are devnet tokens backed by devnet cUSDC. They can't be used on mainnet protocols such as Kamino.
## 1. Long-only or short-only wrapper
Most users want a one-click long or short, not a pair. Wrap it:
```
User clicks "Buy QQQ long" with 100 cUSDC
│
▼
1. mint_paired(100 cUSDC) → 0.066374 QQQ L + 0.163706 QQQ S
2. sell the S leg → 0.066374 QQQ L + about 49.86 cUSDC, less the spread
3. the wallet holds long-only QQQ exposure
```
The short wrapper sells the L leg instead. The amounts use QQQ's devnet state on 10 Oct 2026, where the 27 bps mint fee leaves 99.73 cUSDC split evenly between the legs.
Mint, then sell the unwanted leg with `commit_swap`. A committed order fills at NAV ± `max(commit_spread_bps, 10)` bps at a keeper price push after the commit. The keeper posts the spread as `clamp(half-spread, 10, 500)` bps: 18 for QQQ, 15 for SPY and 18 for XAU as of 10 Oct 2026. Settlement waits while the market is below its 100% collateral floor or the price fails the oracle checks, and reverts until the keeper provisions the CLP's inventory. An unfilled order expires after 180 s, and the keeper refunds it about 5 s later; anyone can send that `cancel_swap`, and the escrow goes back to the user. `mint_paired` and `pool_swap` can't share a transaction: mint needs a price at most 300 s old, and the bin books fill only once it is older than that.
Committed orders work on every live market. The bin books ignore swap guards but fill only while a market's price is stale.
## 2. Arb bot
Every venue prices off the same keeper-posted price. Mint and redeem quote worst-case NAVs (every market except QQQ, where worst-case quoting is switched off as of 10 Oct 2026), committed orders fill at NAV ± the committed spread, and the bin books sit around an inventory-shifted centre. That leaves little to arbitrage:
```
loop:
for each (market, side) book:
nav = NAV from the Market account (see /build/reading-prices)
centre = Pool.base_price_1e6
if the price is stale (the book is open) and a move elsewhere left in-the-money bins:
take them with pool_swap (no privilege needed)
close the position with the paired round trip (mint or redeem)
```
What to know before building one:
* The books fill only while the market's price is stale. The window lasts until the keeper's guard pulls the stale side or a fresh push closes the book, and the take is capped by the depth of the in-the-money bins.
* Closing a one-sided position needs the paired round trip. Mint and redeem reopen only with the next fresh price, so the position carries price risk until then. The round trip's hurdle is the mint or redeem fee (posted at 10 to 39 bps as of 13 Oct 2026, then multiplied by 1.5 in ProxyMode) plus the book's spread. Near NAV it rarely clears.
* `settle_swap` is permissionless: anyone can crank a pending committed order, for no reward. It still depends on the keeper, because an order settles only at a price push newer than its commit. If the keeper stops pushing, orders expire after 180 s, and anyone can then send `cancel_swap` to return the escrow to its owner.
A settlement watchdog is more useful here than an arb engine: crank pending orders after each push, and refund orders that have expired.
## 3. Structured products
Combine positions across markets to express more specific views.
### Dispersion: long index, short components
```
Buy QQQ long (mint the pair, sell the S leg)
Buy NVDA short (mint the pair, sell the L leg)
Buy AAPL short
Buy TSLA short
```
This pays off when the index rises while the named components lag.
The product can be an off-chain orchestrator: a service that mints and sells across markets when the user clicks once. It can also be an on-chain wrapper program that does everything in one instruction through [CPI](/build/cpi). The wrapper is atomic but needs many more accounts.
### Holding the pair
```
Hold QQQ L and QQQ S as minted
Small moves: the pair's value barely changes
Large moves either way: the pair gains, because L + S = P + K·γ / P is convex in P
```
The convexity has a cost. The keeper lowers `s_gamma_index` (γ) in step with the market's realized variance, and `set_s_gamma_index` can only lower it, so the S leg loses value over time even if the price comes back. A pair gains from large moves and pays that decay while it waits for them.
### Equity against the dollar
```
Long S&P 500 long SPY L
Short USD long EUR L + long JPY S
```
EUR and GBP track EUR/USD and GBP/USD, so their L tokens are long the foreign currency. JPY tracks USD/JPY (yen per dollar), so its L token is long the dollar and its S token is short the dollar.
## 4. Portfolio analytics
L and S NAVs are on chain, so valuing a wallet needs no outside feed. Read the wallet's balances and multiply by NAV:
```typescript theme={null}
import { Connection, PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddressSync, unpackAccount } from "@solana/spl-token";
// `markets` are fetched Market accounts; prices() is from /build/reading-prices.
async function portfolioValue(connection: Connection, owner: PublicKey, markets: any[]) {
const now = Math.floor(Date.now() / 1000);
const atas: PublicKey[] = markets.flatMap((m) =>
[m.longMint, m.shortMint].map((mint) => getAssociatedTokenAddressSync(mint, owner, true)));
const infos = await connection.getMultipleAccountsInfo(atas); // one RPC call
const balance = (i: number) => {
const info = infos[i];
return info ? unpackAccount(atas[i]!, info).amount : 0n;
};
let total = 0n;
markets.forEach((m, i) => {
const p = prices(m, now);
total += (balance(2 * i) * p.nav.l + balance(2 * i + 1) * p.nav.s) / 1_000_000n;
});
return total; // cUSDC value at NAV, 6 decimals
}
```
Use `p.redeem` instead of `p.nav` for what the pairs would pay out now. Treat the result as the keeper's number: `keeper_authority` writes the price, and nothing on chain checks it against an independent source today.
## 5. Custodial UX
A custodian (a wallet provider, broker or fintech app) takes user deposits and:
1. Mints Continuum positions on the user's behalf.
2. Shows them as "QQQ exposure" without exposing the paired primitive.
3. Absorbs fees and slippage itself.
The user sees "100 USD long QQQ, currently \$103.40" in the app, and the custodian handles the on-chain steps. The custodial path is regulatory-sensitive. The protocol allows it but doesn't depend on it.
## 6. Borrowing against L tokens (mainnet only)
Continuum has no margin or borrowing. A lending market that accepted L tokens as collateral would allow a loop:
```
1. Mint a QQQ pair → QQQ L + QQQ S
2. Sell QQQ S → QQQ L + cUSDC
3. Borrow against QQQ L → more cash to mint with
4. Mint again → repeat
```
No lending market lists Continuum tokens today, and devnet tokens can't be used on mainnet protocols such as Kamino. Each loop would pay the mint fee, the book's spread and the borrow rate. The liquidation risk would sit with the lender; Continuum positions themselves can't be liquidated. Don't offer this to retail users without disclosure.
## 7. Vault that rebalances
A program takes cUSDC deposits, mints a target basket across markets, rebalances with mint and redeem, and charges a management fee. Build it as an Anchor program that holds positions in a PDA and calls Continuum by [CPI](/build/cpi).
Schedule rebalances inside market hours. Mint and redeem revert once a price is 300 s old, and Stress blocks both by default.
## 8. Cross-protocol routing
A user who wants short QQQ exposure has two paths inside Continuum:
1. Mint a pair and sell the L leg with a committed order.
2. Buy QQQ S with a committed order while QQQ is open, or on the bin book with `pool_swap` while its price is stale.
A router can quote both, along with any outside venue, and take the cheaper.
## 9. Pre-trade quotes
Quote a mint before the user signs, with the worst-case NAVs and the fee multiplier that `mint_paired` applies:
```typescript theme={null}
// prices() and stateMult() are from /build/reading-prices.
function simulateMint(m: any, amount: bigint, now: number) {
const p = prices(m, now);
const rf = m.riskFeeConfig;
const feeBps = (BigInt(m.mintFeeBps) *
stateMult(m, 100, rf.proxyFeeMultiplier, rf.stressFeeMultiplier, now)) / 100n;
const net = amount - (amount * feeBps) / 10_000n;
const half = net / 2n;
return {
fresh: p.fresh,
feeBps,
l: (half * 1_000_000n) / p.mint.l,
s: ((net - half) * 1_000_000n) / p.mint.s,
};
}
```
For QQQ on 10 Oct 2026, `simulateMint(m, 100_000_000n, now)` returns 0.066374 L and 0.163706 S at 27 bps. The transaction mints the same amounts unless the keeper posts a new price or fee, or the risk state changes, before it lands. Show no quote when `fresh` is false: the program rejects prices more than 300 s old.
## 10. Data product
Continuum's activity is on chain. An indexer can follow:
* mint-redeem's `PairedActionExecuted` events: every mint and redeem, with its fee and dev tax.
* The CLP's `SwapExecuted`, `OrderCommitted`, `OrderSettled`, `OrderRefunded` and `OrderCancelled` events.
* `update_risk_state_v2` price pushes, which carry the source's publish time.
* The `RevenueAttribution` account, which splits fees and volume between the keeper and external traders.
A history of book depth, NAV and spreads is a useful dataset for trading and analytics firms.
## Patterns we like
* Use the Market price as the price of record. Every mint, redeem and committed order uses it, and the keeper writes it.
* Route by market state. While a market's price is live, send trades to `commit_swap`, which fills at NAV ± the committed spread within the CLP's inventory and the 100% collateral floor. While it is stale, `pool_swap` fills small trades instantly. Mint or redeem when you want the pair itself.
* Watch the risk state. Stress blocks mint and redeem by default, so show it to the user.
* Plan for gaps in keeper pushes. After 300 s without a push, mint and redeem revert with `TwapNotAvailable` and committed orders can't settle. That happens whenever the underlying market is closed. The bin books open then.
## Patterns to avoid
* Don't pre-mint and hold inventory. Positions track NAV, so mint when you need them; the S leg also loses value to γ while it sits.
* Don't assume markets move together. Each market prices from its own feed, so treat them independently in basket math.
* Don't skip ATA creation. Idempotent creation costs about 0.002 SOL of rent, once per user and mint.
* Don't count on the OI cap. Each CLP stores an `oi_cap` (250,000 cUSDC per market on 10 Oct 2026), but no user instruction enforces it. It is checked only in the authority-only `update_oi`, which nothing calls. Size is limited by book depth, CLP inventory and the collateral floor.
## Talk to us
If you're building on Continuum, drop a note in [Discord](https://discord.gg/U5hPCB8ufJ). We can help with the integration, and mainnet partner integrations get priority listing slots.
## See also
On-chain integration via Cross-Program Invocation.
Pull all the data your integration needs.
# CPI integration
Source: https://docs.continuum.markets/build/cpi
Call mint-redeem from your own Anchor program with declare_program! and the published IDL, read the Market account, and check price freshness.
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`, `commit_swap` and `settle_swap` in the CLP program are public too, as is `cancel_swap` once an order has expired. 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
```bash theme={null}
mkdir -p idls
curl -so idls/mint_redeem.json \
https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/mint_redeem.json
```
`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/`.
```toml theme={null}
[dependencies]
anchor-lang = "0.32.1"
anchor-spl = "0.32.1"
```
## Declare the program
```rust theme={null}
use anchor_lang::prelude::*;
use anchor_spl::token::Token;
// Reads idls/mint_redeem.json at compile time and generates
// mint_redeem::{accounts, cpi, program, types, ...}.
declare_program!(mint_redeem);
use mint_redeem::{accounts::Market, program::MintRedeem};
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); // your program ID
/// mint-redeem reverts with TwapNotAvailable past this age (seconds).
pub const MAX_PRICE_AGE: i64 = 300;
pub fn check_fresh(market: &Market) -> Result<()> {
let age = Clock::get()?.unix_timestamp - market.twap_updated_at;
require!(
market.user_twap_price > 0 && age <= MAX_PRICE_AGE,
VaultError::StalePrice
);
Ok(())
}
#[error_code]
pub enum VaultError {
#[msg("Continuum price is zero or older than 300 s")]
StalePrice,
#[msg("Long price is below the target")]
BelowTarget,
#[msg("Account is not owned by mint-redeem")]
WrongOwner,
}
```
`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.
```rust theme={null}
#[derive(Accounts)]
pub struct PairAccounts<'info> {
/// CHECK: PDA that owns the vault's token accounts and signs the CPI.
#[account(mut, seeds = [b"vault"], bump)]
pub vault: UncheckedAccount<'info>,
#[account(mut)]
pub market: Box>,
/// CHECK: cUSDC account owned by `vault`; mint-redeem checks the mint.
#[account(mut)]
pub vault_collateral: UncheckedAccount<'info>,
/// CHECK: L token account owned by `vault`; mint-redeem checks the mint.
#[account(mut)]
pub vault_long: UncheckedAccount<'info>,
/// CHECK: S token account owned by `vault`; mint-redeem checks the mint.
#[account(mut)]
pub vault_short: UncheckedAccount<'info>,
/// CHECK: mint-redeem requires market.long_mint.
#[account(mut)]
pub long_mint: UncheckedAccount<'info>,
/// CHECK: mint-redeem requires market.short_mint.
#[account(mut)]
pub short_mint: UncheckedAccount<'info>,
/// CHECK: mint-redeem requires market.collateral_vault.
#[account(mut)]
pub collateral_vault: UncheckedAccount<'info>,
/// CHECK: mint-redeem requires a cUSDC account owned by market.fee_recipient.
#[account(mut)]
pub dev_token_account: UncheckedAccount<'info>,
/// CHECK: mint-redeem requires market.oracle_address.
pub oracle: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub mint_redeem_program: Program<'info, MintRedeem>,
}
```
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
```rust theme={null}
#[program]
pub mod my_vault {
use super::*;
/// Mint a pair from cUSDC held by the vault PDA.
pub fn mint_from_vault(ctx: Context, amount: u64) -> Result<()> {
check_fresh(&ctx.accounts.market)?;
let a = &ctx.accounts;
let cpi_accounts = mint_redeem::cpi::accounts::MintPaired {
market: a.market.to_account_info(),
user: a.vault.to_account_info(),
user_collateral: a.vault_collateral.to_account_info(),
user_long_account: a.vault_long.to_account_info(),
user_short_account: a.vault_short.to_account_info(),
long_mint: a.long_mint.to_account_info(),
short_mint: a.short_mint.to_account_info(),
collateral_vault: a.collateral_vault.to_account_info(),
dev_token_account: a.dev_token_account.to_account_info(),
oracle: a.oracle.to_account_info(),
token_program: a.token_program.to_account_info(),
};
let seeds: &[&[u8]] = &[b"vault", &[ctx.bumps.vault]];
let signer = &[seeds];
mint_redeem::cpi::mint_paired(
CpiContext::new_with_signer(a.mint_redeem_program.to_account_info(), cpi_accounts, signer),
amount,
)
}
/// Redeem the vault's pair once the long price reaches a target.
pub fn redeem_from_vault(
ctx: Context,
l_amount: u64,
s_amount: u64,
min_l_price: u64, // 1e6, like Market.user_twap_price
) -> Result<()> {
check_fresh(&ctx.accounts.market)?;
require!(
ctx.accounts.market.user_twap_price >= min_l_price,
VaultError::BelowTarget
);
let a = &ctx.accounts;
let cpi_accounts = mint_redeem::cpi::accounts::RedeemPaired {
market: a.market.to_account_info(),
user: a.vault.to_account_info(),
user_collateral: a.vault_collateral.to_account_info(),
user_long_account: a.vault_long.to_account_info(),
user_short_account: a.vault_short.to_account_info(),
long_mint: a.long_mint.to_account_info(),
short_mint: a.short_mint.to_account_info(),
collateral_vault: a.collateral_vault.to_account_info(),
dev_token_account: a.dev_token_account.to_account_info(),
oracle: a.oracle.to_account_info(),
token_program: a.token_program.to_account_info(),
// Ignored slot. None sends mint-redeem's program ID.
collateral_authority: None,
};
let seeds: &[&[u8]] = &[b"vault", &[ctx.bumps.vault]];
let signer = &[seeds];
mint_redeem::cpi::redeem_paired(
CpiContext::new_with_signer(a.mint_redeem_program.to_account_info(), cpi_accounts, signer),
l_amount,
s_amount,
)
}
}
```
`redeem_paired` requires both amounts to be above 0 (there is no single-leg redeem) and a redeem value of at least 10 cUSDC. 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:
```rust theme={null}
mint_redeem::cpi::mint_paired(
CpiContext::new(a.mint_redeem_program.to_account_info(), cpi_accounts),
amount,
)
```
## Reading `Market` from a raw account
If you receive the market as an `AccountInfo`, deserialize the whole buffer:
```rust theme={null}
/// Deserialize a Market passed as a raw AccountInfo.
pub fn load_market(info: &AccountInfo) -> Result {
require_keys_eq!(*info.owner, mint_redeem::ID, VaultError::WrongOwner);
let data = info.try_borrow_data()?;
// Pass the whole buffer: try_deserialize checks the 8-byte discriminator.
Market::try_deserialize(&mut &data[..])
}
```
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>`) 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:
```typescript theme={null}
import { ComputeBudgetProgram } from "@solana/web3.js";
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 }));
```
### 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
Off-chain integration patterns: wrappers, bots, structured products.
Authority model: who can call what.
# Mint and redeem (full example)
Source: https://docs.continuum.markets/build/example-mint-redeem
A complete TypeScript script that drips cUSDC, checks the price, mints a paired QQQ position and redeems it on devnet.
This Bun script runs a full paired cycle on devnet. It drips cUSDC from the faucet, checks the market's price age, estimates the fill, mints a QQQ pair with `mint_paired` and redeems it with `redeem_paired`. It needs a funded devnet wallet and nothing else.
## Prerequisites
* A devnet wallet at `~/.config/solana/id.json` (or set `WALLET_PATH`) with some SOL for fees and token-account rent. 0.05 SOL is plenty.
* The packages from [SDK setup](/build/sdk-setup): `bun add @coral-xyz/anchor@0.32 @solana/web3.js@1 @solana/spl-token`.
* An open underlying market. The keeper stops posting a market's price while its underlying market is closed, and mint and redeem revert once the price is more than 300 s old.
```bash theme={null}
solana airdrop 1 --url devnet
```
## The script
```typescript theme={null}
// mint-and-redeem.ts
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import { AnchorProvider, BN, Program, Wallet, type Idl } from "@coral-xyz/anchor";
import {
TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
} from "@solana/spl-token";
import fs from "fs";
import os from "os";
import path from "path";
const RPC_URL = process.env.RPC_URL ?? "https://api.devnet.solana.com";
const WALLET_PATH =
process.env.WALLET_PATH ?? path.join(os.homedir(), ".config/solana/id.json");
const IDL_BASE =
"https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl";
const SYMBOL = process.env.SYMBOL ?? "QQQ";
const CUSDC_MINT = new PublicKey("B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE");
const AMOUNT = 100_000_000n; // 100 cUSDC, 6 decimals
const MAX_AGE = 300; // the programs reject prices older than this (seconds)
const MARGIN = 30; // time left for the transaction to land
const E6 = 1_000_000n;
const ONE = 1_000_000_000n; // γ scale
const big = (x: { toString(): string }) => BigInt(x.toString());
const fmt = (x: bigint) => (Number(x) / 1e6).toFixed(6);
async function loadIdl(name: string): Promise {
const res = await fetch(`${IDL_BASE}/${name}.json`);
if (!res.ok) throw new Error(`IDL ${name}: HTTP ${res.status}`);
return (await res.json()) as Idl;
}
// Risk-state multiplier in percent (150 = 1.5x). Recovery falls linearly
// from the Stress value to the Normal value over one hour.
function stateMult(m: any, normal: number, proxy: number, stress: number, now: number) {
const state = Object.keys(m.riskState)[0];
if (state === "normal") return BigInt(normal);
if (state === "proxyMode") return BigInt(proxy);
if (state === "stress") return BigInt(stress);
const t = Math.min(Math.max(now - m.riskStateUpdatedAt.toNumber(), 0), 3600);
return BigInt(stress - Math.floor(((stress - normal) * t) / 3600));
}
// Worst-case NAVs (1e6) and fee rates, computed the way mint-redeem does.
function quote(m: any, now: number) {
const P = big(m.userTwapPrice);
const K = big(m.initialLPrice) * big(m.initialSPrice);
const g = big(m.sGammaIndex);
const gamma = g === 0n || g > ONE ? ONE : g; // 0 means unset: read as 1.0
const sNav = (price: bigint) => ((K / price) * gamma) / ONE;
const wc = m.worstCaseConfig;
const c = wc.enabled
? (big(m.oracleConfidence) *
stateMult(m, wc.normalConfidenceMultiplier, wc.proxyConfidenceMultiplier,
wc.stressConfidenceMultiplier, now)) / 100n
: 0n;
const feeMult = stateMult(m, 100, m.riskFeeConfig.proxyFeeMultiplier,
m.riskFeeConfig.stressFeeMultiplier, now);
return {
gamma,
mint: { l: P + c, s: sNav(P - c), feeBps: (BigInt(m.mintFeeBps) * feeMult) / 100n },
redeem: { l: P - c, s: sNav(P + c), feeBps: (BigInt(m.redeemFeeBps) * feeMult) / 100n },
};
}
async function main() {
const connection = new Connection(RPC_URL, "confirmed");
const secret = JSON.parse(fs.readFileSync(WALLET_PATH, "utf8"));
const wallet = new Wallet(Keypair.fromSecretKey(Uint8Array.from(secret)));
const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" });
const user = wallet.publicKey;
// A fetched IDL has no static types, so the clients are `any` to TypeScript.
const [mr, faucet]: any[] = await Promise.all(
["mint_redeem", "faucet"].map(async (n) => new Program(await loadIdl(n), provider)));
// 1. Load the market and check the price before spending anything
const [market] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from(SYMBOL)], mr.programId);
const m = await mr.account.market.fetch(market);
const now = Math.floor(Date.now() / 1000);
const age = now - m.twapUpdatedAt.toNumber();
const state = Object.keys(m.riskState)[0];
console.log(`${SYMBOL} risk=${state} price=${fmt(big(m.userTwapPrice))} age=${age}s`);
if (m.userTwapPrice.isZero() || age > MAX_AGE - MARGIN) {
throw new Error(`price is ${age}s old; mint and redeem revert after ${MAX_AGE}s`);
}
if (state === "stress" &&
!(m.riskFeeConfig.allowMintInStress && m.riskFeeConfig.allowRedeemInStress)) {
throw new Error("market is in Stress; mint and redeem are blocked");
}
// 2. Estimate the mint at the worst-case NAVs
const q = quote(m, now);
const fee = (AMOUNT * q.mint.feeBps) / 10_000n;
const half = (AMOUNT - fee) / 2n;
console.log(`mint NAVs L ${fmt(q.mint.l)} S ${fmt(q.mint.s)} ` +
`gamma ${(Number(q.gamma) / 1e9).toFixed(9)} fee ${q.mint.feeBps} bps`);
console.log(`expect ~${fmt((half * E6) / q.mint.l)} L + ` +
`${fmt(((AMOUNT - fee - half) * E6) / q.mint.s)} S`);
// 3. Token accounts. The fee recipient can be a PDA, hence `true`.
const userCollateral = getAssociatedTokenAddressSync(CUSDC_MINT, user);
const userLongAccount = getAssociatedTokenAddressSync(m.longMint, user);
const userShortAccount = getAssociatedTokenAddressSync(m.shortMint, user);
const devTokenAccount = getAssociatedTokenAddressSync(CUSDC_MINT, m.feeRecipient, true);
const collateralAtaIx =
createAssociatedTokenAccountIdempotentInstruction(user, userCollateral, user, CUSDC_MINT);
const ataIxs = [
collateralAtaIx,
createAssociatedTokenAccountIdempotentInstruction(user, userLongAccount, user, m.longMint),
createAssociatedTokenAccountIdempotentInstruction(user, userShortAccount, user, m.shortMint),
createAssociatedTokenAccountIdempotentInstruction(
user, devTokenAccount, m.feeRecipient, CUSDC_MINT),
];
const balance = (a: PublicKey) =>
connection.getTokenAccountBalance(a).then((r) => BigInt(r.value.amount), () => 0n);
// 4. Drip 1,000 cUSDC if the wallet is short (one drip per wallet per 24 h)
if ((await balance(userCollateral)) < AMOUNT) {
const dripIx = await faucet.methods
.drip()
.accounts({ mint: CUSDC_MINT, recipient: user, recipientAta: userCollateral })
.instruction(); // faucetState, mintAuthority and dripRecord are derived PDAs
const sig = await provider.sendAndConfirm(new Transaction().add(collateralAtaIx, dripIx));
console.log(`drip ${sig}`);
}
// 5. Mint the pair
const accounts = {
market, user, userCollateral, userLongAccount, userShortAccount,
longMint: m.longMint, shortMint: m.shortMint,
collateralVault: m.collateralVault, devTokenAccount,
oracle: m.oracleAddress, tokenProgram: TOKEN_PROGRAM_ID,
};
const [l0, s0] = await Promise.all([balance(userLongAccount), balance(userShortAccount)]);
const mintIx = await mr.methods
.mintPaired(new BN(AMOUNT.toString()))
.accounts(accounts)
.instruction();
const mintSig = await provider.sendAndConfirm(new Transaction().add(...ataIxs, mintIx));
const [l1, s1] = await Promise.all([balance(userLongAccount), balance(userShortAccount)]);
console.log(`mint_paired ${mintSig}`);
console.log(` +${fmt(l1 - l0)} ${SYMBOL} L +${fmt(s1 - s0)} ${SYMBOL} S`);
// 6. Redeem what was just minted. Both amounts must be > 0.
const nominal = ((l1 - l0) * q.redeem.l) / E6 + ((s1 - s0) * q.redeem.s) / E6;
console.log(`expect ~${fmt(nominal - (nominal * q.redeem.feeBps) / 10_000n)} cUSDC back`);
const redeemIx = await mr.methods
.redeemPaired(new BN((l1 - l0).toString()), new BN((s1 - s0).toString()))
.accounts({ ...accounts, collateralAuthority: null }) // ignored slot; null sends mint-redeem's program ID
.instruction();
const c0 = await balance(userCollateral);
const redeemSig = await provider.sendAndConfirm(new Transaction().add(redeemIx));
console.log(`redeem_paired ${redeemSig}`);
console.log(` +${fmt((await balance(userCollateral)) - c0)} cUSDC`);
}
main().catch((e) => { console.error(e); process.exit(1); });
```
## Run
```bash theme={null}
bun mint-and-redeem.ts # QQQ
SYMBOL=EUR bun mint-and-redeem.ts # any live market
```
Expected output for QQQ's devnet state on 10 Oct 2026 (price \$751.27, γ 0.993214606, posted fee 18 bps). Signatures are elided:
```
QQQ risk=proxyMode price=751.270000 age=6s
mint NAVs L 751.270000 S 304.599737 gamma 0.993214606 fee 27 bps
expect ~0.066374 L + 0.163706 S
drip
mint_paired
+0.066374 QQQ L +0.163706 QQQ S
expect ~99.460329 cUSDC back
redeem_paired
+99.460329 cUSDC
```
When the underlying market is closed, the script stops at step 1. On Saturday 10 Oct 2026 it printed this (Bun's source excerpt and stack trace trimmed):
```
QQQ risk=proxyMode price=751.270000 age=84194s
error: price is 84194s old; mint and redeem revert after 300s
```
## What just happened
* Step 1 read the `Market` account and stopped if the price was older than 270 s or the market was in Stress. The 270 s is the program's 300 s limit, minus 30 s for the transaction to land. Stress blocks mint and redeem on every live market, because `allow_mint_in_stress` and `allow_redeem_in_stress` are false.
* Step 2 priced the mint the way `mint_paired` does. L costs P + c and S costs K / (P − c) × γ, where P is `user_twap_price`, K is `initial_l_price × initial_s_price`, and c is `oracle_confidence` times the risk-state multiplier (1.5× in ProxyMode). QQQ is the one market with worst-case quoting switched off (`worst_case_config.enabled` is false as of 10 Oct 2026), so its c is 0 and both legs price at NAV. The fee is the posted 18 bps times the same 1.5× multiplier: 27 bps.
* Step 4 dripped 1,000 cUSDC because the wallet held less than 100. The faucet allows one drip per wallet every 24 hours.
* Step 5 deposited 100 cUSDC. The program kept a 0.27 cUSDC fee and minted 0.066374 L and 0.163706 S from the other 99.73 cUSDC, half to each leg. A tenth of the fee (`dev_tax_pct` = 10) went to the fee recipient; the rest stayed in the vault.
* Step 6 burned both legs and paid 99.460329 cUSDC after the 27 bps redeem fee. The round trip cost 0.539671 cUSDC, about 54 bps.
The estimates match the program exactly while the market is over-collateralized. Below a 100% collateral ratio, `redeem_paired` pays pro rata (`nominal × total_collateral / total_claims`), and the redeem estimate comes out high.
## Redeeming one leg
There is no single-leg redeem. `redeem_paired` requires `l_amount > 0` and `s_amount > 0`, and fails with `InvalidAmount` otherwise. To exit a single leg, sell it through a committed order while the market is open, or on the bin book with `pool_swap` while its price is stale. You can also buy the other leg and redeem the pair. See [Trade flow](/flows/trade).
## Common errors
| Error | Cause and fix |
| - | - |
| `TwapNotAvailable` (6012) | The price is 0 or more than 300 s old. The keeper doesn't post while the underlying market is closed; retry during market hours. |
| `BelowMinimum` (6019) | The mint amount, or the value of a redeem, is under 10 cUSDC. |
| `InvalidAmount` (6001) | A redeem amount is 0 (both legs must be above 0), an amount exceeds the market's supply, or a token account has the wrong mint. |
| `MintNotAllowedInStress` (6009), `RedeemNotAllowedInStress` (6010) | The market is in Stress, which blocks both by default. Wait until it leaves Stress. |
| `Unauthorized` (6021) | `devTokenAccount` isn't owned by `market.feeRecipient`. |
| `InsufficientLiquidCollateral` (6022) | A redeem pays more than the market's liquid collateral (`total_collateral` minus `deployed_to_yield`) or the vault holds. Send `request_redemption` with the same amounts, or retry after the keeper recalls from lending. See [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet). |
| SPL Token `insufficient funds` (custom program error `0x1`) | The wallet holds less cUSDC than the mint amount, or fewer L or S tokens than the redeem amounts. |
| `AccountNotInitialized` (3012) | A token account doesn't exist yet. The script creates all four idempotently before minting. |
| `CooldownNotElapsed` (faucet, 6000) | The wallet already dripped in the last 24 hours. |
→ [Full error catalog](/reference/errors)
## Extending the example
* Mint in several markets at once by adding one `mintPaired` instruction per market to the same transaction, within Solana's 1,232-byte transaction size limit.
* Refresh estimates live: subscribe to the market account with `connection.onAccountChange(market, ...)` and run `quote()` on each update.
* Take a directional position: after step 5, sell the leg you don't want through a committed order while the market is open ([Trade flow](/flows/trade)).
## See also
Read the price, confidence and γ from the Market account.
Use cases: wrappers, bots, structured products, integrations.
# IDLs and PDAs
Source: https://docs.continuum.markets/build/idls-and-pdas
Download the published IDLs, derive every PDA the six programs use, and compose the mint and redeem account lists.
A cheat sheet: where the IDLs live, how to derive each PDA, and how to build the mint and redeem account lists.
## IDL files
The program source is private. The IDLs, generated from the source of the deployed programs, are published in the docs repository ([browse](https://github.com/continuum-markets/continuum-docs/tree/main/idl)).
| Program | IDL | Program ID (devnet) |
| - | - | - |
| `mint-redeem` | [`idl/mint_redeem.json`](https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/mint_redeem.json) | `5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4` |
| `clp` | [`idl/clp.json`](https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/clp.json) | `8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES` |
| `oracle` | [`idl/oracle.json`](https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/oracle.json) | `5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC` |
| `registry` | [`idl/registry.json`](https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/registry.json) | `Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau` |
| `faucet` | [`idl/faucet.json`](https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/faucet.json) | `9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY` |
| `trading` | [`idl/trading.json`](https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/trading.json) | `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA` |
```bash theme={null}
mkdir -p idl types
for name in mint_redeem clp oracle registry faucet trading; do
curl -fsSL -o "idl/$name.json" \
"https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/$name.json"
done
# Optional: TypeScript types for typed account access (Anchor CLI, no source needed)
anchor idl type idl/mint_redeem.json -o types/mint_redeem.ts
anchor idl type idl/clp.json -o types/clp.ts
```
Each IDL's `address` field holds the program ID, and Anchor 0.32 reads it from there. Three details trip people up:
* Pass the type explicitly, as in `new Program(idl as MintRedeem, provider)`. Without it TypeScript falls back to the generic `Idl` type and `program.account.market` doesn't type-check.
* Anchor TS decodes `base_price_1e6`, `gamma_day_decay_1e9` and `short_payoff_cap_c_1e9` as `basePrice1E6`, `gammaDayDecay1E9` and `shortPayoffCapC1E9`, but `anchor idl type` names them with a lowercase `e`. Read those three fields by their runtime names.
* The registry IDL has no PDA seeds, so Anchor can't fill in `registry` or `marketEntry` for you. Pass them explicitly, using the seeds below.
## PDA derivation cheatsheet
Every PDA is `PublicKey.findProgramAddressSync(seeds, programId)`. This script derives them all for one market and one wallet, then checks them with a single `getMultipleAccountsInfo` call. It needs only `@solana/web3.js`. Save it as `pdas.ts`, set your wallet, and run `npx tsx pdas.ts`.
```typescript theme={null}
import { Connection, PublicKey } from "@solana/web3.js";
const MINT_REDEEM = new PublicKey("5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4");
const CLP = new PublicKey("8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES");
const ORACLE = new PublicKey("5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC");
const REGISTRY = new PublicKey("Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau");
const FAUCET = new PublicKey("9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY");
const CUSDC_MINT = new PublicKey("B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE");
const SYMBOL = "QQQ";
const wallet = new PublicKey("YOUR_WALLET_PUBKEY");
const nonce = 1n; // the u64 nonce you pass to commit_swap
const pda = (seeds: Buffer[], programId: PublicKey) =>
PublicKey.findProgramAddressSync(seeds, programId)[0];
const seed = (s: string) => Buffer.from(s);
const u64le = (n: bigint) => {
const b = Buffer.alloc(8);
b.writeBigUInt64LE(n);
return b;
};
// Registry seeds use the symbol uppercased and zero-padded to 16 bytes.
const registrySymbol = (s: string) => {
const b = Buffer.alloc(16);
b.write(s.toUpperCase(), "ascii");
return b;
};
const market = pda([seed("market"), seed(SYMBOL)], MINT_REDEEM);
const order = pda(
[seed("order"), wallet.toBuffer(), market.toBuffer(), u64le(nonce)],
CLP,
);
const addresses: Record = {
// mint-redeem
market,
feeWaiver: pda([seed("fee_waiver"), wallet.toBuffer(), market.toBuffer()], MINT_REDEEM),
// oracle
oracleConfig: pda([seed("oracle_config"), market.toBuffer()], ORACLE),
// clp
globalClp: pda([seed("global_clp")], CLP),
treasuryPolicy: pda([seed("treasury_policy")], CLP),
clp: pda([seed("clp"), market.toBuffer()], CLP),
longPool: pda([seed("pool"), market.toBuffer(), Buffer.from([0])], CLP),
shortPool: pda([seed("pool"), market.toBuffer(), Buffer.from([1])], CLP),
revenueAttribution: pda([seed("revenue_attribution")], CLP),
order,
escrow: pda([seed("escrow"), order.toBuffer()], CLP),
// faucet
faucetState: pda([seed("faucet"), CUSDC_MINT.toBuffer()], FAUCET),
faucetMintAuthority: pda([seed("mint_authority"), CUSDC_MINT.toBuffer()], FAUCET),
dripRecord: pda([seed("drip"), CUSDC_MINT.toBuffer(), wallet.toBuffer()], FAUCET),
// registry
registryState: pda([seed("registry")], REGISTRY),
registryEntry: pda([seed("market"), registrySymbol(SYMBOL)], REGISTRY),
};
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const names = Object.keys(addresses);
const infos = await connection.getMultipleAccountsInfo(names.map((n) => addresses[n]));
names.forEach((name, i) => {
const info = infos[i];
const status = info ? `${info.data.length} bytes` : "no account";
console.log(`${name.padEnd(20)} ${addresses[name].toBase58().padEnd(44)} ${status}`);
});
```
Output for QQQ on devnet (11 Oct 2026); the wallet-specific rows depend on the wallet you pass:
```
market F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5 891 bytes
feeWaiver (wallet-specific) no account
oracleConfig 7uMgq2tpFW9FatCwAPtAjndvADCr3p8NiNFo7EZLSaV1 1781 bytes
globalClp BcGR8kuGCiMhwonyztXZHEZtjJXxXVRgjwbn58bUfXtw 536 bytes
treasuryPolicy 58JeHAcMAAyEZWusVqmvoFNVqh37cTkmfbbP6bMHExWR 210 bytes
clp EEDKqU3C4wgWQBcgFagHTqEAjKNMCV4eVYyaUt9swKHc 544 bytes
longPool DbPryNwJ2ThJPTzb7UMBoEQhR2QEqcHxenGA8JCqG9Qr 422 bytes
shortPool HUudWC8ykMCZg3CNs3TPhRmLczdrPVwmXXSYvvKAxKbB 422 bytes
revenueAttribution A8mK2pnagabEcnkSiWieEZyQjjNWFYm1Z91dUVMXe6a4 273 bytes
order (wallet-specific) no account
escrow (wallet-specific) no account
faucetState 9U5jPkt46MhtqGt22yWQuKTUDKNUZXEZBekJNJuSMt1N 98 bytes
faucetMintAuthority HGtoCZ8VWGJvW5yiBai7VZictfrs3h7tmvFFJUoumoNw no account
dripRecord (wallet-specific) 17 bytes
registryState ED8joCbjr7hnWPgmiGspQ2LNpUJGfjobZYqEHD1PWyE1 45 bytes
registryEntry G2QaqhBF1j1U9DA7s8sjuZJ3ysmPbEJbce9qaKRzzhX7 no account
```
Some rows can show "no account", and that is expected:
* `feeWaiver` exists only after the wallet has donated to that market with `donate_to_vault`.
* `order` and `escrow` exist only while that order is open. `settle_swap` and `cancel_swap` close both.
* `dripRecord` exists after the wallet's first faucet drip.
* `faucetMintAuthority` is a signing PDA that holds no data. It is the cUSDC mint's mint authority.
* `registryEntry` is empty because the Registry holds no markets yet. Get market addresses from [Addresses](/reference/addresses).
The `Market` account is allocated 891 bytes on devnet, and the struct can need up to 899. Its `collateral_vaults` list is variable-length, so read Market fields through the IDL, never at fixed offsets (see [Market account](/programs/mint-redeem#market-account)).
The two `market` seeds differ. mint-redeem uses the raw symbol bytes (`QQQ`), while the Registry uses the symbol uppercased and zero-padded to 16 bytes.
### Token accounts (ATAs)
Associated token accounts aren't PDAs of Continuum's programs, but you derive them constantly:
```typescript theme={null}
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
const userCusdc = getAssociatedTokenAddressSync(cusdcMint, owner);
const userLong = getAssociatedTokenAddressSync(longMint, owner);
const userShort = getAssociatedTokenAddressSync(shortMint, owner);
```
Pass `true` as the third argument when the owner can be a PDA, as `market.feeRecipient` can. As of 10 Oct 2026 QQQ's fee recipient is the `GlobalClp` PDA, and without the flag `getAssociatedTokenAddressSync` throws `TokenOwnerOffCurveError`.
## Composing a mint call
`mint_paired` takes 11 accounts. You derive the Market PDA from the symbol, fetch the market once, and derive the rest from its fields:
```typescript theme={null}
import { BN, Program } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import idl from "./idl/mint_redeem.json" with { type: "json" };
import type { MintRedeem } from "./types/mint_redeem";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const program = new Program(idl as MintRedeem, { connection });
const owner = new PublicKey("YOUR_WALLET_PUBKEY");
// 1. Derive the market PDA from the symbol.
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
program.programId,
);
// 2. Fetch the market (one RPC call).
const market = await program.account.market.fetch(marketPda);
// 3. Derive the rest from the market's fields.
const ata = (mint: PublicKey, holder: PublicKey) =>
getAssociatedTokenAddressSync(mint, holder, true); // the fee recipient can be a PDA
const accounts = {
market: marketPda,
user: owner,
userCollateral: ata(market.collateralMint, owner),
userLongAccount: ata(market.longMint, owner),
userShortAccount: ata(market.shortMint, owner),
longMint: market.longMint,
shortMint: market.shortMint,
collateralVault: market.collateralVault,
devTokenAccount: ata(market.collateralMint, market.feeRecipient),
oracle: market.oracleAddress,
tokenProgram: TOKEN_PROGRAM_ID,
};
const mintIx = await program.methods
.mintPaired(new BN(25_000_000)) // 25 cUSDC
.accountsStrict(accounts)
.instruction();
// redeem_paired ends with collateralAuthority, a slot the program ignores.
// Pass null, and Anchor sends mint-redeem's program ID.
const redeemIx = await program.methods
.redeemPaired(new BN(1_000_000), new BN(1_000_000)) // 1 L and 1 S
.accountsStrict({ ...accounts, collateralAuthority: null })
.instruction();
```
The user's cUSDC, L and S token accounts must exist before the call; otherwise Anchor fails with `AccountNotInitialized` (3012). Add `createAssociatedTokenAccountIdempotentInstruction` for each one to the same transaction. In production, batch the market fetch with the user's token-account reads through `getMultipleAccountsInfo`.
## Account-fetch helpers
With `mintRedeem` and `clp` as typed `Program` instances and the PDAs from the script above:
```typescript theme={null}
const market = await mintRedeem.account.market.fetch(marketPda);
const clpAccount = await clp.account.clp.fetch(clpPda);
const globalClp = await clp.account.globalClp.fetch(globalClpPda);
const longBook = await clp.account.pool.fetch(longPoolPda);
```
For several markets at once:
```typescript theme={null}
const markets = await mintRedeem.account.market.fetchMultiple(marketPdas);
// (Market | null)[], in the order of marketPdas
```
Don't fetch `OracleConfig` for prices: the oracle program isn't updated on devnet. The live price is `market.userTwapPrice`.
## Discriminators
Every Anchor account and instruction starts with an 8-byte discriminator: the first 8 bytes of `sha256("account:")` for accounts and `sha256("global:")` for instructions. The IDLs list them, and Anchor checks them for you. `Market`'s discriminator is `[219, 190, 213, 55, 0, 227, 198, 154]`.
To decode raw account data without `fetch`, use the program's coder with the camelCase account name:
```typescript theme={null}
const info = await connection.getAccountInfo(marketPda);
const decoded = program.coder.accounts.decode("market", info!.data);
```
## See also
Project bootstrap and a first read.
Mint and redeem in one script.
# Reading prices
Source: https://docs.continuum.markets/build/reading-prices
Read a market's price, confidence and γ from its Market account, compute L and S NAV and the worst-case quotes, and check freshness.
Mint, redeem and committed orders all price from the market's `Market` account in the mint-redeem program. Read that account; you don't need an off-chain feed.
| Field | Scale | Meaning |
| - | - | - |
| `user_twap_price` | 1e6 | The reference price P. |
| `twap_updated_at` | Unix seconds | When the keeper last wrote P. Mint and redeem revert 300 s after it. |
| `oracle_confidence` | 1e6 | The confidence band, before the risk-state multiplier. |
| `s_gamma_index` | 1e9 | γ, the volatility decay on the short leg. 0 means unset and reads as 1.0. |
| `initial_l_price`, `initial_s_price` | 1e6 | Their product is the constant K. |
| `worst_case_config`, `risk_fee_config`, `risk_state` | | Confidence and fee multipliers, and the current risk state. |
Despite its name, `user_twap_price` is a spot price, not a time-weighted average. The keeper writes it with mint-redeem's `update_risk_state_v2`, already adjusted for corporate actions, and the same call stamps `twap_updated_at`.
On devnet the keeper prices markets from a free public quote source, because Pyth's public Hermes endpoint now needs an API key. A GJR-GARCH volatility model sets the confidence band. The keeper checks every print against Hyperliquid and skips the push when the two disagree, so Hyperliquid can veto a price but never sets one. Mainnet will use Pyth (Pyth Pro). See [Oracle](/concepts/oracle).
## NAV and worst-case quotes
```
L NAV = P
S NAV = K / P × γ where K = initial_l_price × initial_s_price
```
Mint and redeem don't trade at NAV. They quote each leg against the user, with c = `oracle_confidence` × the confidence multiplier for the current risk state:
| | L | S |
| - | - | - |
| Mint (the user pays) | P + c | K / (P − c) × γ |
| Redeem (the user receives) | P − c | K / (P + c) × γ |
The multipliers live in `worst_case_config`: 1× in Normal, 1.5× in ProxyMode and 3× in Stress. In Recovery the multiplier falls from 3× to 1× over one hour. Every devnet market runs in ProxyMode. When `worst_case_config.enabled` is false, c is 0 and both legs trade at NAV; as of 10 Oct 2026 that is true for QQQ only. Fees are separate and use `risk_fee_config` (see [Mint and redeem example](/build/example-mint-redeem)).
## Sample
```typescript theme={null}
// read-prices.ts
import { Connection, PublicKey } from "@solana/web3.js";
import { Program, type Idl } from "@coral-xyz/anchor";
const IDL_URL =
"https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/mint_redeem.json";
const MAX_AGE = 300; // seconds; mint and redeem revert past this
const ONE = 1_000_000_000n; // γ scale
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const idl = (await (await fetch(IDL_URL)).json()) as Idl;
const mintRedeem: any = new Program(idl, { connection }); // fetched IDL: untyped
const big = (x: { toString(): string }) => BigInt(x.toString());
const fmt = (x: bigint) => (Number(x) / 1e6).toFixed(6);
// Risk-state multiplier in percent (150 = 1.5x). Recovery falls linearly
// from the Stress value to the Normal value over one hour.
function stateMult(m: any, normal: number, proxy: number, stress: number, now: number) {
const state = Object.keys(m.riskState)[0];
if (state === "normal") return BigInt(normal);
if (state === "proxyMode") return BigInt(proxy);
if (state === "stress") return BigInt(stress);
const t = Math.min(Math.max(now - m.riskStateUpdatedAt.toNumber(), 0), 3600);
return BigInt(stress - Math.floor(((stress - normal) * t) / 3600));
}
function prices(m: any, now: number) {
const P = big(m.userTwapPrice); // 1e6, corporate-action adjusted
const K = big(m.initialLPrice) * big(m.initialSPrice);
const g = big(m.sGammaIndex);
const gamma = g === 0n || g > ONE ? ONE : g; // 0 means unset: read as 1.0
const sNav = (p: bigint) => ((K / p) * gamma) / ONE;
const wc = m.worstCaseConfig;
const mult = stateMult(m, wc.normalConfidenceMultiplier, wc.proxyConfidenceMultiplier,
wc.stressConfidenceMultiplier, now);
const c = wc.enabled ? (big(m.oracleConfidence) * mult) / 100n : 0n;
const age = now - m.twapUpdatedAt.toNumber();
return {
P, c, gamma, age, fresh: P > 0n && age <= MAX_AGE,
nav: { l: P, s: sNav(P) },
mint: { l: P + c, s: sNav(P - c) }, // what a minter pays per token
redeem: { l: P - c, s: sNav(P + c) }, // what a redeemer receives per token
};
}
const symbols = ["QQQ", "SPY", "EUR"];
const pdas = symbols.map((s) => PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from(s)], mintRedeem.programId)[0]);
const markets = await mintRedeem.account.market.fetchMultiple(pdas);
const now = Math.floor(Date.now() / 1000);
for (let i = 0; i < symbols.length; i++) {
const m = markets[i];
if (!m) continue;
const p = prices(m, now);
console.log(`${symbols[i]} ${Object.keys(m.riskState)[0]} age ${p.age}s fresh ${p.fresh}`);
console.log(` P ${fmt(p.P)} c ${fmt(p.c)} gamma ${(Number(p.gamma) / 1e9).toFixed(9)}`);
console.log(` NAV L ${fmt(p.nav.l)} S ${fmt(p.nav.s)}`);
console.log(` mint L ${fmt(p.mint.l)} S ${fmt(p.mint.s)}`);
console.log(` redeem L ${fmt(p.redeem.l)} S ${fmt(p.redeem.s)}`);
}
```
Output on Saturday 10 Oct 2026 at 19:32 UTC. The underlying markets were closed, so every price was stale:
```
QQQ proxyMode age 83828s fresh false
P 751.270000 c 0.000000 gamma 0.993214606
NAV L 751.270000 S 304.599737
mint L 751.270000 S 304.599737
redeem L 751.270000 S 304.599737
SPY proxyMode age 84128s fresh false
P 778.569999 c 0.184738 gamma 0.998541030
NAV L 778.569999 S 655.662481
mint L 778.754737 S 655.818093
redeem L 778.385261 S 655.506943
EUR proxyMode age 81188s fresh false
P 1.120099 c 0.000102 gamma 1.000000000
NAV L 1.120099 S 1.175489
mint L 1.120201 S 1.175596
redeem L 1.119997 S 1.175382
```
QQQ's c is 0 because its worst-case quoting is off. SPY's c is its confidence of 0.123159 times 1.5. EUR's `s_gamma_index` was still 0 (never written), so γ read as 1.0. Its first write, on 11 Oct 2026, set it to 0.99954.
## Freshness
Mint and redeem revert with `TwapNotAvailable` when P is 0 or `twap_updated_at` is more than 300 s old. There is no fallback price. The keeper withholds a push whenever its source price is stale. That happens every time the underlying market closes, so outside trading hours mint and redeem stop working 300 s after the last push.
Committed orders (enabled on every live market since 11 Oct 2026) settle only at a push newer than the commit, and expire 180 s after it. The bin books are the reverse: they trade only once the price is more than 300 s old outside `Normal`, against a demand-sized free-float book that the keeper posts.
Apply the same rule in your UI: mark prices older than 300 s as stale and disable mint and redeem.
## `OracleConfig` is not the live price
The oracle program's `OracleConfig` accounts are not updated on devnet; the keeper writes prices only to `Market`. As of 10 Oct 2026, QQQ's `OracleConfig` still holds a `last_price` of `562330000000`, written on 4 Apr 2026. `OracleConfig` prices are scaled to 1e9, not 1e6, so that is \$562.33. Don't read `OracleConfig` for live prices.
## Book centre (`Pool` account)
Each side of a market has a bin book: a `Pool` account in the CLP program at `[b"pool", market, [side]]`. Its `base_price_1e6` is the centre the keeper last posted. The keeper shifts that centre for the protocol's inventory, so it can sit away from NAV. With a CLP client loaded from `clp.json` the same way:
```typescript theme={null}
const [poolPda] = PublicKey.findProgramAddressSync(
[Buffer.from("pool"), marketPda.toBuffer(), Buffer.from([0])], // 0 = long, 1 = short
clp.programId,
);
const pool = await clp.account.pool.fetch(poolPda);
// Anchor renders base_price_1e6 as basePrice1E6 (capital E).
const centre = Number(pool.basePrice1E6) / 1e6;
```
On 10 Oct 2026 QQQ's long book was centred at 751.27 and its short book at 304.599737, the same as the NAVs above.
## Subscribe or poll
* Subscribe with `connection.onAccountChange(marketPda, ...)` to see each push as it lands.
* Poll every 15 to 30 s for dashboards, and batch markets into one `fetchMultiple` call.
The Continuum app reads all market accounts in one batched call every 30 s.
## Decimals
Token amounts and `Market` prices use 6 decimals. γ and `OracleConfig` prices use 9. Keep raw values in `BN` or `bigint` math and convert once, for display:
```typescript theme={null}
// WRONG: multiplies two 1e6-scaled numbers
const wrong = market.userTwapPrice.toNumber() * market.totalLSupply.toNumber();
// UI value
const ui = (market.userTwapPrice.toNumber() / 1e6) *
(market.totalLSupply.toNumber() / 1e6);
// Raw, still 1e6-scaled
const raw = market.userTwapPrice.mul(market.totalLSupply).div(new BN(1e6));
```
`BN` has arbitrary precision, so prefer it over `toNumber()` for anything you multiply.
## See also
Full state-fetch patterns (markets, vaults, positions).
What `Stress`, `ProxyMode` and `Recovery` mean and how to surface them.
# SDK setup
Source: https://docs.continuum.markets/build/sdk-setup
Set up a TypeScript project for Continuum on devnet, with the published IDLs, an RPC endpoint, a wallet and a first market read.
You can call Continuum from any Solana client. This page sets up a small TypeScript project with Anchor's client and the published IDLs, then reads a market on devnet.
The Continuum app itself uses `@solana/kit` 8.3, with program clients that Codama generates from the same IDLs (see [Kit and Codama](#kit-and-codama)). Anchor's client needs less setup for scripts, so the build pages use it.
## Stack
| Package | Why |
| - | - |
| Bun, or Node 18+ with `tsx` | Runs TypeScript. The samples use top-level `await` and the global `fetch`, so use an ES module project. |
| `@coral-xyz/anchor` 0.32 | Anchor's TypeScript client. The programs are built with Anchor 0.32.1. |
| `@solana/web3.js` 1.x | RPC, keypairs and transactions. |
| `@solana/spl-token` | Associated token account (ATA) addresses and token instructions. |
| Published IDLs | JSON descriptions of each program's instructions, accounts, events and errors. |
Trading needs no AMM SDK. While a market is open, `commit_swap` and `settle_swap` handle committed orders, enabled on every live market since 11 Oct 2026. Each bin book is a `Pool` account in the CLP program, and `pool_swap` fills against it instantly, but only while the market's price is stale.
## Minimal project
```bash theme={null}
mkdir continuum-app && cd continuum-app
bun init -y
bun add @coral-xyz/anchor@0.32 @solana/web3.js@1 @solana/spl-token
```
Pin `@solana/web3.js` to 1.x. Anchor 0.32 depends on 1.x and `@solana/spl-token` 0.4 requires it as a peer, while npm's `next` tag for `@solana/web3.js` already points at 3.x.
## The IDLs
The IDLs for all six programs are published in the docs repository. Browse them at [github.com/continuum-markets/continuum-docs/tree/main/idl](https://github.com/continuum-markets/continuum-docs/tree/main/idl), or fetch the raw files:
```
https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/.json
```
`` is `mint_redeem`, `clp`, `oracle`, `registry`, `faucet` or `trading`. The program source is not public, so these files are the reference for instruction names, arguments and account order. They use the Anchor 0.30+ IDL format, and each one carries its program ID in `address`:
```bash theme={null}
curl -s https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/mint_redeem.json | jq -r .address
# 5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4
```
Don't use `anchor idl fetch` for these programs. Their on-chain IDL accounts are out of date: as of 13 Oct 2026, mint-redeem's lists 14 of its 45 instructions and has no `collateral_authority` account. The devnet program IDs are in [Programs overview](/programs/overview).
## Boilerplate (`client.ts`)
```typescript theme={null}
// client.ts
import { Connection, Keypair } from "@solana/web3.js";
import { AnchorProvider, Program, Wallet, type Idl } from "@coral-xyz/anchor";
import fs from "fs";
import os from "os";
import path from "path";
const RPC_URL = process.env.RPC_URL ?? "https://api.devnet.solana.com";
const IDL_BASE =
"https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl";
export async function loadIdl(name: string): Promise {
const res = await fetch(`${IDL_BASE}/${name}.json`);
if (!res.ok) throw new Error(`IDL ${name}: HTTP ${res.status}`);
return (await res.json()) as Idl;
}
export function makeProvider(): AnchorProvider {
const connection = new Connection(RPC_URL, "confirmed");
const walletPath =
process.env.WALLET_PATH ?? path.join(os.homedir(), ".config/solana/id.json");
const secret = JSON.parse(fs.readFileSync(walletPath, "utf8"));
const wallet = new Wallet(Keypair.fromSecretKey(Uint8Array.from(secret)));
return new AnchorProvider(connection, wallet, { commitment: "confirmed" });
}
// A fetched IDL has no static types, so these clients are `any` to TypeScript.
export async function loadPrograms(provider: AnchorProvider) {
const load = async (name: string): Promise =>
new Program(await loadIdl(name), provider);
const [mintRedeem, clp, faucet] = await Promise.all(
["mint_redeem", "clp", "faucet"].map(load),
);
return { mintRedeem, clp, faucet };
}
```
`new Program(idl, provider)` takes the program ID from the IDL's `address`. Fetching at runtime always gets the current file. If you keep a copy in your repo instead, compare it with the published file after each program upgrade.
## RPC choice
Public devnet (`https://api.devnet.solana.com`) is fine for one-off scripts, but it rate-limits with HTTP 429. Anything that polls or runs continuously needs a private RPC, for example:
```
https://devnet.helius-rpc.com/?api-key=YOUR_KEY
https://solana-devnet.g.alchemy.com/v2/YOUR_KEY
```
Batch reads where you can. `program.account.market.fetchMultiple(pdas)` reads several markets in one `getMultipleAccounts` call.
## Wallet and devnet funds
Browser apps should sign through a Wallet Standard wallet. Scripts can load a keypair file, as `makeProvider` does. Privileged instructions check signer keys stored on chain (the keeper, CLP and market authorities). An integration only needs the user's own wallet.
Fund a devnet wallet with SOL (`solana airdrop 1 --url devnet`, or `connection.requestAirdrop` in code), then drip cUSDC from the [faucet program](/programs/faucet). It sends 1,000 cUSDC per wallet every 24 hours, and the [mint and redeem example](/build/example-mint-redeem) includes the `drip` call.
## First call: read a market
```typescript theme={null}
// read-market.ts
import { PublicKey } from "@solana/web3.js";
import { makeProvider, loadPrograms } from "./client";
const provider = makeProvider();
const { mintRedeem } = await loadPrograms(provider);
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
mintRedeem.programId,
);
const market = await mintRedeem.account.market.fetch(marketPda);
console.log("market ", marketPda.toBase58());
console.log("risk state ", Object.keys(market.riskState)[0]);
console.log("L mint ", market.longMint.toBase58());
console.log("S mint ", market.shortMint.toBase58());
console.log("collateral ", market.totalCollateral.toNumber() / 1e6, "cUSDC");
```
`bun read-market.ts` printed this on 10 Oct 2026:
```
market F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5
risk state proxyMode
L mint E4xJxBPyDXXJK2uqF17cq9kch1xHCTBZ6vjMxKWKANgZ
S mint BisvPibkUJqix3ZzaeHCxJ2W7KqoroFpnyf55nzFoicG
collateral 1883044.749247 cUSDC
```
Every devnet market runs in ProxyMode. `totalCollateral` is the market's own collateral ledger, which the solvency checks read. Each market has its own vault, but a plain token transfer can raise a vault's balance without changing this field.
## Typed clients
To get TypeScript types, download the IDL and generate a type with the Anchor CLI (0.30 or later):
```bash theme={null}
mkdir -p idl src/types
curl -so idl/mint_redeem.json \
https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/mint_redeem.json
anchor idl type idl/mint_redeem.json -o src/types/mint_redeem.ts
```
```typescript theme={null}
import { Program } from "@coral-xyz/anchor";
import type { MintRedeem } from "./src/types/mint_redeem";
import idl from "./idl/mint_redeem.json";
const mintRedeem = new Program(idl as MintRedeem, provider);
const market = await mintRedeem.account.market.fetch(marketPda); // typed fields
```
Pass the type argument explicitly: without it, TypeScript infers a plain `Program`. A typed `.accounts()` only accepts accounts that Anchor can't derive, so pass PDAs such as `market` through `.accountsPartial()`. Anchor can't derive the market PDA itself, because its seed is a field of the market account.
## Kit and Codama
To build on `@solana/kit` like the Continuum app, generate clients with Codama:
```bash theme={null}
bun add @solana/kit
bun add -d codama@1.11.0 @codama/nodes-from-anchor@1.5.6 @codama/renderers-js@2.5.0
```
```typescript theme={null}
// generate.ts
import { createFromRoot } from "codama";
import { rootNodeFromAnchor } from "@codama/nodes-from-anchor";
import { renderVisitor } from "@codama/renderers-js";
const IDL_BASE =
"https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl";
for (const name of ["mint_redeem", "clp", "faucet"]) {
const idl = await (await fetch(`${IDL_BASE}/${name}.json`)).json();
await createFromRoot(rootNodeFromAnchor(idl)).accept(
renderVisitor(".", { generatedFolder: `src/generated/${name}` }),
);
}
```
These are the versions the app pins. The script writes one client per program under `src/generated/` and adds `@solana/kit` and `@solana/program-client-core` to `package.json`. Only `initialize_market`, an admin instruction, needs the workaround.
## Common pitfalls
### Wrong cluster
The IDLs carry devnet program IDs. Pointed at another cluster, `fetch` fails with `Account does not exist or has no data`.
### Missing token accounts
Mint and redeem need the user's cUSDC, L and S token accounts and the fee recipient's cUSDC account to exist. Create them first with `createAssociatedTokenAccountIdempotentInstruction`. A missing account fails with Anchor's `AccountNotInitialized` (3012). Some fee recipients are PDAs, so pass `allowOwnerOffCurve = true` when you derive their ATA.
### Decimals
cUSDC, L and S use 6 decimals, and prices in `Market` are scaled to 1e6. `s_gamma_index` (γ) is scaled to 1e9, and so are the prices in the oracle program's `OracleConfig`. Divide by the right power of ten before display, and multiply by `10 ** 6` when you build token amounts.
## See also
Where IDLs ship, PDA derivation patterns.
End-to-end script: drip cUSDC, mint, redeem, verify.
# CLP - Continuous Liquidity Provider
Source: https://docs.continuum.markets/concepts/clp
The on-chain program that hosts Continuum's trading venues and the vaults that stock them with inventory.
The **CLP** (Continuous Liquidity Provider) is the on-chain program where all trading happens. It hosts two venues, committed orders while a market is open and native bin books while its price is stale, plus the per-market vaults that stock them with inventory. The instant oracle swap it also hosted was retired on 11 Oct 2026.
Users call the CLP every time they trade a leg. Inventory and treasury operations, such as funding books, sweeping excess and minting pairs into vaults, are gated to the keeper or an admin.
## Venue 1: native bin books
Each market has one `Pool` account **per side**, a long pool and a short pool (PDA seeds `[b"pool", market, side]`), so there are 26 native pools across the 13 live markets.
* Up to 16 price bins live inline in the `Pool` account itself (live books use 12, and XAU's long book uses 6). There are no separate bin-array accounts and no rent churn on a reposition.
* The geometry has two zones: fine core steps near the centre and wide wings further out. QQQ uses three 20 bps core steps, then 300 bps wings. `configure_pool_geom(wing_step_bps, core_bins)` sets it.
* The keeper posts the shape. `set_pool_shape(base_price_1e6, asks, bids)` writes the centre and the per-bin sizes in one call, and the program checks the sums against the pool's vault balances. The keeper centres each book on NAV plus a small inventory shift, and repositions it when NAV drifts past a gate of at most 20 bps.
`pool_swap(params: PoolSwapParams { direction, amount_in, min_out })` walks the bins with real slippage and charges no swap fee. It is pure inventory trading: nothing is minted or burned. It trades only while the market floats: its risk state isn't `Normal` and its last price push is more than 300 s old, the same condition that freezes mint and redeem. While prices are pushing it fails with `PoolClosedWhileLive`, and trades go through committed orders. Nothing in the program can set `pool.frozen` or disable a pool, so there is no pool-level kill switch. `pool_swap` is held to a rolling notional cap: 1,000,000 cUSDC per hour on QQQ, SPY and XAU, and unset (uncapped) on the other markets, as of 11 Oct 2026.
### What the book actually is
The keeper's env vars carry a `CLMM_` prefix and its pricing module is `clmm.rs`. Those are legacy names. The venue is a bin book, and it works differently from a Uniswap v3 pool:
| | Uniswap v3 pool | Continuum's bin book |
| - | - | - |
| Liquidity shape | Continuous `x·y=k` curve inside a tick range | Discrete bins; a fixed reserve at a fixed price |
| Price inside a unit | Moves continuously along the curve | Constant within a bin; a fill walks to the next bin |
| Who supplies it | Many permissionless LPs choosing their own ranges | One quoter: the protocol is the sole LP |
| How the shape is set | Emerges from LP deposits | Posted by the keeper (`set_pool_shape`) at each reposition |
Mechanically it is a **discrete-bin liquidity book**, the same family as Trader Joe's Liquidity Book and Meteora's DLMM (the "D" is for *discrete*), which is what the program's `clmm.rs` header means by "DLMM-style". Ask bin `i` sits `i + 1` steps above the centre and bid bin `i` sits `i + 1` steps below. Each step multiplies the price by `1 + step/1e4`, using the core step for the first `core_bins` steps and the wing step beyond. A bin holds a reserve and fills at its own price until it runs out.
It differs from those too, by being *less* of an AMM. The whole book lives inline in one `Pool` account with no per-bin accounts, and the keeper **posts** the ladder; deposits don't shape it. There is no bonding curve. The most accurate description is a **keeper-quoted bin book**: an on-chain market maker whose quotes are settled by an AMM-style swap instruction.
We keep the `CLMM_*` env-var prefix and the `clmm.rs` filename for compatibility with deployed configuration; the docs say "book" or "bin book".
### Lean by design
Book depth is sized to **demand, never to TVL**:
* Loss to toxic arbitrage grows with deployed depth, while organic revenue is capped by demand, so depth beyond demand is pure downside.
* Bid cUSDC sitting in bins earns nothing, so each side's bids are held at the depth its next free-float book will post (at least \$500). The excess is swept to the CLP vault, where it provisions committed orders.
* Asks trim to at most 1.5× bids via `pool_sweep_synth`.
The keeper trims bloated sides back, and imbalance control mints pairs for the CLP's stock when it needs them. Large sells don't need standing bids at all: while the market is open they go through committed orders, and the keeper provisions the cUSDC at settle time, from the treasury or by redeeming pairs.
## Venue 2: committed orders
The large-order path uses three instructions and one account:
```
commit_swap(side, direction, amount_in, min_out, nonce)
→ escrows the input into an Order PDA [b"order", user, market, nonce]
(TTL 180 s)
settle_swap() ── permissionless
→ fills at NAV ± max(commit_spread_bps, 10) bps at the latest push,
which must be newer than the commit; min_out miss → full refund
cancel_swap() ── anyone, after the TTL
→ refund to the owner; checks nothing else
```
Since 11 Oct 2026, committed orders are the protocol's only venue while a market is open, and they run on every live market. `commit_swap` needs the market's swap guards (`swap_guards`) enabled and not frozen.
How the venue behaves:
* There is no size cap. The fill price is set after the commitment, so latency arbitrage is impossible by construction. The limits are oracle health, a 100% collateral floor and keeper-provisioned inventory. A fill that fails any of them reverts, and the order stays pending for a later push. A `min_out` miss refunds the order instead.
* `COMMIT_SPREAD_BPS = 10` is only a floor. The keeper posts `commit_spread_bps = clamp(half-spread, 10, 500)` from the books' vol model: 18 bps for QQQ, 15 for SPY and 18 for XAU as of 10 Oct 2026. The post-commit price removes latency arbitrage, and the spread prices the protocol's convexity exposure on large fills.
* Funds are always recoverable. After the 180 s TTL (`COMMIT_TTL_SECS`), anyone can send `cancel_swap`, which returns the escrow to the owner whatever the oracle, keeper or admin state. The keeper sends it about 5 s after expiry, and the app offers a **Refund now** button 60 s after. Before expiry nobody can cancel. Every terminal path (settle, refund, cancel) closes the `Order` account and returns its rent.
* Anyone can call `settle_swap`. The keeper cranks it every 20 s, or every 5 s while orders are pending.
The `Order` account layout supports `getProgramAccounts` filters: `user` at offset 8, `market` at 40, `status` at 74.
`settle_swap` fills at the keeper-written `user_twap_price`, and its freshness check uses the keeper-written `twap_updated_at`. Both come from one key; nothing on chain checks them against an independent price.
## Retired: the instant oracle swap
`clp_swap(side, direction, amount_in, min_out)` filled instantly from CLP inventory at NAV ± `spread_bps`, on QQQ, SPY and XAU. It filled at the last pushed price, which is a stale quote between pushes, and it was retired on 11 Oct 2026. The instruction keeps its accounts and arguments, so existing clients still decode it, but every call fails with `SwapDisabled`.
Its guards, set by `configure_swap`, now serve committed orders. On the 13 markets with guards on, `settle_swap` needs a price at most 600 s old and a market at least 100% collateralized at that price. The confidence and deviation limits (5,000 and 10,000 bps) are off in practice, and the price write itself refuses a move of more than 25% per push.
## Vaults and capital flow
```
Admin funding (GlobalClp → allocate_to_market)
▼
Per-market CLP vault (cUSDC)
│ vault_mint_pairs → CLP-PDA-signed CPI into mint-redeem;
▼ paired L+S land in CLP custody
CLP inventory (L + S + cUSDC)
│ clp_fund_pool_synth / → stock the per-side books
│ set_pool_shape
▼
bin books ◄──── pool_sweep_usdc / pool_sweep_synth ──── excess back to the CLP
│
└─ committed orders provision at settle time, from the vault
or the treasury (vault_mint_pairs / vault_redeem_pairs as needed)
```
`vault_mint_pairs`, `vault_redeem_pairs` and `vault_redeem_single` are CPIs into mint-redeem signed by the CLP PDA. The paired ones go through the ordinary `mint_paired` and `redeem_paired`, at NAV and with the same fee and price checks as any user. Each one must name the CLP's own market, and its cUSDC, L and S accounts must belong to the CLP PDA. Inventory can't be minted against another market or paid to a wallet.
Each market keeps its collateral in its own vault, owned by its Market PDA, and keeps its own `total_collateral` ledger. Every outflow is limited to that market's liquid share, `total_collateral − deployed_to_yield`. QQQ, SPY and XAU shared a unified vault until 11 Oct 2026. After they moved out, every vault's balance equaled its market's ledger, and the program upgrade that day removed the unified vault. A cross-market rebalancer moves only collateral above the source market's own posted buffer target. `rebalance_collateral` on mint-redeem caps each move at that amount on-chain, so a larger request moves less and never takes the source below target.
## The treasury
The `GlobalClp` vault at the top of the diagram is the protocol treasury. It held about 40.0M cUSDC late on 12 Oct 2026, after the keeper returned idle CLP cash on 11 Oct and the CLP's own holdings were cut to working size on 12 Oct. On devnet the keeper is now burning it down, and it held 36.1M cUSDC on 13 Oct 2026. Besides `allocate_to_market`, four instructions draw on it:
* `backstop_market` moves cUSDC straight into a market's collateral vault and credits its `total_collateral`. It has no rate limit, because the funds stay inside the protocol. The keeper calls it when a market falls below 100% or stays short of its posted target; see [Solvency → Treasury backstop](/concepts/solvency#treasury-backstop).
* `admin_withdraw` pays cUSDC out of the protocol, to one named account only. Its allowance refills at a rate the keeper keeps in proportion to the balance, so a full exit takes 24 h whatever the treasury holds. Loosening the limits waits 24 h. A guardian key can freeze withdrawals and veto a loosening, but it can't move funds.
* `treasury_lend` sends idle cUSDC to a lending venue and `treasury_recall` brings it back. A lend spends the same allowance as `admin_withdraw` and stops while the treasury is frozen; a recall restores the allowance and always works. Nothing lends the treasury on a schedule yet, and devnet has no venues.
* `advance_to_market` lends cUSDC to a market's collateral vault so that a queued redemption can be paid, within the market's advance cap. It is not charged to the withdrawal allowance, because the cash stays inside the protocol, and the market repays it from its own next recall. See [mint-redeem → MarketAdvance](/programs/mint-redeem#marketadvance).
The keeper's yield allocator counts the treasury as first-loss capital when it sizes lending positions. See [clp → Treasury](/programs/clp#treasury) for the instructions.
## Where protocol revenue accumulates
* Trading-spread profits build up in the pool vaults and CLP custody as the books buy below NAV and sell above it. They reach a market's collateral only when someone adds them. Anyone can call `donate_to_vault`, and the market authority can call `fund_collateral`. Profits deposited into the treasury reach a market through `backstop_market`, which the keeper calls when a market falls below 100% or stays short of its target. The keeper calls neither `donate_to_vault` nor `fund_collateral`.
* Of each mint and redeem fee, 90% stays in the market's collateral vault and 10% (`dev_tax_pct`) goes to the market's fee recipient, which has been the treasury on every live market since 12 Oct 2026.
* Harvested yield goes into the market's own vault and is added to `total_collateral`, so it raises that market's collateral ratio. Collateral above the target can still move to the CLP with `withdraw_excess_to_clp`. Nothing is lent on devnet: the keeper's emulator harvests the yield its mainnet plan would earn, and every live market has `yield_enabled` set as of 11 Oct 2026.
See [Solvency](/concepts/solvency) for how these channels affect the collateral ratio.
## Reading pool state
```typescript theme={null}
import { web3 } from "@coral-xyz/anchor";
const side = 0; // 0 = long, 1 = short
const [poolPDA] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("pool"), marketPDA.toBuffer(), Buffer.from([side])],
clpProgram.programId,
);
const pool = await clpProgram.account.pool.fetch(poolPDA);
console.log("Centre price:", pool.basePrice1E6.toString());
console.log("Bins:", pool.numBins, "core step:", pool.stepBps, "x", pool.coreBins, "wing step:", pool.wingStepBps);
```
The app's [/pools](https://continuum.markets/pools) page renders this directly: the per-leg pool price, a deviation-from-NAV badge, instant depth and the per-market spread.
## Read more
All instructions, account fields, errors.
The spread engine and book-management loop behind set\_pool\_shape.
How lean books and depth caps defend the collateral ratio.
Choosing a venue and executing trades end to end.
# Keeper
Source: https://docs.continuum.markets/concepts/keeper
The off-chain service that pushes prices, manages the bin books, settles committed orders, and posts fees, buffers and volatility decay.
The **keeper** is an off-chain Rust service, run by the protocol, that drives Continuum's operational loops. Its source code is not published. This page describes how the protocol's keeper behaves and how it is configured.
1. The price updater pushes each market's price and confidence band on-chain, and withholds the push when the source goes stale.
2. The book manager (`clmm_pool.rs`) centres and reshapes the books, sizes the spread, refills and trims inventory, and posts the buffer, fees, committed spread and volatility decay.
3. The commit settler cranks committed-order settlement, which anyone may do.
4. The liquidity reallocator keeps each market's CLP vault at the cash it needs before its next top-up, holds the rest in the treasury, and backstops a market's collateral from the treasury when it falls below 100% or stays short of its target.
5. The yield allocator decides how much of each market's collateral to lend, and where. It lends only on mainnet, and only when switched on.
The keeper is **not in your transaction**. User mints, redeems and trades go straight to the on-chain programs. But mint and redeem rely on its pushes: both revert with `TwapNotAvailable` when the last push is more than 300 s old.
## Task inventory
### Price updater (every 5 s in the Docker image)
For each market, the keeper fetches a price and confidence band and pushes both with `mint_redeem::update_risk_state_v2`, together with the source's publish time. Pyth's Hermes service needs an API key (`PYTH_API_KEY`). Without one, the devnet keeper prices markets from a keyless public quote source and derives the band from a GJR-GARCH volatility model. Before it pushes a fresh price, it checks the price against Hyperliquid's oracle price for the same market and withholds the push if the two disagree. On devnet every market runs in `ProxyMode`. See [Oracle](/concepts/oracle).
The updater knows when a market is closed. If the source's publish time is more than 120 s old (`HERMES_STALE_SECS`), or the fetch fails, the keeper **withholds** the push. The stored price then ages past 300 s, mint and redeem freeze, and the book manager switches to free-float books.
### Book manager (30 s)
Per market, each tick:
* Feeds the vol state with the latest price, ignoring repeated closed-hours prices.
* Centres each book on NAV plus an **inventory shift** (the GLFT / Avellaneda-Stoikov centre shift). The shift is one dollar amount, added to one leg's centre and subtracted from the other's, so the pair price doesn't move: a lone long buyer pays a small premium that funds a lone short buyer's discount.
* **Repositions** a book with `set_pool_shape` when NAV drifts from its centre by more than a gate. The gate is 20 bps at most, cut down to each leg's funded quote edge.
* **Refills and trims** both ways. Each side's bid cUSDC holds at the depth its next free-float book will post (\$500 today): the pool vault's excess goes to the CLP vault, and bids that sellers spent are refilled from it (`clp_fund_pool_usdc`). Asks trim to at most 1.5× that target. Nothing refills while a book floats off-hours.
* Posts one **free-float book** per side off-hours, anchored at the last price. Each side puts all its depth in one bin, the outermost within 1% of that price: the r55 study found that bins further out took no ordinary flow and only carried gap risk. The depth is sized to demand, max(\$500, min(the 99.9th percentile of one side's measured flow per closed period, a risk cap)). The risk cap limits what a gap at the 99.9th percentile of that market's daily move could cost the treasury's share for the market (r61), so a volatile market gets less. There are no fixed dollar ceilings. Until the keeper measures its own fills, flow comes from `CLMM_FREEFLOAT_FLOW_USD_` and defaults to zero, so every side posts \$500. A guard can then pull the bin on the side that other venues show is stale. It never moves the centre or adds depth.
* Posts each market's **buffer target** (`set_buffer_target`): the daily Monte Carlo target from the market's live state, or a fixed fallback if that is over 48 hours old. The same nightly job publishes each market's CLP stock share for imbalance control (see [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer)), only when it differs from the value on chain. Posts the **mint/redeem fee** (`update_market`, `clamp(fee_mult × half-spread, 10, 100)` bps, the same value for both) and the **committed spread** (`set_commit_spread`, `clamp(half-spread, 10, 500)` bps) once they move by at least 5 and 2 bps respectively. The keeper posts a committed spread on all 13 markets, and since 11 Oct 2026 every one accepts committed orders.
* Posts the **short-leg volatility decay** (`set_s_gamma_index`). It is always on and has no switch. Charges accrue until they reach a 2e-4 posting gate, and the unposted balance is saved to R2 or `DATA_DIR`, so a restart doesn't discard it. A market that hasn't had a γ write for a day is posted anyway once at least 1e-6 is pending, so slow markets such as FX are charged at least daily. See [NAV → Volatility decay](/concepts/nav#volatility-decay).
* Runs the **cross-market collateral rebalance**. It plans at the price the program checks, which is the stored price with γ applied to the short leg, and leaves each source one confidence band of margin above its target. A market within one confidence band of its target is left alone, since the next price can close that gap. Past the band it is filled to its target plus the band. Markets below 100% collateralization are filled first, then markets below their target, and each source's excess is split across the markets it can reach in proportion to their need. The program clamps each move to the source's excess above its own target.
### Commit settler (20 s idle, 5 s while orders pend)
The settler finds pending `Order` accounts with one filtered `getProgramAccounts` call, and also wakes early on websocket notifications. It settles up to 10 orders per tick, largest first, and simulates each settle before sending it:
* A refund (a `min_out` miss), or a fill the CLP vault already covers, is sent unchanged.
* A fill the CLP can't cover is funded first. The settler borrows the cash it lacks from the treasury (`allocate_to_market`) and mints or redeems pairs if needed: a sell pays out cUSDC, and a buy needs synth, which the CLP mints in pairs at twice the value it lacks. These steps go in the same transaction as the settle where they fit, so a fill that fails takes its funding back with it. Otherwise they run in sequence before the settle.
* An order that the collateral floor or the oracle checks hold back waits for a later push.
About 5 s after an order's 180 s TTL, the settler refunds it with `cancel_swap`, which returns the escrow to the owner. Anyone may settle or refund; the keeper just does it promptly.
### Liquidity reallocator (30 s)
The CLP's cUSDC sits in the treasury (the global vault) and in each market's CLP vault. The reallocator is the only task that moves it between them. A CLP vault holds only working cash: what its one instant consumer, the books' bid refills, can spend before the next tick. That is both sides refilled from empty at the bid target for each book-manager tick in one reallocator tick, plus a quarter of one tick as slack: 2 × \$500 × 1.25 = \$1,250 per market today. Imbalance control's pair mint draws its cash from the treasury in the same transaction, and skips when that would leave the treasury below its liquid share. The inventory restocker that kept about \$10,000 a side for the retired instant swap was deleted on 13 Oct 2026.
Each vault is kept between that need and twice it. Below the need, it is topped up to 1.5×. Above twice the need, everything over 1.5× returns to the treasury. Every move is at least half a tick's spend, and even at the full spend rate a market needs at most one top-up per tick. An inactive market keeps nothing.
The first pass, on 11 Oct 2026, returned \$12.99M from 11 markets, \$1.18M each, and left them \$60,000 apiece. QQQ, SPY and XAU also held about \$1.04M each as cash for the instant swap. Once `clp_swap` was retired later that day, that cash was no longer needed, and they returned \$3.6M to the treasury between them. (`pool_swap`'s own rolling notional cap on those three markets is a separate setting and is still in place.) The treasury held \$22.13M after those returns.
Committed orders need no standing cash, because the settler draws from the treasury when they settle. A shared lock stops the reallocator from returning that cash before the orders settle. Cash waiting in the treasury is available to any market, and the yield allocator counts it as first-loss capital.
#### Treasury backstop
The reallocator also tops up market collateral from the treasury, with `backstop_market`, which pays into the market's collateral rather than its CLP vault. It reads each market's collateral ratio at the stored price:
* Below 100%, it fills at once, at least up to the market's target. Committed orders refuse to fill below the 100% floor, so this is what lets them resume.
* Short of its target by more than one confidence band for 5 minutes, it fills to the target plus the band. That is the cross-market rebalancer's destination rule, and the rebalancer moves other markets' excess first.
The target the treasury fills to is the market's posted target, capped at 130%. MSTR, closed on 12 Oct 2026, gets none. Fills are not returned automatically: a later surplus stays as a cushion, which the rebalancer can move between markets.
### Treasury withdrawal rate (900 s)
`admin_withdraw` can take only what the treasury policy's allowance holds, and the allowance refills at a set rate. This task keeps that rate in proportion to the balance, so a full exit takes 24 hours whatever the treasury holds: an hour's worth at once, the rest at the rate. On 12 Oct 2026 the treasury held \$23.11M, and the rate was still the 64.06 cUSDC a second set when it held \$5.53M. The task proposed 256.78 cUSDC a second with a 924,403 cUSDC burst, which applied on 13 Oct 2026. The treasury had grown to \$40.0M by late on 12 Oct, so a full exit at that rate took longer than 24 hours until the task proposed a new one. The policy read on chain at 13 Oct 2026 has that rate and burst, with a further change to a limit of 1,000,000,000 cUSDC a second pending until 04:29 UTC on 14 Oct.
The program decides how a change lands. A cut applies at once. A raise waits out the policy's 24 h delay, during which the guardian can cancel it, and the task applies it once due, so a deposit can't speed up withdrawals any sooner. The task changes nothing while the allowance is refilling, after a withdrawal in the last hour or a raise just applied, so the rate in force when an exit starts holds until it ends. It moves the limits only when they are more than 10% off, and it leaves alone any pending change that alters the destination, guardian or delay.
### Imbalance control (900 s)
After the keeper's γ charge, a price move changes a market's claims by exactly (L − S) times the return, where L and S are the values of all its long and all its short tokens. Nothing moves the anchor, so the imbalance β = (L − S)/(L + S) drifts with the price, and the posted buffer has to cover a year of that drift. This task keeps β near zero with the protocol's own tokens. When β leaves its band (0.05), it redeems the CLP's surplus of the heavier leg at NAV through `vault_redeem_single`. Collateral and claims fall by the same amount and no fee is charged, so no holder's tokens change value. The cash lands in the CLP vault, and the reallocator returns it to the treasury.
It acts only on a price under 240 s old, so not while a market is closed, and redeems at most \$50,000 per market per tick, nothing under \$100. It also owns the CLP's own stock. Each leg keeps a share of the market's claims, which the daily Monte Carlo job re-derives every night (0.5% until it has). It never redeems a leg below that share, and when both legs sit above it, it trims the matched excess with fee-free redemptions of equal value. The share is the cheapest trade-off between capital held and mint fees saved. It is computed from that night's re-centring events at the current volatility, 30 days of measured committed-order volume and the live mint fees. Yesterday's value stays while it is within 10% of the night's best. When the CLP holds too little of the heavier leg, it first mints pairs, with the cash coming from the treasury in the same transaction (the mint is skipped if that would leave the treasury below its liquid share). The CLP vault's own cash is the books' bid budget and is never touched. A pair mint splits the collateral 50/50 by value, so each leg gains the shortfall. Then it redeems the heavier leg's surplus: in effect a mint of the lighter leg. That costs about twice the mint fee on the value added, and 90% of the fee stays in the market's collateral.
It has run live since 12 Oct 2026, after a few hours in dry-run. The daily collateral targets plan on it: each market's buffer is computed with the book re-centred whenever its imbalance leaves the band, for as long as the live imbalance stays within twice the band (see [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer)). A market whose imbalance gets past that is planned as if nothing re-centred it.
### Yield allocator (300 s)
The allocator is the only part of the keeper that lends collateral. Each tick it reads the USDC reserves of three mainnet venues straight from chain: Kamino's main market, Save's main pool and Jupiter Lend Earn. MarginFi is disabled: the keeper's decoder expects a 1,864 byte `Bank`, the live account is 3,912 bytes and fails to decode, so it was in no plan anyway, and it was removed on 13 Oct 2026. From each reserve it rebuilds the venue's interest-rate curve, so it knows how far the supply rate falls as the protocol's own deposit pushes utilization down. It plans on a blended rate, 25% today's rate and 75% the venue's 30-day mean, so a one-day spike doesn't pull collateral.
Its limits are derived from stated assumptions:
| Limit | Rule | Default |
| - | - | - |
| Liquid share | The vault keeps one recall cycle of net redemptions: the larger of a measured share s and the stressed run over the recall horizon, q = max(s, 1 − (1 − r)^(T / 24 h)) | s is the nightly cr-targets job's liquid share, clamped to 1% to 10%: a rise applies at once and a fall is limited. It is used while that file is under 48 h old, and `YIELD_LIQUID_SHARE` (3%) is the fallback. Continuum has no flow yet, so r59 measured the closest analog, Kamino Main USDC's 9,009 hourly net flows: paying 99% of 5-minute windows instantly needs 1.7–6% of the deposit base, 3.2% at the measured 1 h to 24 h scaling. r = 50% a day and T = 0.1 h give only 0.3%, so s binds. There is no fixed-dollar floor: it was \$10,000, twice the CLP's underived \$5,000. With recall inside the redeem transaction, s can go to 0 |
| Venue cap | Each venue's total position, summed over all markets, is at most its share of first-loss capital, set by its record: half at Kamino and Jupiter Lend, which have no lender losses on record, and a quarter at Save. Any two venues can fail outright and every market stays fully backed | First-loss capital = the treasury + 50% of all markets' collateral above 100% |
| Venue exit cap | The protocol can always get out: at most half the venue's withdrawable liquidity, and never past its deposit cap | 50% |
| Hurdle | Stop adding once the marginal rate falls below it | 1% APR |
| Switching | Move only when the gain over 30 days, after a 25 bps charge for rate noise on anything withdrawn, beats twice the transaction cost | 2× |
First-loss capital is the capital that can absorb a venue failure. It is the treasury, which the allocator reads on chain every tick, plus 50% of all markets' collateral above 100% (`YIELD_VENUE_LOSS_BUDGET`). The treasury counts because `backstop_market` can move it into any market's collateral. With the shares at their defaults, any two venues can lose everything at once and `backstop_market` restores the markets. Save's record is a \$1.26M bad debt in three isolated pools in November 2022, which a DAO vote repaid; MarginFi's is a critical flash-loan bug, reported through its bug bounty and patched before anyone used it. On 12 Oct 2026 the shares bound nothing. Kamino, Save and MarginFi were held by their own exit liquidity, and only Jupiter Lend's cap came from its share. MarginFi has since been removed from the plan.
Per market, only the liquidity budget binds: the liquid share, 3% as the fallback and re-derived each night in the range 1% to 10%. The on-chain rails (`yield_enabled` and `yield_ceiling_bps`) apply on top. A market's ledger has three venue entries, one for each of the three venues, so a market can hold all of them. A market at or below 100% lends nothing.
Inside those limits it fills the venues in order of marginal rate, so every funded venue ends at about the same marginal rate. When the venues can't take every market's full budget, each market lends the same share of its budget, so every market keeps the same liquid share. A market in Stress or Recovery recalls everything.
Until 11 Oct 2026 the venue cap was half of each market's own collateral above 100%. At the 102% target that left about 3% of collateral lendable. On 11 Oct 2026 the new rule was measured against live mainnet venues for the 14 devnet markets of that date, which held \$19.2M of collateral. With the \$5.53M funded treasury, the plan lent about two-thirds of that collateral, held back by Jupiter Lend's venue cap of \$5.7M. Later that day the liquidity reallocator returned \$12.99M of idle CLP cash, taking the treasury to \$18.52M and first-loss capital to \$18.73M. The plan now lends 69.9%, the same share in every market, and earns 3.07% on all of it, about \$589,000 a year. Each market's liquid share is what binds now, not venue capacity. Kamino and MarginFi stopped at their exit caps, and Jupiter Lend and Save share the rest where their marginal rates meet. On 12 Oct 2026 the protocol's own holdings were cut to working size, leaving about \$1.4M of collateral. At that size no venue cap binds except MarginFi's small exit cap, and each market's liquid share decides what is lent (MarginFi was removed from the plan on 13 Oct 2026). That size was chosen partly because the venues couldn't absorb much more: at a \$1M to \$5M treasury, the loss shares would let only 10% to 38% of the old \$18.3M be lent.
It sends transactions only on mainnet USDC with `COLLATERAL_YIELD_ENABLED=true`, which is off by default. On every other cluster it plans and reports to the dashboard but sends nothing. The on-chain rails sit on top: `yield_enabled` and `yield_ceiling_bps` on each market. Since 12 Oct 2026 every market has yield enabled with its ceiling at 10,000 bps: all idle collateral may be lent, and the keeper recalls it before paying out. The program accepted at most 9,000 bps until then. Study r57 had recommended 9,000, because a 10% cash share pays a 10% run even on a day the lending venues freeze (as Solend's did in June 2022, during FTX and in July 2023); the operator chose 100%.
Devnet has no real lending, so a yield emulator (`DEVNET_YIELD_EMULATION`, default on, devnet only) stands in for it. Every hour it mints the cUSDC that the allocator's mainnet plan would have earned on each market's collateral, crediting each venue at its 30-day mean rate rather than its spot rate, and pushes it through the real `harvest_collateral_yield`, which adds it to the market's `total_collateral`. When no fresh mainnet snapshot is available, it credits nothing for that hour. The emulator sizes its plan against the same treasury, so emulated yield rose with the new venue cap.
### Redemption settler (60 s idle, 10 s while requests pend)
Pays the `request_redemption` requests that holders queue when a redeem is larger than the vault's cash. Per market, oldest request first, it pays the largest part that fits with `settle_redemption`. If the vault's cash isn't enough, it recalls the market's lending deposits in the same transaction. For what the venues can't release in time, it takes a treasury advance (`advance_to_market`) within the market's on-chain `max_advance_bps`. When a market has an advance outstanding and no open request, it repays the advance from the vault, recalling first if needed, so the market's own recall goes back to the treasury before it is lent again. The transactions that need a venue withdraw use an address lookup table when they don't fit one legacy packet. The task is off unless `REDEMPTION_SETTLE_ENABLED` is true. See [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet).
### Devnet treasury trim (5 min)
Devnet only, and off unless `DEVNET_TREASURY_TARGET_USD` is set. It withdraws what the treasury policy's allowance allows every tick, never past the target balance, and burns the cUSDC in the same transaction. The treasury's rate task holds the rate in force while the trim runs.
### Housekeeping
SOL balance checks every 300 s (with alerts, and airdrop top-ups on devnet), an inventory monitor, and an operator dashboard.
## The spread engine (model-free)
The half-spread that shapes every book is **not a forecasting model**. No trained model is in the live path: a linear-quantile forecaster measured net-negative in research, because inside a `max()` with the floors it could only widen, taxing flow without adding protection. The live spread is a max-of-floors stack:
```
half_spread = max(
realized-q90 ratchet, # trailing empirical q90 of window |moves|;
# warms within ~8 windows
floor_mult × σ_60d, # slow auto-floor from 60-day unconditional vol;
# self-corrects mis-set listing floors
oracle_conf + fixed, # never quote inside oracle uncertainty
min_bps, # per-market static boot floor:
) # QQQ 18, SPY 15, XAU 18, NVDA 30
```
Two post-processing steps follow. Above `freeze_trigger`, the book **freezes**: it stops repositioning and floats rather than chasing a disorderly tape. And a **protective drift skew** widens the trend-adverse side by ×(1 + z), where z is a slow, roughly 1-day return z-score, while the other side **never tightens**. Backtested, the skew fixes trend bleed (XAU went from −233 to +257 bps/yr). The antisymmetric variant, which also tightened the other side, was falsified.
The vol state lives in memory. After a restart, the `min_bps` and oracle-confidence floors govern until the ratchet warms, within about 8 windows; the slow floor takes longer. Closed-hours repeated prices are ignored.
### Listing without model training
Per-market config (`min_bps` / `floor_mult` / `fee_mult` / window) lives in the keeper's config JSON; `fee_mult` is 1.0 on every live market. Listing a new asset needs **no training**: the floors plus the ratchet carry both safety and competitiveness. The recipe is steep-wing geometry sized to the asset's vol class, `min_bps` at least about 2× its 5-minute σ, and `fee_mult` around 1.0 to 1.5 for high-vol single names.
## Configuration
Most keeper behavior sits behind env switches that default on. γ decay has none.
| Switch | Default | Gates |
| - | - | - |
| `CLMM_POOLS_ENABLED` | on | Book management as a whole |
| `CLMM_VOL_SPREAD_ENABLED` | on | Vol-scaled spread (off → static floors) |
| `CLMM_INVENTORY_SKEW_ENABLED` | on | Inventory centre shift |
| `CLMM_BUFFER_POST_ENABLED` | on | Buffer-target posting |
| `CLMM_FEE_SCALE_ENABLED` | on | Vol-informed fee posting |
| `CLMM_COMMIT_SPREAD_ENABLED` | on | Committed-spread posting |
| `CLMM_REBALANCE_ENABLED` | on | Cross-market collateral rebalance |
| `CLMM_REFILL_ENABLED` | on | Refill (asks and bids) and trim |
| `FF_GUARD_ENABLED` | on | Off-hours free-float guard |
| `COMMIT_SETTLE_ENABLED` | on | Settlement crank |
| `LIQUIDITY_REALLOCATOR_MODE` | `live` | Treasury ↔ CLP-vault moves (`off`, `dryrun` or `live`) |
| `YIELD_ALLOCATOR_ENABLED` | on | Yield planning, on every cluster |
| `COLLATERAL_YIELD_ENABLED` | off | Lending collateral (mainnet USDC only) |
| `DEVNET_YIELD_EMULATION` | on (devnet only) | Devnet yield emulator |
The keyless fallback price source has its own switch, which is off by default. The devnet keeper runs with it on.
Main tunables:
| Variable | Default | Meaning |
| - | - | - |
| `PYTH_API_KEY` | unset | Pyth Hermes API key. Without it, Pyth requests fail and the keyless fallback, if enabled, prices the market |
| `GAMMA_DECAY_COEF` | `1.0` | Scales the γ charge; must be in (0, 1]. Setting `CLMM_GAMMA_DECAY_ENABLED` to any value aborts boot |
| `UPDATE_INTERVAL_MS` | `15000` (`5000` in the Docker image) | Price updater cadence |
| `HERMES_STALE_SECS` | `120` | Source-price age at which a push is withheld, for every source |
| `CLMM_REPOSITION_BPS[_SYM]` | `20` | Largest drift gate before a reposition |
| `CLMM_BID_TARGET_USD` | unset | Overrides the bid cUSDC each side stages while open (default: the next free-float book's depth, \$500 today): swept above it, refilled below it |
| `CLMM_TRIM_RATIO` | `1.5` | Max asks-to-bid-target value ratio |
| `LIQUIDITY_REALLOCATOR_INTERVAL_SECS` | `30` | Reallocator cadence (min 30). A longer tick widens each vault's band |
| `COMMIT_SETTLE_INTERVAL_SECS` | `20` | Settler cadence when idle |
| `PORT`, `DASHBOARD_PORT` | `8484` | Dashboard port (`PORT` wins, then `DASHBOARD_PORT`) |
Full list: [keeper running guide](/keeper/running).
## Can I run my own keeper?
Not the protocol's: its source isn't published, and its privileged instructions check the market's authorities. What's open and what isn't:
* Anyone can crank `settle_swap`. Settling committed orders needs no authority.
* Paired arbitrage is open. Ordinary `mint_paired` / `redeem_paired` plus book trades need no privilege, and neither does `donate_to_vault`.
* Pricing and book shaping are privileged. `update_risk_state_v2`, `set_buffer_target` and `set_s_gamma_index` require `market.keeper_authority`. `set_pool_shape` requires the pool's `authority`, fee posting through `update_market` requires `market.authority`, and `set_commit_spread` requires the CLP's `authority`.
## Dashboard
The keeper serves an operator dashboard on one HTTP port (`PORT`, then `DASHBOARD_PORT`, default 8484). It shows price freshness per market, risk state, book positions and drift, pending committed orders, vault balances and yield status. It is for the operator; integrators read state from on-chain accounts.
## Read more
Architecture, loops, and operational detail.
Config, keypairs, dashboard, troubleshooting.
The books and vaults these loops manage.
What the protocol enforces independently of the keeper.
# Markets
Source: https://docs.continuum.markets/concepts/markets
A market is one synthetic asset. It has a Market account, a pair of SPL mints, a collateral vault, two native bin books and a per-market CLP. Here's how they're shaped and listed.
A **market** in Continuum is one synthetic asset. Each market has:
* A `Market` PDA (mint-redeem program) holding initial prices, fees, supply, the keeper's latest price, the risk state, the buffer target and the keeper authority.
* Two SPL mints, long (`L`) and short (`S`), both with the Market PDA as mint authority.
* A collateral vault backing the outstanding L and S supply. It is the Market PDA's own cUSDC token account; QQQ, SPY and XAU moved to theirs from a shared vault on 11 Oct 2026.
* Two native bin-book `Pool` PDAs (clp program), one per side.
* A `Clp` PDA and CLP vault holding the protocol's trading inventory and capital for the market.
* An `OracleConfig` PDA (oracle program), which mint and redeem take as their `oracle` account.
Markets are not interchangeable. A `QQQL` token is a different mint from an `SPYL` token, even though both are long tokens.
## PDA derivation
```
Market: seeds = [b"market", asset_symbol_bytes] (mint-redeem program)
OracleConfig: seeds = [b"oracle_config", market_pda] (oracle program)
Clp: seeds = [b"clp", market_pda] (clp program)
Pool: seeds = [b"pool", market_pda, side_byte] (clp program; side 0 = long, 1 = short)
```
The symbol is the asset ticker as ASCII bytes (`"QQQ"`), at most 10 characters. The CLP vault and each pool's two token vaults are associated token accounts (ATAs) of their PDA. [Address registry](/reference/addresses#derive-addresses-from-a-symbol) has a sample that derives all of them.
## What's stored on the Market account
Public fields you'll likely read:
| Field | Type | Meaning |
| - | - | - |
| `asset_symbol` | string | Market ticker. |
| `long_mint` / `short_mint` | Pubkey | L and S SPL mints. |
| `collateral_mint` | Pubkey | cUSDC mint. |
| `collateral_vault` | Pubkey | Token account holding the backing cUSDC: the Market PDA's cUSDC ATA on every live market. |
| `oracle_address` | Pubkey | The market's `OracleConfig` PDA, passed to mint and redeem as `oracle`. The keeper doesn't update it on devnet, so it is not a price source. |
| `fee_recipient` | Pubkey | Owner of the cUSDC account (`devTokenAccount`) that receives the dev tax. |
| `mint_fee_bps` / `redeem_fee_bps` | u16 | Fees the keeper posts, `clamp(fee_mult × half-spread, 10, 100)` bps. The risk-state multiplier applies on top. |
| `initial_l_price` / `initial_s_price` | u64 | Prices set at init, scaled by 1e6. Their product is the constant `K`. `reanchor_gamma` can fold γ into `initial_s_price`. |
| `total_l_supply` / `total_s_supply` | u64 | Outstanding supply on each side. |
| `total_collateral` | u64 | This market's collateral ledger figure, not a token balance. |
| `buffer_target_bps` | u16 | Overcollateralization target posted by the keeper: the market's daily Monte Carlo target, or a fixed fallback when that is more than 48 hours old. See [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer). |
| `is_active` | bool | If false, mint and redeem revert with `MarketNotActive`. |
| `risk_state` | enum | `Normal` / `ProxyMode` / `Stress` / `Recovery`. Every devnet market runs in ProxyMode. |
| `user_twap_price` / `twap_updated_at` | u64 / i64 | The keeper's latest reference price and when it was written. Despite the name it is a spot price, scaled by 1e6 and adjusted for corporate actions. Mint and redeem revert with `TwapNotAvailable` if it is 0 or more than 300 s old. |
| `oracle_confidence` | u64 | Confidence band pushed with each price. On devnet it comes from a GJR-GARCH volatility model. |
| `s_gamma_index` | u64 | γ, the short leg's volatility-decay index, scaled by 1e9 (0 reads as 1.0). |
| `keeper_authority` | Pubkey | Signer the program requires for the keeper's price, γ and buffer-target updates. |
| `dev_tax_pct` | u8 | Share of each fee sent to `fee_recipient` (10%). The rest stays in the vault. |
Full schema in the [mint-redeem reference](/programs/mint-redeem).
## Reading market state
```typescript theme={null}
import { Program, type Idl } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import idl from "./mint_redeem.json"; // the published mint_redeem IDL
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const program = new Program(idl as Idl, { connection });
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
program.programId,
);
const info = await connection.getAccountInfo(marketPda);
if (!info) throw new Error("QQQ market not found");
const market = program.coder.accounts.decode("market", info.data);
console.log("Symbol:", market.assetSymbol);
console.log("Fees (bps):", market.mintFeeBps, market.redeemFeeBps);
console.log("Total collateral (cUSDC):", market.totalCollateral.toNumber() / 1e6);
console.log("Price:", market.userTwapPrice.toNumber() / 1e6);
console.log("Price age (s):", Math.floor(Date.now() / 1000) - market.twapUpdatedAt.toNumber());
```
Run on Saturday 10 Oct 2026, it printed fees of 18 bps, 1,883,044.75 cUSDC of total collateral and a price of 751.27. The price was about 23 hours old because QQQ was closed. To read all live markets at once, batch with `getMultipleAccounts`. See [Reading state](/flows/read-state).
## Live markets
Thirteen markets are live on devnet. They have 26 bin books between them and about \$1.4M of total collateral (12 Oct 2026, after the protocol's own holdings were cut to working size). Each book is a ladder of discrete bins on both sides of a centre price. The first `core_bins` steps out from the centre are `step_bps` wide, and later steps are `wing_step_bps` wide. `num_bins` is the number of bins on each of the bid and ask sides. Geometry as of 10 Oct 2026:
| Symbol | Asset | Core step × core bins | Wing step | Bins a side |
| - | - | - | - | - |
| `QQQ` | Nasdaq-100 ETF | 20 bps × 3 | 300 bps | 12 |
| `SPY` | SPDR S\&P 500 ETF | 15 bps × 3 | 250 bps | 12 |
| `XAU` | Gold spot | Long 50 bps × 3, short 20 bps × 3 | 300 bps | Long 6, short 12 |
| `XAG` | Silver spot | 20 bps × 3 | 300 bps | 12 |
| `NVDA` | NVIDIA | 30 bps × 3 | 450 bps | 12 |
| `AAPL` | Apple | 25 bps × 3 | 400 bps | 12 |
| `EUR` | EUR/USD | 10 bps × 3 | 200 bps | 12 |
| `MSFT` | Microsoft | 25 bps × 3 | 400 bps | 12 |
| `TSLA` | Tesla | 35 bps × 3 | 500 bps | 12 |
| `AMZN` | Amazon | 25 bps × 3 | 400 bps | 12 |
| `GOOGL` | Alphabet | 25 bps × 3 | 400 bps | 12 |
| `GBP` | GBP/USD | 10 bps × 3 | 200 bps | 12 |
| `JPY` | USD/JPY | 10 bps × 3 | 200 bps | 12 |
Fine core steps quote tight near the centre, and wide wings hold inventory for larger moves. The keeper centres each book on an inventory-shifted price rather than on NAV itself, and re-centres it as the price moves (see [Book management](/keeper/book-management)). The books charge no swap fee.
During market hours the keeper stages, per side, the bids its next free-float book will post (\$500 until it has measured flow) and trims asks worth more than 1.5 times that. While mint and redeem are frozen, it posts a "free float" book instead: each side in one bin within 1% of the last price, sized to measured demand (\$500 a side until the keeper has measured flow) and limited by a risk cap on that market's share of the treasury. There are no fixed dollar ceilings.
The books fill only while the market's price is stale: its risk state isn't `Normal` and its last push is more than 300 s old. On QQQ, SPY and XAU, book trades are held to a cap of 1,000,000 cUSDC of notional per one-hour window (`swap_guards.window_cap_usd`). The other 10 live markets have no window cap set (checked on chain, 13 Oct 2026).
While the price is live, trades go through committed orders. They are on for every live market since 11 Oct 2026, because each one's `Clp` account has `swap_guards.enabled` set. The CLP's instant `clp_swap` was retired the same day and reverts with `SwapDisabled` on every market. An order fills at NAV ± `max(commit_spread_bps, 10)` bps, where the keeper posts `commit_spread_bps` as `clamp(half-spread, 10, 500)`. Committed orders have no window cap; the market's CLP inventory and a 100% collateral floor limit the fill.
→ [Live markets table with all pubkeys](/markets/live)
## How a market gets listed
The Continuum team lists markets as an operator process. In outline:
Create the two mints (6 decimals, Market PDA as mint authority, no freeze authority) and the market's cUSDC vault. Then call `initialize_market(params: InitMarketParams)` (admin only: the signer must be the program's upgrade authority) with the symbol, initial prices, starting fees and keeper authority.
`initialize_oracle_config` creates the `OracleConfig` PDA, and `upsert_oracle_feed` registers Pyth price accounts as its feeds. The keeper doesn't use them on devnet.
`initialize_clp(oi_cap, oracle_config)` creates the per-market `Clp`. The CLP vault and the CLP's L and S token accounts are its ATAs, and `allocate_to_market` funds the vault from the GlobalClp.
`init_pool(side, step_bps, num_bins)` for each side, then `configure_pool_geom(wing_step_bps, core_bins)`. After seeding the pool vaults, the first `set_pool_shape` posts the bins and enables the pool.
The keeper needs code changes and a redeploy before it prices the new market, because it discovers markets only at boot. The app needs its address, taxonomy, logo and token-metadata entries.
→ [Listing a market](/markets/listing)
## Roadmap
Planned markets, withdrawn markets and the path to mainnet are on the [Roadmap](/markets/roadmap).
## Trading hours
Mint and redeem follow the underlying's session, while the books trade around the clock. The keeper pushes a price only when its source prints a fresh quote, which happens during these sessions:
| Markets | Sessions with keeper prices |
| - | - |
| US stocks and ETFs (QQQ, SPY, NVDA, AAPL, MSFT, TSLA, AMZN, GOOGL) | 09:30 to 16:00 ET, Monday to Friday |
| FX (EUR, GBP, JPY) | Sunday 17:00 ET to Friday 17:00 ET |
| Gold and silver (XAU, XAG) | Sunday 18:00 ET to Friday 17:00 ET, with a daily pause from 17:00 to 18:00 ET |
NYSE holidays close US stocks and ETFs, and NYSE's 13:00 ET early closes (27 Nov and 24 Dec 2026) shorten the session. Metals shut for Good Friday, Christmas and New Year's Day, and on other NYSE holidays and early closes they stop at 13:00 ET and reopen at 18:00 ET. The keeper's alert clock knows these days; the pushes themselves follow the source's actual prints.
* During the session, the keeper pushes prices, and mint, redeem and committed orders work. The bin books don't fill while the price is live.
* When the underlying closes, the pushes stop. Once the last price is more than 300 s old, mint and redeem revert with `TwapNotAvailable`. A committed order settles only at a keeper push after it was placed, so none settle while the market is closed. About 5 s after the order's 180 s TTL, the keeper sends `cancel_swap`, which returns the escrow to the owner.
* The bin books open, showing the demand-sized free-float book centred on the last price. A guard compares other venues that keep trading off-hours and may pull bins on the side a move has made stale. It never moves the centre or adds depth.
* At the next session's first fresh price, the 300 s clock restarts, mint, redeem and committed orders reopen, the books stop filling, and the keeper re-centres them.
See [Oracle](/concepts/oracle) for how the price itself is produced.
## Read more
All 13 markets, every pubkey.
Batch fetch markets, derive NAV, query book depth.
# NAV math
Source: https://docs.continuum.markets/concepts/nav
How long and short NAVs are derived from one price, how volatility decay marks the short leg down, and how to compute a paired position's value.
NAV (net asset value) is what each long and short token is worth in cUSDC **at the protocol layer**. You pay it to mint, you receive it when you redeem, and the keeper centres the bin books on it.
## The two formulas
```
L_NAV = P
S_NAV = K / P × γ K = initial_l_price × initial_s_price, γ = s_gamma_index / 1e9
```
`P` is `market.user_twap_price`: the last price the keeper pushed with mint-redeem `update_risk_state_v2`, scaled to 1e6. Despite its name it is a spot price, and the program has already adjusted it for [corporate actions](#corporate-actions). There is no fallback price. Mint and redeem revert with `TwapNotAvailable` if `P` is 0 or the push is more than 300 s old.
γ is the short leg's volatility-decay index, between 0.5 and 1.0 (an unset index of 0 reads as 1.0). It only moves down, so the product `L_NAV × S_NAV = K·γ` falls below `K` as decay accrues. See [Volatility decay](#volatility-decay).
`initial_l_price` and `initial_s_price` are set when a market is created, and every live market launched with the two equal. QQQ launched at 480, so `K = 480 × 480 = 230,400`. If QQQ moves 10% to 528 with γ = 1, then `S_NAV = 230,400 / 528 = 436.36`. `initial_s_price` can change later, because `reanchor_gamma` folds γ into it.
## Why constant-product
The relationship `L_NAV × S_NAV = K·γ` gives the pair four properties:
1. Pair value has a floor. `L_NAV + S_NAV` is lowest where `L_NAV = S_NAV = √(K·γ)`, which is the launch price before any decay, and grows as the price moves away in either direction. It can't collapse to zero.
2. No positioning-driven funding. A perp DEX uses funding to pay whoever holds the unpopular side. Continuum issues both legs together, and the reciprocal NAV move takes the place of that payment. The short leg still pays [volatility decay](#volatility-decay) for the pair's convexity.
3. The short side can't go negative. As `L_NAV → ∞`, `S_NAV → 0`. An S holder can lose 100% of the position but never owes more.
4. The long side has no cap. There is no cliff and no liquidation: if the underlying rises 100×, so does `L_NAV`.
## Computing paired position value
For a position holding `n_L` long tokens and `n_S` short tokens:
```
position_value_cUSDC = n_L × L_NAV + n_S × S_NAV
```
For a position minted from `mint_amount` cUSDC, ignoring the worst-case markup:
```
fee_bps = mint_fee_bps × risk-state multiplier
mint_value_per_side = mint_amount × (1 - fee_bps / 10_000) / 2
n_L = mint_value_per_side / L_NAV_at_mint
n_S = mint_value_per_side / S_NAV_at_mint
```
At mint time the position is worth `mint_amount × (1 - fee_bps / 10_000)`: your deposit minus the fee.
## How position value moves with the underlying
Take a 100 cUSDC QQQ mint at 480, with `K = 230,400`, γ = 1, no confidence markup, and a 27 bps fee (QQQ's 18 bps posted fee × 1.5 in `ProxyMode`, as of 10 Oct 2026):
```
mint_value_per_side = 100 × 0.9973 / 2 = 49.865 cUSDC
n_L = 49.865 / 480 = 0.103885 QQQL
n_S = 49.865 / 480 = 0.103885 QQQS
```
When QQQ moves to a new price `P`:
```
L_NAV = P
S_NAV = 230,400 / P
value = 0.103885 × P + 0.103885 × 230,400 / P
= 0.103885 × P + 23,935 / P
```
Some sample values:
| QQQ price | Position value | Note |
| - | - | - |
| 240 (-50%) | 124.66 cUSDC | short leg's gain dominates |
| 384 (-20%) | 102.22 cUSDC | small gain from convexity |
| 432 (-10%) | 100.28 cUSDC | small gain |
| **480** (mint) | **99.73 cUSDC** | deposit minus the fee |
| 528 (+10%) | 100.18 cUSDC | small gain |
| 576 (+20%) | 101.39 cUSDC | small gain |
| 720 (+50%) | 108.04 cUSDC | long leg's gain dominates |
| 1440 (+200%) | 166.22 cUSDC | long appreciation |
A paired position has **near-zero delta** around its mint price. Its value curves up in both directions, so it gains only on large moves, and decay on the short leg works against that gain while you hold it. To take a directional view, sell one leg.
## Volatility decay
All of the pair's convexity lives in the **short** leg. `L_NAV = P` is linear (`d²L/dP² = 0`), while `S_NAV = K/P` is convex (`d²S/dP² = 2K/P³ > 0`). The protocol is the counterparty to that convexity, so it is **short volatility** on the short side. The γ-index charges for it:
```
S_NAV = K / P × γ γ = s_gamma_index / 1e9
```
* It is always on. The keeper posts it with `set_s_gamma_index`, and nothing turns it off: a keeper started with `CLMM_GAMMA_DECAY_ENABLED` set to any value refuses to boot. `GAMMA_DECAY_COEF` (default 1.0, which is break-even) scales the charge and must be in (0, 1], so it can't zero it either.
* It charges realized variance and ignores direction. The keeper measures the price close to close over 30-minute windows and banks each window's squared return as decay. It charges a confirmed move of more than 1% at once, and posts when the banked total passes 2e-4, or once a day when at least 1e-6 is owed and nothing has been posted for a day. A choppy path costs decay; a smooth directional move barely does, because that payoff sits in the long leg.
* It only ratchets down, and bad writes are rejected rather than clamped. `set_s_gamma_index` rejects a value above the current γ (`GammaMustRatchetDown`), a value outside \[0.5, 1.0] (`GammaOutOfRange`), and a write computed against an old epoch (`GammaEpochMismatch`). So γ can fall by at most 50% within one epoch.
* `reanchor_gamma` starts a new epoch. Only the market authority can call it, at least 14 days after the last one. It sets `initial_s_price` to `initial_s_price × γ` and resets γ to 1.0, which leaves every claim unchanged at every price. What it renews is the decay budget. It is not a re-anchor to the current spot price.
* It is not a funding rate. No cash moves between holders; γ is a mark on the short token's value. An instant mint→redeem round trip at a fixed γ is unaffected, and **long positions never pay it**. The app labels it "volatility decay" on the short side.
As of 11 Oct 2026 no market has been re-anchored. Read on 13 Oct 2026, γ on the 13 live markets runs from about 0.9998 (GBP) down to about 0.957 (NVDA). MSTR, closed on 12 Oct 2026, stood at about 0.944.
See [Solvency](/concepts/solvency) for why this covers the variance regime but leaves a directional run uncovered.
## NAV in the on-chain Market account
The relevant fields on the [`Market` PDA](/programs/mint-redeem#market-account), in IDL order. `collateral_vaults` is a variable-length `Vec` that comes before all of them, so none has a fixed byte offset. Decode the account with the IDL.
```rust theme={null}
pub struct Market {
// ...
pub initial_l_price: u64, // 1e6; set at creation
pub initial_s_price: u64, // 1e6; reanchor_gamma folds γ into it
// ... fees, supplies, total_collateral, risk state ...
pub user_twap_price: u64, // 1e6; last pushed spot price (L_NAV), corporate-action adjusted
pub twap_updated_at: i64, // unix seconds; source publish time of that push
// ...
pub oracle_confidence: u64, // 1e6; absolute half-width of the confidence band
// ... keeper authority, dev tax, yield fields, buffer_target_bps ...
pub s_gamma_index: u64, // 1e9 fixed point; 0 reads as 1.0
// ...
pub gamma_epoch: u32, // bumped by reanchor_gamma
// ...
pub price_adjust_num: u64, // cumulative corporate-action ratio; 0/0 reads as 1/1
pub price_adjust_den: u64,
// ...
}
```
To read NAV from a client:
```typescript theme={null}
import { BN } from "@coral-xyz/anchor";
const market = await mintRedeem.account.market.fetch(marketPDA);
// No fallback: mint and redeem revert with TwapNotAvailable when this is 0
// or the push is more than 300 s old.
const lNav = market.userTwapPrice;
if (lNav.isZero()) throw new Error("no price pushed yet");
// Short NAV includes the γ decay index (1e9 = 1.0). An unset index (0) reads as 1.0.
const GAMMA_ONE = new BN(1_000_000_000);
const gamma = market.sGammaIndex.isZero() ? GAMMA_ONE : BN.min(market.sGammaIndex, GAMMA_ONE);
const sNav = market.initialLPrice.mul(market.initialSPrice).div(lNav).mul(gamma).div(GAMMA_ONE);
const fresh = Date.now() / 1000 - market.twapUpdatedAt.toNumber() <= 300;
```
The 6-decimal scaling means `lNav.toNumber() / 1e6` gives a human-readable price. These are mid NAVs; mint and redeem quote each leg off them with the markup below.
## Worst-case quoting
With worst-case quoting on (`worst_case_config.enabled`, the default), mint and redeem price each leg against the user. The band is `c = oracle_confidence × m`, where `m` is the risk state's confidence multiplier: 1× in `Normal`, 1.5× in `ProxyMode`, 3× in `Stress`, and falling back from 3× in `Recovery`. The full table is on [Risk states](/concepts/risk-states#fee-and-confidence-multipliers).
The long NAV is linear in price (`L_NAV = P`) but the short NAV is **inverse** (`S_NAV = K/P × γ`), so a single widened price can't protect both legs. Each leg is quoted at the price that moves *against* the user:
| | Long leg (`L_NAV`) | Short leg (`S_NAV`) |
| - | - | - |
| **Mint** (user buys) | `P + c` → fewer L | `K / (P − c) × γ` is higher → fewer S |
| **Redeem** (user sells) | `P − c` → less out | `K / (P + c) × γ` is lower → less out |
A paired mint→redeem round trip therefore returns **less** than the deposit at any confidence. As of 13 Oct 2026 worst-case quoting is on for the 12 other live markets and off for QQQ, which quotes both legs at mid NAV.
## Corporate actions
A stock split changes the units a price is quoted in and leaves the value of every claim alone. The market authority records one with `apply_corporate_action(params: CorporateActionParams { ratio_num, ratio_den, effective_ts })`.
* The program keeps a cumulative ratio, `price_adjust_num / price_adjust_den`, and multiplies every incoming price by it before storing it. The stored price, `K` and γ don't change, so the adjusted price runs continuously through the split and no value moves between longs and shorts.
* The action is time-gated. A future `effective_ts` arms it, and prices keep their old units until that time. Only one action can be pending, at most 400 days ahead, with a ratio of at most 1,000 either way. After the ex-date, the program rejects pushes whose source publish time is before it.
* An action that lands late halts the market rather than repricing it whenever the post-split price moves more than 25%, because the move cap rejects that push. For a split, that means ratios above about 4:3. A gentler ratio looks like an ordinary price move, so the only control for it is arming the action ahead of the ex-date.
No action is armed as of 10 Oct 2026. Every market has `price_adjust_num = price_adjust_den = 0`, which reads as 1/1.
## NAV vs pool price
NAV is what the protocol mints and redeems at. **Book price** is what each bin book trades at between repositions. The keeper keeps them close: it centres each book on NAV plus a small inventory shift, repositions when NAV drifts past the gate (at most 20 bps by default), and never quotes inside the oracle's confidence band.
In steady state, `book_price ≈ NAV ± half_spread`, offset by the inventory shift. When pushes stop for 300 s and mint and redeem freeze, the keeper swaps each live book for a free-float book anchored at the last price, then re-anchors on the first fresh push. The books fill only while the price is stale. Pick a venue by market state:
* While the market is open, commit an order. It fills at a push after the commit, at NAV ± the committed spread, at any size.
* While its price is stale, hit the book (`pool_swap`). It's instant, up to the book's capped size, and you pay the bin ladder's spread.
* If you want the pair itself, mint or redeem at NAV. You pay the fee and the worst-case markup, and no spread.
→ [Trading venues](/flows/trade)
# Oracle
Source: https://docs.continuum.markets/concepts/oracle
Where Continuum's prices come from, what the keeper pushes on-chain, and the staleness chain that freezes mint and redeem when the underlying market closes.
Continuum's price layer is one stored number per market. The keeper fetches a price and a confidence band for each market and writes both to its `Market` account with `mint_redeem::update_risk_state_v2`. Mint, redeem, committed-order settlement and book repositioning all read that stored price, `user_twap_price`. Despite its name it is a spot price, scaled to 1e6, and the program has already applied any [corporate-action](/concepts/nav#corporate-actions) adjustment.
## Sources
| Network | Price | Confidence band | How it reaches the chain |
| - | - | - | - |
| **Devnet** | A free public quote source that needs no API key | GJR-GARCH volatility model | Keeper pushes with `update_risk_state_v2`; every market runs in `ProxyMode` |
| **Mainnet (planned)** | Pyth (Pyth Pro) | Pyth's interval, or the GARCH forecast if that is wider | Program reads Pyth in `Normal`; the keeper still pushes the band and risk state |
Pyth's public Hermes service has required an API key since 26 Aug 2026, so the devnet keeper prices markets from a keyless public quote source. That source publishes no confidence interval, so the keeper derives one from a GJR-GARCH volatility model kept per market.
The keeper checks every fresh price against Hyperliquid before pushing it. It compares the price with Hyperliquid's oracle price for the same market (SPY and QQQ go through a learned index ratio), and a disagreement beyond a per-market band blocks the push. Hyperliquid can veto a price but never sets one. If Hyperliquid is unreachable, the push goes ahead.
The oracle program is not in the devnet price path. The keeper skips `update_price_observation` on devnet, so `OracleConfig` there is stale. Don't read it for live prices; read the `Market` account.
`ProxyMode` means the program prices off the keeper-pushed `user_twap_price`. In `Normal`, the mainnet steady state, mint and redeem read the market's Pyth account inside the transaction instead. That read enforces its own limits: it rejects a print older than 60 s (`MAX_PYTH_AGE_SECS`) or one whose confidence is wider than 2% of the price (`MAX_PYTH_CONF_BPS`), with `OraclePriceUnavailable`. Devnet never uses this path.
Committed-order settlement (`settle_swap`) prices off the keeper-written `user_twap_price`, and its freshness check uses the keeper-written `twap_updated_at`. Both come from one key; nothing on chain checks them against an independent price.
## What one push contains
```
update_risk_state_v2(new_risk_state, user_twap_price, oracle_confidence, source_publish_time)
```
* **`user_twap_price`** is the source price, 1e6-scaled. The program multiplies it by the market's corporate-action ratio before storing it. This is `L_NAV`.
* **`oracle_confidence`** is the absolute half-width of the confidence band, in the same units and with the same adjustment. It drives worst-case quotes, and the keeper's spread never quotes inside it.
* **`new_risk_state`** is the keeper's risk regime for the market. See [Risk states](/concepts/risk-states).
* **`source_publish_time`** is when the source published the price. The program rejects a push whose publish time is in the future or more than 300 s old (`OraclePriceUnavailable`), and stores it as `twap_updated_at`. When the keeper can't verify the publish time it sends 0, and the program stores the current on-chain time instead.
Two rails bound each write:
* On-chain, the program rejects any price move over 25% from the stored price in one push (`MAX_TWAP_MOVE_BPS`), so a compromised keeper key can't teleport NAV. The first push to a market is exempt.
* On mainnet only, the keeper refuses to push a price more than 10% away from the oracle program's last TWAP (`MAX_HERMES_DEVIATION_BPS`). On devnet this envelope is off on purpose: the stored price *is* the keeper's own last write, so comparing against it would be circular and would deadlock after any pause.
## The staleness chain: what happens when the market closes
Quote sources keep serving the **last** price after the underlying stops trading, at night and over weekends for equities. Re-pushing that frozen price would keep mint and redeem open at a stale NAV all weekend. A chain of gates stops that:
```
underlying closes
→ the source's publish time ages past 120 s (HERMES_STALE_SECS, applies to every source)
or the fetch fails
→ keeper WITHHOLDS the push
→ twap_updated_at ages past 300 s (MAX_TWAP_AGE_SECS)
→ mint and redeem revert with TwapNotAvailable
→ keeper swaps the live books for free-float books
```
In `ProxyMode` the freeze exists only because the keeper withholds pushes. Nothing on chain knows the market is closed, and any accepted push resets the clock.
While frozen:
* **Mint and redeem revert** with `TwapNotAvailable`, so nothing prices off a dead feed.
* **The bin books open.** `pool_swap` fills only now, while the price is stale outside `Normal`, against a free-float book: one per side, anchored at the last price, with each side in one bin within 1% of it, sized to measured demand (\$500 a side until flow is measured) and limited by a risk cap on that market's share of the treasury. A guard can pull bins on the side that other venues (extended-hours sessions, futures, Hyperliquid, tokenized gold and euro) show is stale. It never moves the centre or adds depth.
* **Committed orders can't settle**, because settlement needs a push newer than the commit. About 5 s after the 180 s TTL, the keeper refunds the order with `cancel_swap`, which anyone may send.
* At reopen, the first accepted push re-anchors NAV, committed orders settle again, the books stop filling, and the keeper re-posts the live books.
## Confidence intervals
The confidence band feeds two mechanisms:
1. Mint and redeem price each leg off `P + c` or `P − c`, whichever is worse for the user, where `c = oracle_confidence` × the risk state's confidence multiplier. See [worst-case quoting](/concepts/nav#worst-case-quoting).
2. The keeper's [spread engine](/concepts/keeper#the-spread-engine-model-free) includes `oracle_conf + fixed` in its max-of-floors stack, so the books never quote inside the band.
## Reading prices from a client
Read the `Market` account. It holds the price the program will use, and reading it needs no oracle CPI.
```typescript theme={null}
import { BN } from "@coral-xyz/anchor";
const market = await mintRedeem.account.market.fetch(marketPDA);
const lNav = market.userTwapPrice; // 1e6; 0 means no price yet
const GAMMA_ONE = new BN(1_000_000_000);
const gamma = market.sGammaIndex.isZero() ? GAMMA_ONE : BN.min(market.sGammaIndex, GAMMA_ONE);
const sNav = lNav.isZero()
? new BN(0)
: market.initialLPrice.mul(market.initialSPrice).div(lNav).mul(gamma).div(GAMMA_ONE);
// Mirrors the program's own MAX_TWAP_AGE_SECS check
const ageSecs = Date.now() / 1000 - market.twapUpdatedAt.toNumber();
if (lNav.isZero() || ageSecs > 300) {
// Underlying market closed, a veto, or a keeper outage.
// Mint and redeem revert with TwapNotAvailable; books float; committed orders won't settle.
console.warn("Price stale: mint/redeem frozen until the next push.");
}
```
Check `twap_updated_at` before you assume a mint or redeem will go through. The protocol prices off the stored value, whatever your client computes.
## Read more
What `Normal` / `ProxyMode` / `Stress` / `Recovery` mean for your operations.
`update_risk_state_v2`, staleness gates, error catalog.
# How Continuum works
Source: https://docs.continuum.markets/concepts/overview
The mental model, covering paired tokens at NAV, two protocol-native trading venues, and the keeper that prices them.
Continuum has three moving parts. Two are on-chain programs and one runs off-chain.
* `mint-redeem` issues and burns paired long and short tokens against cUSDC collateral at NAV. It also stores each market's price, risk state and solvency target.
* `clp` is the protocol's trading and treasury program. It hosts both trading venues and the vaults that stock them.
* The keeper is an off-chain service run by the protocol. It pushes prices on-chain, manages the bin books, settles committed orders, and posts fees and volatility decay.
## The system at a glance
```
┌────────────────────────────────────────┐
│ Frontend / SDK │
└───────┬───────────────────┬────────────┘
│ │
mint / redeem at NAV trade either leg
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────────────┐
│ mint-redeem │ │ clp │
│ │ │ │
│ mint_paired │ │ 1. Committed (commit_swap │
│ redeem_paired │◄──┤ orders settle_swap)│
│ price + risk state │CPI│ 2. Bin books (pool_swap) │
│ buffer target, γ index │ │ only while price is stale │
│ collateral vaults │ │ per-market inventory vaults │
└──────────────────────────┘ └─────────────────────────────────┘
▲ ▲
│ │
┌───────────┴───────────────────┴───────────┐
│ Keeper (off-chain) │
│ price push · book manager · settler · │
│ refill/trim · buffer, fees, γ · yield │
└───────────────────────────────────────────┘
```
Everything a user touches is protocol-native. There is no external AMM in the call path.
## Minting and redeeming
The only program a user writes to for issuance is `mint-redeem`:
* `mint_paired(collateral_amount)` deposits cUSDC and mints matched long (`L`) and short (`S`) tokens at NAV.
* `redeem_paired(l_amount, s_amount)` burns long and short tokens and returns cUSDC at NAV. Both amounts must be above zero, but they don't have to match.
Both need at least 10 cUSDC of value and a keeper price push no more than 300 s old.
NAV comes from one price `P` and a constant `K = initial_l_price × initial_s_price`. `L_NAV = P` and `S_NAV = K / P × γ`, where γ (at most 1) is the short leg's [volatility decay](/concepts/nav#volatility-decay). So `L_NAV × S_NAV = K·γ`, and as one leg rises the other falls reciprocally. A paired mint deposits exactly the NAV value of the tokens it creates. No user path mints one side alone; see [Solvency](/concepts/solvency).
→ [Paired tokens](/concepts/paired-tokens) · [NAV math](/concepts/nav) · [Full reference](/programs/mint-redeem)
## The trading venues
Trading a single leg happens in the `clp` program, which has two venues of its own. Which one fills depends on whether the market's price is live:
| Venue | Instruction | Fill | When |
| - | - | - | - |
| Bin book | `pool_swap` | Instant, walks on-chain bins with real slippage, small capped size | Only while the price is stale |
| Committed order | `commit_swap` → `settle_swap` | At a keeper push after the commit, NAV ± the committed spread (10 bps floor), any size | While the keeper pushes prices |
Every market has its two bin books. Since 11 Oct 2026, committed orders are switched on for every live market, and they are the only one of the protocol's own venues that fills while prices are pushing. `pool_swap` fails with `PoolClosedWhileLive` until the price is more than 300 s old outside `Normal`. The instant oracle swap (`clp_swap`) was retired the same day and always fails with `SwapDisabled`. The [order book](/flows/order-book) is a third venue. It is a Manifest book where traders trade with each other, reached through the `trading` program, and it is open at any hour, and while a market is closed the app shows its orders and the bin books' quotes as one book.
### 1. Bin books: closed hours, small
Each market has one protocol-owned pool per side: 26 books across the 13 live markets. The bins sit inline in the `Pool` account in two zones, with fine core steps near the centre and wide wings further out (QQQ: three 20 bps core steps, then 300 bps wings). The keeper centres each book on NAV plus a small inventory shift, and repositions it when NAV drifts past a gate of at most 20 bps.
Standing depth is deliberately lean. It is sized to organic demand rather than TVL, because the loss to toxic arbitrage grows with deployed depth. Depth is sized per market to measured demand, at least \$500 a side. They fill only while the market's price is stale: when mint and redeem freeze off-hours, the keeper replaces them with a demand-sized [free-float book](/concepts/oracle#the-staleness-chain-what-happens-when-the-market-closes) and `pool_swap` opens.
### 2. Committed orders: open hours, any size
This is the open-hours path, at any size. You escrow your input into an `Order` PDA. The order fills at a keeper push made after your commit (not necessarily the first one), at NAV ± `max(commit_spread_bps, 10)` bps, where the keeper posts `commit_spread_bps` from the books' volatility model. Because the price is set after you commit, there is no stale quote to pick off, so there is no size or window cap. The limits are oracle health, the collateral floor and the inventory the keeper has provisioned.
* Settlement is permissionless; the keeper cranks it.
* A `min_out` miss refunds in full and closes the order, so it can't loop on reverts.
* After the 180 s TTL, anyone can send `cancel_swap`, and the escrow goes back to its owner. The keeper does it about 5 s after expiry. It checks nothing else, so escrowed funds come back whatever the oracle, keeper or admin state.
* Every terminal path closes the `Order` account and returns its rent to the user.
In the app, a market order is a committed order while the market is open, unless traders' orders on the order book fill all of it now for less when you buy, or for more when you sell.
## End-to-end mint flow
```
1. User wallet ─── mint_paired(100 cUSDC) ──► mint-redeem program
2. mint-redeem reads ─── user_twap_price (≤ 300 s old) from the Market account
3. mint-redeem transfers ─── 100 cUSDC less the dev tax ──► collateral vault
4. mint-redeem mints ─── L_qty long tokens ──► user's long ATA
5. mint-redeem mints ─── S_qty short tokens ──► user's short ATA
```
The keeper isn't part of the transaction, but the transaction depends on its pushes. Mint and redeem revert with `TwapNotAvailable` when the last push is more than 300 s old.
## End-to-end committed-order flow
```
1. User ─── commit_swap(side, direction, amount_in, min_out, nonce) ──► clp
input escrowed into an Order PDA, TTL = 180 s
2. Keeper ─── update_risk_state_v2 (a price push after the commit) ──► mint-redeem
3. Anyone ─── settle_swap() with the Order account ──► clp
fills at NAV ± committed spread at the latest push;
min_out miss → full refund; either way the Order closes
4. (fallback) after 180 s: the keeper, or anyone, sends cancel_swap;
the escrow goes back to the user
```
## Why this design
| Question | Answer |
| - | - |
| Why no funding rate? | Long and short supply is issued in pairs and every token is fully backed when minted, so there is no open-interest imbalance to fund. One-sided demand shows up as inventory in the CLP, and the keeper prices it by shifting the book centres: a lone long buyer pays a small premium that funds a lone short buyer's discount. The short leg does carry [volatility decay](/concepts/nav#volatility-decay) for the pair's convexity. Longs carry nothing. |
| Why no liquidations? | A paired position prepays full collateral. There is no margin to call. |
| Why two venues? | Each fits a different market state. While prices are pushing, every trade gets a NAV fill at a price set after the commitment, which can't be latency-arbitraged. While the market is closed no push arrives to settle one, so a lean on-chain book with real slippage fills small trades at once. An instant fill at a live oracle price is a stale quote between pushes, which is why the instant oracle swap was retired and the books close while the price is live. |
| Why is the committed spread so thin? | The price is set after the commitment, so there is nothing stale to select against. The retired oracle swap charged 30 bps on QQQ, SPY and XAU. The spread still prices the protocol's convexity exposure on large fills, so it follows the book's half-spread with a 10 bps floor. |
| Why is book depth so lean? | Toxic loss grows with deployed depth, while organic revenue is capped by demand. Depth is sized to demand, and the excess goes back to the CLP vault, where it provisions committed orders. See [CLP](/concepts/clp). |
## Where to go next
Why mint is paired, how the long/short pair settles.
The constant-product formula, volatility decay and what they imply for P\&L.
Book geometry, committed-order mechanics, vault plumbing.
Price pushes, book management, settlement crank, fee and decay posting.
# Paired L+S tokens
Source: https://docs.continuum.markets/concepts/paired-tokens
Mint produces matched long and short tokens. Here's why, and how the two settle against the underlying.
A Continuum position is a pair of SPL tokens: one long (`L`) and one short (`S`). They are minted and redeemed at the protocol's NAV, and each leg trades on its own: on its bin book, or through a committed order on markets that have them.
## What "paired" means
When you call `mint_paired(collateral_amount)`, the program:
1. Reads the price `P`, which is `L_NAV`, from the market's last keeper push (`user_twap_price`). In `Normal` mode it reads Pyth instead.
2. Derives `S_NAV = K / P × γ`, where `K = initial_l_price × initial_s_price` and γ is the short leg's volatility-decay index.
3. Takes the mint fee, then splits the rest into two halves of equal **value**.
4. Mints `(net / 2) / L_NAV` long tokens and `(net / 2) / S_NAV` short tokens. With worst-case quoting on, each leg is priced slightly against you; see [NAV math](/concepts/nav#worst-case-quoting).
Every live market launched with `initial_l_price` equal to `initial_s_price`, so the two legs are worth the same at the launch price. QQQ launched at 480, which makes `K = 480 × 480 = 230,400`. At `P = 480`, a 100 cUSDC mint gives about 0.104 QQQL and 0.104 QQQS before fees. At `P = 750`, `S_NAV = 230,400 / 750 = 307.2` (with γ = 1), and the same mint gives about 0.067 QQQL and 0.163 QQQS.
Either way the two legs carry the same **value** at mint time: 50 cUSDC each, before fees. As the price moves, the long leg's value rises while the short leg's falls reciprocally.
## Why pairs, not single sides
A user-facing single-sided mint would be unsafe. Minting only L, with no offsetting S, leaves the vault holding collateral against a one-way bet. If 1,000 users minted L-only at NAV and the price rose, the vault would owe more than it holds, and nothing on the short side would offset it.
Paired mint avoids that: each mint deposits exactly the NAV value of the tokens it creates, plus the fee. After that the deposit stays fixed while the claims move with the price. A pair's value is convex in `P`, so any move away from the mint price raises it. Fees, volatility decay and a collateral buffer cover that gap; if collateral still falls short, redeem pays pro rata. See [Solvency](/concepts/solvency).
Single-sided instructions do exist, but only for privileged signers. `keeper_mint_single` and `keeper_redeem_single` check `market.keeper_authority`, and `redeem_single_for_vault` accepts only the market's CLP PDA. Users can't call them.
## The constant-product invariant
```
L_NAV × S_NAV = K × γ K = initial_l_price × initial_s_price, γ ≤ 1
```
γ only ratchets down between re-anchors. `reanchor_gamma` folds γ into `initial_s_price` and resets γ to 1, which leaves the product unchanged. The invariant means:
* As `L_NAV` rises, `S_NAV` falls **reciprocally**, in proportion to `1 / L_NAV`.
* One L plus one S is worth `L_NAV + S_NAV`. That sum is lowest where `L_NAV = S_NAV = √(K·γ)`, which is the launch price before any decay, and grows as the price moves away in either direction.
* The short side approaches zero but never goes below it, so short holders never owe more than they paid.
* The long side has no cap, and the short side's offsetting value shrinks as it rises.
→ [Full NAV math](/concepts/nav)
## P\&L of a paired position
Suppose you mint 100 cUSDC of QQQ at 480 (`L = S = 480`, γ = 1, fees ignored):
```
Mint: 100 cUSDC → 0.10417 QQQL @ 480 + 0.10417 QQQS @ 480
```
If QQQ rises 10% to 528:
```
L_NAV = 528
S_NAV = 230,400 / 528 = 436.36
Position value = 0.10417 × 528 + 0.10417 × 436.36
= 55.00 + 45.45
= 100.45 cUSDC
```
If QQQ falls 10% to 432:
```
L_NAV = 432
S_NAV = 230,400 / 432 = 533.33
Position value = 0.10417 × 432 + 0.10417 × 533.33
= 45.00 + 55.56
= 100.56 cUSDC
```
The paired position has **near-zero net delta**. The small gain either way is the curvature of `K / P`, and holding the short leg pays volatility decay, which works against that gain. To take a directional view, sell one of the legs.
## Selling a leg
Each leg has its own bin book (`QQQL/cUSDC` and `QQQS/cUSDC`):
* Sell QQQL (or buy QQQS) to express a bearish view; buy QQQL (or sell QQQS) for a bullish one.
* While the market is open, trades go through a committed order, at any size. It fills at a keeper push after the commit, at NAV ± the committed spread (10 bps floor). While the price is stale, small amounts hit the book instantly.
A common flow:
```
1. mint_paired(100 cUSDC) at QQQ = 480 → 0.104 QQQL + 0.104 QQQS
2. sell the S leg: 0.104 QQQS → about 50 cUSDC, less the book's spread
→ net: 0.104 QQQL + about 50 cUSDC
(long-only exposure, fully funded)
```
You started with 100 cUSDC. You now hold about 50 cUSDC of long QQQ exposure and about 50 cUSDC in hand, minus fees and the spread. The cUSDC came from selling the short leg into the protocol's standing bid.
The reverse (sell QQQL, hold QQQS) gives you short-only exposure.
## What if NAV and book price disagree?
They can't drift far apart. Committed orders price off NAV directly, and the books fill only while the price is stale. The keeper centres each book on NAV plus a small inventory shift, and repositions it once NAV drifts past the gate (at most 20 bps by default). The shift is one dollar amount, added to one leg's centre and subtracted from the other's, so the pair still trades at NAV and no mint/redeem arbitrage opens. See [Peg maintenance](/keeper/arb-paths).
While prices are pushing, a fast move can't be traded against a book, because the books don't fill then. Off-hours, if the underlying moves on another venue before the keeper's guard pulls the stale side, a trader can lift the in-the-money bins. The loss is capped at those bins' deliberately lean depth. The books charge real bin-ladder slippage, so near NAV there is rarely anything to take.
## Decimals and minimums
| Token | Decimals | Minimum |
| - | - | - |
| cUSDC (collateral) | 6 | `10_000_000` base units = 10 cUSDC per mint |
| L tokens | 6 | n/a (set by the cUSDC amount) |
| S tokens | 6 | n/a (set by the cUSDC amount) |
The program enforces the minimum on both sides: a mint must deposit at least 10 cUSDC, and a redeem must be worth at least 10 cUSDC (`BelowMinimum`).
## Token metadata
Long and short SPL mints have **Metaplex token metadata** (name, symbol, image). Wallets and explorers show them as, for example, "QQQ Long" / "QQQS" with the Continuum logo.
→ [Live markets table](/markets/live) lists every long/short mint pubkey.
## Common questions
**Q: Do paired tokens need ERC-20-style approvals?**
No. They are SPL tokens. Solana uses Associated Token Accounts (ATAs); there is no `approve` step.
**Q: Can I transfer the paired position to another wallet?**
Yes. Both L and S are standard SPL tokens, so you transfer them like any other Solana token. The receiver can redeem them at NAV.
**Q: Do I have to redeem in pairs?**
Yes, in the sense that `redeem_paired` needs some of each leg: both `l_amount` and `s_amount` must be above zero. The amounts don't have to match, because redemption is **value-weighted**: you receive the NAV value of whatever you burn.
**Q: What happens if I lose access to half the pair?**
The program won't redeem one leg alone. Sell it with a committed order while the market is open or on its bin book while it is closed, or get some of the other leg and redeem the two together.
**Q: Are there fees on transfers?**
No. Continuum charges fees at mint and redeem (`mint_fee_bps`, `redeem_fee_bps`). The keeper posts the same value for both, 10 to 39 bps across the 13 live markets as of 13 Oct 2026, and the program multiplies it by 1.5 in `ProxyMode` (QQQ: 18 bps posted, 27 bps charged). Transfers are free at the protocol layer; you pay normal Solana fees only.
# Risk states
Source: https://docs.continuum.markets/concepts/risk-states
The per-market risk regime, the fee and confidence surcharges each state applies, why devnet runs in ProxyMode, and the staleness gate that freezes mint and redeem off-hours.
Every market carries a `RiskState` on its [`Market`](/programs/mint-redeem#market-account) account. The keeper writes it with each price push (`update_risk_state_v2`). The state sets two surcharges, one on the fee and one on the confidence band, and decides whether mint and redeem are open at all. A separate staleness gate freezes mint and redeem when pushes stop, most often because the underlying market has closed.
On devnet, every market runs in **`ProxyMode`** all the time, by design. `ProxyMode` means the program prices off the keeper-pushed `user_twap_price` instead of reading a Pyth account. `Normal` is the mainnet steady state, where mint and redeem read Pyth directly.
## The states
| State | Meaning | User mint | User redeem | Price used |
| - | - | - | - | - |
| `Normal` | Pyth-direct steady state (mainnet) | Open | Open | Pyth account, at most 60 s old |
| `ProxyMode` | Pricing off the keeper-pushed price (devnet) | Open | Open | `user_twap_price`, at most 300 s old |
| `Stress` | Oracle unreliable, defensive | Blocked by default (`MintNotAllowedInStress`) | Blocked by default (`RedeemNotAllowedInStress`) | `user_twap_price` |
| `Recovery` | Wind-down after `Stress` | Open | Open | `user_twap_price`, at most 300 s old |
`Stress` blocks both sides because `allow_mint_in_stress` and `allow_redeem_in_stress` default to false, and both are false on all markets, 14 when checked on 10 Oct 2026 and 13 live now. The market authority can turn either on with `configure_risk_fees`. A mint in `Stress` is then also held to `stress_mint_cap` per transaction. That cap is 0 (no cap) on every market.
## Fee and confidence multipliers
Each state multiplies the market's posted fee and its confidence band. Both multipliers are surcharges on top of the posted values, and every live setting is 1× or more.
| State | Fee multiplier | Confidence multiplier |
| - | - | - |
| `Normal` | 1× | 1× |
| `ProxyMode` | 1.5× (`proxy_fee_multiplier`) | 1.5× (`proxy_confidence_multiplier`) |
| `Stress` | 3× (`stress_fee_multiplier`), if trading is allowed | 3× (`stress_confidence_multiplier`) |
| `Recovery` | Falls from 3× to 1× over 1 hour | Falls from 3× to 1× over 1 hour |
These are the defaults, and the values on every live market as of 10 Oct 2026. The market authority sets the fee multipliers with `configure_risk_fees` and the confidence multipliers with `configure_worst_case_quoting`. The confidence multiplier applies only where worst-case quoting is on; see [NAV math](/concepts/nav#worst-case-quoting).
On devnet the 1.5× fee multiplier always applies: QQQ posts an 18 bps fee and is charged 27 bps.
The `Recovery` decline is linear and starts when the market enters `Recovery`. The program sets `risk_state_updated_at` only when the state changes, so later pushes in `Recovery` don't restart the clock. The keeper holds `Recovery` for an hour after leaving `Stress`, so the surcharge is back to 1× by the time the keeper moves the market to its next state.
### Transition rules
The on-chain transition matrix stops a compromised keeper from skipping recovery: **`Stress` can only move to `Recovery`** (or stay in `Stress`). A direct `Stress` → `Normal` or `Stress` → `ProxyMode` jump is rejected, which would otherwise drop the surcharge from 3× straight away. Every other transition is allowed: `ProxyMode` and `Recovery` move freely, and `Normal` can be entered from any state except `Stress`. Each push is also capped on price (a 25% move limit); see [Oracle](/concepts/oracle#what-one-push-contains).
## The staleness chain (off-hours behavior)
Risk states describe the oracle's *quality*. A separate chain handles its *absence*, most often when the underlying market closes for the night or the weekend:
```
underlying closes
→ source publish time > 120 s old keeper withholds the push
→ twap_updated_at > 300 s old MAX_TWAP_AGE_SECS gate trips
→ mint and redeem revert TwapNotAvailable
→ books switch to free-float books off-hours price discovery
```
The program enforces the freeze, but only the keeper's silence causes it. Any accepted push resets `twap_updated_at`, so the freeze lasts exactly as long as the keeper withholds. While frozen, mint and redeem revert in every state except `Normal`, the bin books, which fill only while the price is stale outside `Normal`, trade against free-float books, and committed orders can't settle; the keeper refunds each one about 5 s after its 180 s TTL. The first accepted push at reopen re-anchors NAV. The full chain is on [Oracle](/concepts/oracle#the-staleness-chain-what-happens-when-the-market-closes).
If you only check one thing before you submit a mint or redeem, check `market.twap_updated_at`.
## What you should do in each state
### `ProxyMode` (devnet)
Operate normally. Fees carry the 1.5× surcharge, and quotes carry a 1.5× confidence markup on both legs, usually a few bps. If you run a bot, remember that a mint or redeem needs a push no more than 300 s old.
### `Stress`
Mint and redeem are blocked by default (`MintNotAllowedInStress`, `RedeemNotAllowedInStress`). Committed orders still fill while the keeper pushes prices, and the bin books open once the price is more than 300 s old. If you build a UI, say so:
> "QQQ is in Stress mode. Minting and redeeming are paused; you can still trade QQQL and QQQS."
### `Recovery`
Mint and redeem reopen. Fees and confidence markups start at 3× and fall evenly to 1× over the first hour.
## Reading risk state
```typescript theme={null}
const market = await mintRedeem.account.market.fetch(marketPDA);
// market.riskState is an enum object: { normal: {} } | { proxyMode: {} } | ...
const riskState = Object.keys(market.riskState)[0];
const priceAge = Date.now() / 1000 - market.twapUpdatedAt.toNumber();
console.log("Risk state:", riskState);
console.log("Price age (s):", priceAge.toFixed(0), priceAge > 300 ? "(frozen)" : "(fresh)");
console.log("Posted fees (bps):", market.mintFeeBps, "/", market.redeemFeeBps);
```
The risk state can lag reality by one keeper tick. Outside `Normal`, the price age is the authoritative "will my transaction go through" signal.
## State during maintenance
For a hard halt, the market authority sets `market.is_active = false` with `update_market`. Mint and redeem then revert (`MarketNotActive`), and so does `commit_swap` (`MarketInactive`). `pool_swap` and `settle_swap` don't check the flag, so pending orders can still settle and the books still fill while the price is stale; after the TTL anyone can send `cancel_swap` to refund an order, whatever the state.
## Read more
The price source, the push pipeline and the staleness chain in full.
What the program enforces independently of risk state.
# Solvency
Source: https://docs.continuum.markets/concepts/solvency
The paired-mint invariant, the convexity gap it leaves open, and the defenses sized against it, from lean books and keeper-posted fees to volatility decay, the posted buffer and the treasury backstop.
Continuum's solvency rests on one structural rule, one known extraction path and a set of defenses sized against that path. This page takes them in that order.
## The invariant: paired mint/redeem is solvency-neutral
Every user mint and redeem is **paired**. `mint_paired` deposits cUSDC and issues L and S worth exactly the deposit, less the fee, at the current NAV. `redeem_paired` burns some of each leg (both amounts above zero) and pays their NAV value. Because `S_NAV = K·γ / L_NAV`, one L plus one S is worth
```
f(P) = P + K·γ / P (P = L_NAV, K = initial_l_price × initial_s_price)
```
A paired operation moves collateral and claims by the same amount at the moment it happens. After that the collateral stays put while the claims move with the price. `f` is convex, with its minimum at `P = √(K·γ)`. At a fixed γ, a position of value `v` minted at price `P_m` is later worth `(v/2)·(P/P_m + P_m/P)`, which is at least `v`, so its claim rises with any price move away from `P_m`. There is no user-facing single-sided mint; an unpaired mint would leave the vault holding a one-way bet.
## The attack surface: the convexity gap
With no external venue to trade against, the only riskless extraction is the **paired round trip**: mint a pair, wait for the price to move, redeem. Its gross profit is that convexity gap.
Structurally, the protocol is **short the pair's convexity**, which means short volatility. Two charges work against the round trip. Volatility decay marks the short leg down by the realized variance of the path, and the fee hurdle taxes every mint and redeem. The keeper posts fees from 10 to 100 bps (10 to 39 bps across the 13 live markets as of 13 Oct 2026), the program multiplies them by 1.5 in `ProxyMode`, and the round trip pays them twice, plus the worst-case markup.
The result splits by asset class. For a broad index trading near its anchor price, the gap over realistic horizons stays below the hurdle. For a high-growth single name on an NVDA-like path, a flat at-NAV venue alone *can* be drained at scale: the gap compounds with the trend, and volatility decay barely charges a smooth trend. That is why the defenses below exist, and why single-name listings need all of them.
## The defenses
The CLP account also stores an `oi_cap` (250,000 cUSDC per market), but nothing enforces it, so it is not one of these defenses.
### 1. Depth-capped books
No instant venue exposes unlimited size at NAV. The bin books trade only while a market's price is stale, and carry lean, demand-sized depth (about \$500 of bids and at most \$750 of asks per side while the market is open; off-hours, at least \$500 a side, limited by a risk cap on that market's share of the treasury) with real slippage across bins. Extraction per round trip is capped by what's deployed, and what's deployed is deliberately small. See [CLP: lean by design](/concepts/clp#lean-by-design).
Committed orders have no size cap. They fill at a price set *after* the commitment, so they offer no stale quote to extract against, and they pay the keeper's vol-scaled committed spread. They refuse to fill while the market is below 100% collateralized at the settle price. The cash a large fill needs comes from the treasury when it settles. The instant oracle swap, which filled at the last pushed price, was retired on 11 Oct 2026.
### 2. Vol-informed fees
The keeper posts each market's mint and redeem fee from realized volatility:
```
fee_bps = clamp(fee_mult × half_spread, 10, 100) fee_mult = 1.0 on every market
charged_bps = fee_bps × risk-state multiplier 1.5× in ProxyMode
```
`half_spread` is the long book's half-spread from the keeper's [spread engine](/concepts/keeper#the-spread-engine-model-free), and the keeper posts the result as both the mint and the redeem fee. QQQ posts 18 bps and is charged 27 bps on devnet. Of every fee, 90% stays in the market's collateral vault, raising the collateral ratio directly, and 10% is the dev tax. Higher vol means a bigger convexity gap and a proportionally higher hurdle.
### 3. Posted overcollateralization buffer
Each market has a buffer target, which the keeper posts on-chain with `set_buffer_target`. The target collateral ratio is 100% plus the buffer. The buffer is the smallest one that keeps the chance of the collateral ratio falling below 100% at or under 1% a year, assuming the treasury tops the market back up within 12 hours. No buffer is below 1%, the program's minimum.
A job reruns that calculation once a day, after the US close: a jump-diffusion Monte Carlo of each market over the next year (100,000 paths, with market hours, overnight and weekend gaps and earnings) that starts from the market's live state:
* **Leg imbalance** β = (L − S)/(L + S) by value. It grows as the price drifts from the market's anchor, and it is what eats a buffer; the γ charge already covers the convexity.
* **γ**, the short leg's volatility decay.
* **The volatility regime**: recent daily volatility against the long-run calibration. It can only raise a target, by up to 2×; a calm month never lowers one.
Every path runs twice: once with the book left alone, and once with the keeper's [imbalance control](/concepts/keeper#imbalance-control-900s) redeeming the CLP's heavier leg whenever |β| leaves 0.05. A market whose live |β| is within 0.10 gets the second need, because the band is visibly holding. Past 0.10 it gets the first. With the band held, no market needs more than 0.63% (TSLA), so every buffer heads for the 1% minimum.
A target rises at once and falls at most 50 bps or 25% a day, whichever is larger. From 12 Oct 2026 the targets step down to 101% at that pace, which takes NVDA and TSLA about nine days. The keeper reads the job's output every 30 minutes and posts a target only when it differs from the value on chain. If the output is missing or older than 48 hours, the keeper posts a fixed fallback table computed for a book at its anchor.
| Market | Daily target, 12 Oct 2026 | Need with the band held | Fixed fallback |
| - | - | - | - |
| QQQ | 101.84% | 100.17% | 101.7% |
| SPY | 101.05% | 100.13% | 101% |
| XAU | 101.54% | 100.14% | 101.4% |
| XAG | 105.97% | 100.33% | 105.9% |
| AAPL | 103.33% | 100.27% | 102.9% |
| MSFT | 103.69% | 100.34% | 103.3% |
| GOOGL | 104.83% | 100.43% | 104.8% |
| AMZN | 106.06% | 100.48% | 106.1% |
| NVDA | 110.69% | 100.49% | 110% |
| TSLA | 112.40% | 100.63% | 112% |
| EUR, GBP, JPY | 101% | 100.06% to 100.09% | 101% |
The program clamps any target to \[100, 5000] bps over parity (101% to 150%).
These targets replaced a formula the keeper computed live from 40 minutes of realized volatility, `B = δ²/2 + |q|·δ + margin`, on 11 Oct 2026. A replay of the keeper's own prices showed that formula swinging from 4% to 50% within the hour and falling to 2% after restarts.
On chain the target gates only three things: the donor fee waiver, how much excess a market can shed to others (`withdraw_excess_to_clp` and `rebalance_collateral`), and the credit a donation earns for closing the gap to target. Mints and redeems don't depend on it. The keeper's [treasury backstop](#treasury-backstop) fills markets toward it.
### 4. Volatility decay (the γ-index)
The convexity the protocol is short lives entirely in the short leg: `S_NAV = K/P` is convex, and `L_NAV = P` is linear. γ is the live mechanism that charges for it. The keeper ratchets γ down by realized variance, which marks short claims down and raises the collateral ratio directly. At `GAMMA_DECAY_COEF` = 1.0 the charge is break-even against the short leg's convexity on a choppy path.
```
S_NAV = (K / P) × γ γ ratchets down by realized variance
```
It charges **realized variance** and ignores direction. A smooth directional run barely moves γ, so it doesn't cover a single name trending away from its anchor. That case rests on lean depth, the committed spread, fees and asset selection. If collateral still falls short, the [treasury backstop](#treasury-backstop) tops the market up, and pro-rata redemption is the last resort. γ is always on with no kill switch, only ratchets down, is floored at 0.5 per epoch, and **never touches longs**. See [NAV → Volatility decay](/concepts/nav#volatility-decay).
### 5. `reanchor_gamma` is not a spot re-anchor
`reanchor_gamma` renews γ's budget. Only the market authority can call it, at least 14 days after the previous one. It folds γ into `initial_s_price` and resets γ to 1.0, so every claim is unchanged at every price. It collects and returns nothing; it lets decay keep charging once γ nears its floor. It does not move the curve's minimum to the current price. A re-anchor to spot would revalue holders' positions, and the program has no instruction for it.
## How the collateral ratio refills
Five channels push the ratio up over time:
1. 90% of every mint and redeem fee stays in the vault, immediately.
2. Each volatility-decay step marks short claims down.
3. When the price moves back toward a position's mint price, its claim falls and the vault over-collateralizes passively.
4. Collateral can be added directly. Anyone can call `donate_to_vault`, and the market authority can call `fund_collateral`, which grants no fee waiver and needs no fresh price. The CLP's `backstop_market` moves cUSDC from the treasury into a market's vault the same way. CLP trading profits reach a market's collateral only through these. The keeper calls `backstop_market` when a market falls below 100% or stays short of its target (see [Treasury backstop](#treasury-backstop)), and calls neither of the others. On 11 Oct 2026, `fund_collateral` brought 13 markets up to their 102% target; XAU was already above it.
5. Harvested collateral yield is added to `total_collateral` by `harvest_collateral_yield`.
On devnet, collateral yield is emulated. Every live market has `yield_enabled = true` as of 11 Oct 2026. Each hour the keeper mints the cUSDC that its mainnet lending plan would have earned on a market's collateral and harvests it into that market, which raises its collateral ratio. Nothing is actually lent on devnet.
## The treasury
The CLP's global vault (`5GSTKN4u5LfropcnQQoqFht48JMaF1SzZzBcgDNL5VKy`) is the protocol treasury. It held about 22.13M cUSDC after the keeper's returns of 11 Oct 2026: 5,534,820.82 from funding, plus idle CLP cash that the keeper returned to it. It grew to about 40M by 12 Oct, and the devnet keeper is now burning it down: it held 36.1M cUSDC on 13 Oct 2026.
Moving treasury cUSDC into a market is fast. `backstop_market` puts it straight into a market's vault and credits `total_collateral`. It has no rate limit and works while withdrawals are frozen, because the cUSDC stays inside the protocol.
Taking cUSDC out of the protocol is slow. `admin_withdraw` pays only one named account. Its allowance refills at a rate the keeper keeps in proportion to the balance, so emptying the treasury takes 24 h however much it holds (see [Keeper → Treasury withdrawal rate](/concepts/keeper#treasury-withdrawal-rate-900s)). Loosening any limit waits 24 h. A guardian key can freeze withdrawals and veto a pending loosening, but it can't move funds. See [clp → Treasury](/programs/clp#treasury).
The CLP's working cash comes from the treasury too. The keeper keeps each market's CLP vault at what its books' bid refills can spend before the next top-up, \$1,250 per market, and returns the rest. Imbalance control's pair mint is paid from the treasury in the same transaction. Committed orders draw what they need when they settle. See [Keeper → Liquidity reallocator](/concepts/keeper#liquidity-reallocator-30s).
### Treasury backstop
The keeper's liquidity reallocator uses the treasury to top up a market's collateral with `backstop_market`, which pays into the market's collateral rather than its CLP vault. It reads the collateral ratio at the stored price:
* Below 100%, it fills at once, at least up to the market's target. Committed orders refuse to fill below 100%, so this is what lets them resume.
* Short of target by more than one confidence band for 5 minutes, it fills to the target plus the band. This is the same destination rule the cross-market rebalancer uses, and the rebalancer moves other markets' excess first.
The target the treasury fills to is the market's posted target, capped at 130%. MSTR, closed on 12 Oct 2026, gets none. Fills are not returned automatically: a later surplus stays as a cushion, which the rebalancer can move between markets.
The keeper's yield allocator counts the treasury as first-loss capital when it caps each lending venue. See [Keeper → Yield allocator](/concepts/keeper#yield-allocator-300s).
## Lending venues and redemption timing
On mainnet the keeper lends each market's collateral to USDC lending venues and keeps a liquid share in the vault for redemptions: the nightly job's share of 1% to 10% of collateral, or 3% when that file is missing or over 48 hours old, sized to one short recall cycle of redemptions ([Keeper → Yield allocator](/concepts/keeper#yield-allocator-300s)). Devnet has no lending venues, so nothing is lent there.
A redemption larger than the vault's cash waits for a recall, and each venue limits how fast money comes back. The limits on 12 Oct 2026:
| Venue | Free cash (utilisation) | Withdrawal limit, shared by every depositor |
| - | - | - |
| Kamino Main | \$7.0M (94%) | \$90M per 24 h window |
| Jupiter Lend | \$44.4M (91%) | 50% of supply at once, refilling over 6 h |
| Save | \$5.1M (77%) | \$10M per \~1.75 h across the pool |
Free cash runs out before any of these limits does. Each venue's admins can also pause withdrawals. MarginFi, which the keeper no longer lends to, paused all of them for about 79 hours from 1 April 2026, then wrote lenders down 1%.
The r58 study replayed these rules against 197 days of venue data, March to October 2026. With Jupiter Lend's free cash at its current utilisation, a redemption of up to 10% of a market's collateral was paid within one 5-minute keeper cycle on every day, even with every depositor withdrawing at once. Under a rougher path for Jupiter's free cash, 1–2% of days missed the 5 minutes. A single holder redeeming 25–50% of a market could wait hours to days in a venue-wide run, or until a frozen venue reopens. Redemption sizes and pause odds are assumptions, because Continuum has no redemption history yet.
A `redeem_paired` the vault can't pay reverts with `InsufficientLiquidCollateral`. The holder then queues the redemption with `request_redemption`. The keeper's settler pays it in parts: from the vault's cash, then from a recall inside the same transaction, and for what the venues can't release in time from a treasury advance (`advance_to_market`, capped per market by `max_advance_bps`). The advance cash is not counted in `total_collateral`, and the keeper repays it from the market's own next recall. A venue loss is charged to the outstanding advance first, because the treasury is first-loss capital. The owner can cancel the unpaid rest at any time. See [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet).
## Supporting structural protections
* Users can only mint in pairs. `keeper_mint_single` and `keeper_redeem_single` need `market.keeper_authority`, and `redeem_single_for_vault` accepts only the market's CLP PDA. The keeper's single-sided mint collects full NAV for the leg it mints.
* Price writes are bounded. `update_risk_state_v2` rejects a move of more than 25% per push and a source publish time more than 300 s old, so a compromised keeper key can't teleport NAV. On mainnet the keeper adds its own 10% envelope before pushing.
* A staleness gate stops pricing off a dead feed. Outside `Normal`, mint and redeem revert with `TwapNotAvailable` once the last push is more than 300 s old. See [Risk states](/concepts/risk-states).
* Swap guards run on every oracle-priced fill. `settle_swap` checks price age, confidence, deviation and a 100% collateral floor. `pool_swap` trades only while the market's price is stale, under a rolling notional cap where one is set.
* Each market keeps its own ledger and its own vault, and on 11 Oct 2026 every vault's balance equaled its ledger. The same day's upgrade removed the shared unified vault from the program. Every outflow is limited to the market's own liquid share, `total_collateral − deployed_to_yield`, so one market can't spend another's backing. The rebalancer moves only collateral above the source's posted target, and the program caps each move at that amount.
* Redemption goes pro rata in a shortfall. If a tail move pushes the collateral ratio below 100%, redeem pays `nominal × min(1, CR)`. The vault can't be overdrawn, and every remaining holder gets the same ratio, so there's no first-mover advantage and no bank-run dynamic.
## What this means for users
**You can't lose more than your collateral.** A paired position is fully prepaid, with no margin and no liquidations. Redeeming needs both legs (each amount above zero) and a price push no more than 300 s old, so it depends on the keeper pushing prices. While the vault is fully collateralized you get NAV, less the fee and markup; in a shortfall you get the pro-rata share. A single leg exits through a committed order while the market is open, or through its bin book while its price is stale.
## What this means for builders
A lone leg can't be redeemed by itself. If you receive 100 QQQS in payment, you can sell it through a committed order while QQQ is open or on the QQQS book while it is closed, or redeem it together with some QQQL. The things to monitor are **risk state** (`Stress` blocks mint and redeem by default) and **price freshness** (mint and redeem freeze off-hours when pushes stop).
→ [Risk states](/concepts/risk-states) · [Reading state](/flows/read-state)
# Mint paired tokens
Source: https://docs.continuum.markets/flows/mint
Deposit cUSDC and receive matched long and short tokens, each priced at a worst-case quote.
`mint_paired(collateral_amount)` moves cUSDC from the user into the market's collateral vault and mints the long (L) and short (S) tokens to the user. After the fee, half the value goes into each leg.
## Math
Prices and amounts use 6 decimals (`1_000_000` is 1 cUSDC). Every devnet market runs in `ProxyMode`, where the price `P` is `Market.user_twap_price`. Despite its name, that field holds the keeper's latest spot price, adjusted for corporate actions.
```
K = initial_l_price * initial_s_price
γ = s_gamma_index / 1e9 // short-leg volatility decay, at most 1
c = oracle_confidence * state_mult / 100 // 0 when worst_case_config.enabled is false
L_quote = P + c
S_quote = K / (P - c) * γ
fee_bps = mint_fee_bps * state_mult / 100
fee = collateral_amount * fee_bps / 10_000
net = collateral_amount - fee
qty_L = (net / 2) / L_quote
qty_S = (net - net / 2) / S_quote
```
Every division rounds down. Both quotes run against the user. L is priced at `P + c`, above its NAV, and S at `K / (P - c) * γ`, above its NAV too, so the user gets slightly fewer of each. `state_mult` is 150 (1.5×) in `ProxyMode`; the other states are in the [risk-state table](#behavior-across-risk-states).
A worked example: SPY's last push before the weekend, at 20:10 UTC on 9 Oct 2026, set `P` to 778.569999 with `oracle_confidence` 0.123159 and γ 0.99854103. That gives `c` = 0.184738, an L quote of 778.754737 and an S quote of 655.818093 (S's NAV was 655.662481). At those quotes a 1,000 cUSDC mint pays a 24 bps fee of 2.40 cUSDC and receives 0.640509 SPYL and 0.760576 SPYS.
QQQ is the exception. As of 10 Oct 2026 its `worst_case_config.enabled` is false, so `c` is 0 and both legs are quoted at NAV. The other 12 live markets have it on (read on chain, 13 Oct 2026).
## Fees
The keeper posts one fee per market and writes it to both `mint_fee_bps` and `redeem_fee_bps`. The value is `clamp(fee_mult × half-spread, 10, 100)` bps. The half-spread comes from the keeper's volatility model for that market, and `fee_mult` defaults to 1.0. As of 13 Oct 2026 the posted fees on the 13 live markets run from 10 bps (EUR, GBP, JPY) to 39 bps (TSLA); QQQ posts 18 bps, SPY 16 and NVDA 31.
The program multiplies the posted fee by the risk-state multiplier. In `ProxyMode`, which every devnet market is in, that is 1.5×, so QQQ's 18 bps is charged as 27 bps. The multiplier never lowers a fee.
The dev tax, `fee × dev_tax_pct / 100`, goes to the fee recipient; `dev_tax_pct` is 10 on every market (10 Oct 2026). Since 12 Oct 2026 every live market's fee recipient is the treasury → [Addresses → Fee recipient](/reference/addresses#fee-recipient). The other 90% of the fee stays in the collateral vault and counts toward `total_collateral`, where it backs the market. [Fee waivers](#fee-waivers) can remove the fee entirely.
## Price freshness
Outside `Normal` state, mint reverts with `TwapNotAvailable` when `user_twap_price` is 0 or `twap_updated_at` is more than 300 seconds old. While the underlying market is closed (nights, weekends and holidays), the keeper normally withholds pushes, so minting stops until trading resumes. Any push restarts the 300-second clock. The [bin books](/flows/trade#bin-books) trade off-hours instead, and only then.
In `Normal` state the program reads the price from the market's oracle account instead. No devnet market runs in `Normal`.
## Accounts
Names are as in Anchor's TypeScript client.
| Account | Notes |
| - | - |
| `market` (mut) | Market PDA, seeds `[b"market", symbol]` under mint\_redeem |
| `user` (signer, mut) | Pays `collateral_amount` |
| `userCollateral` (mut) | The user's cUSDC account |
| `userLongAccount` (mut) | Token account for `market.long_mint`. Must already exist |
| `userShortAccount` (mut) | Token account for `market.short_mint`. Must already exist |
| `longMint`, `shortMint` (mut) | `market.long_mint` and `market.short_mint` |
| `collateralVault` (mut) | `market.collateral_vault`, the market's own vault (the Market PDA's cUSDC ATA) |
| `devTokenAccount` (mut) | A cUSDC account owned by `market.fee_recipient` (its ATA) |
| `oracle` | `market.oracle_address`. The program checks the address; outside `Normal` it reads no price from it |
| `tokenProgram` | SPL Token |
The program creates no accounts. The app adds an idempotent ATA creation for each leg in front of every mint, and the sample below does the same. The cUSDC account must exist and hold the amount. The program doesn't check the balance, so a short balance fails inside the SPL Token transfer.
## TypeScript
```typescript theme={null}
import { Program, BN } from "@coral-xyz/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import idl from "./mint_redeem.json"; // idl/mint_redeem.json in the docs repo
const program = new Program(idl, provider); // provider: an AnchorProvider for the user's wallet
const user = provider.wallet.publicKey;
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
program.programId,
);
const market = await program.account.market.fetch(marketPda);
const userCollateral = getAssociatedTokenAddressSync(market.collateralMint, user);
const userLongAccount = getAssociatedTokenAddressSync(market.longMint, user);
const userShortAccount = getAssociatedTokenAddressSync(market.shortMint, user);
// The fee recipient is a PDA, so allow an off-curve owner.
const devTokenAccount = getAssociatedTokenAddressSync(market.collateralMint, market.feeRecipient, true);
const mintIx = await program.methods
.mintPaired(new BN(100_000_000)) // 100 cUSDC
.accounts({
market: marketPda,
user,
userCollateral,
userLongAccount,
userShortAccount,
longMint: market.longMint,
shortMint: market.shortMint,
collateralVault: market.collateralVault,
devTokenAccount,
oracle: market.oracleAddress,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
// Both leg accounts must exist. Creating them idempotently does nothing if they do.
const tx = new Transaction().add(
createAssociatedTokenAccountIdempotentInstruction(user, userLongAccount, user, market.longMint),
createAssociatedTokenAccountIdempotentInstruction(user, userShortAccount, user, market.shortMint),
mintIx,
);
await provider.sendAndConfirm(tx);
```
For a full script with wallet setup, see [Mint and redeem example](/build/example-mint-redeem).
## Minimum mint size
`collateral_amount` must be at least 10 cUSDC (`10_000_000` base units). A smaller amount, zero included, fails with `BelowMinimum`.
## Errors
| Error | Code | Cause |
| - | - | - |
| `MarketNotActive` | 6000 | `market.is_active` is false |
| `BelowMinimum` | 6019 | `collateral_amount` is below 10 cUSDC, including 0 |
| `TwapNotAvailable` | 6012 | The price is 0 or more than 300 s old. Usually the underlying market is closed |
| `MintNotAllowedInStress` | 6009 | The market is in `Stress`. `allow_mint_in_stress` is false on every live market |
| `ExceedsStressMintCap` | 6011 | Only in `Stress` with minting allowed, when the amount exceeds `stress_mint_cap`. The cap is 0 (off) on every market |
| `OraclePriceUnavailable` | 6005 | A worst-case quote came out as 0 because the confidence band reached the price |
| `InvalidAmount` | 6001 | A token account, mint or the vault doesn't match the market |
| `Unauthorized` | 6021 | `devTokenAccount` isn't owned by `market.fee_recipient` |
| `AccountNotInitialized` | 3012 | A leg account doesn't exist yet |
| SPL Token error `0x1` | | The cUSDC balance is below `collateral_amount` |
See the [full error catalog](/reference/errors).
## Behavior across risk states
| Risk state | Price | Fee and confidence multiplier | Mint |
| - | - | - | - |
| `Normal` | The market's oracle account | 1× | Allowed |
| `ProxyMode` (every devnet market) | `user_twap_price`, at most 300 s old | 1.5× | Allowed |
| `Stress` | `user_twap_price`, at most 300 s old | 3× | Blocked by default |
| `Recovery` | `user_twap_price`, at most 300 s old | Falls linearly from 3× to 1× over the hour after `risk_state_updated_at` | Allowed |
These multipliers are surcharges. No state discounts the fee, and there is no size throttle outside `Stress`. `risk_state_updated_at` changes only when the state does, so the `Recovery` multiplier keeps falling while the keeper pushes prices. See [Risk states](/concepts/risk-states).
## Fee waivers
`mint_paired_with_waiver` takes the same accounts plus the user's `FeeWaiver` PDA, seeds `[b"fee_waiver", user, market]`. It charges no fee when all three of these hold, and the normal fee otherwise:
* The waiver hasn't expired (`waiver_end_at` is now or later).
* The price push is at most 300 s old.
* The vault's collateral ratio at the current price is at or above the market's target. The target is 100% plus `buffer_target_bps`, clamped to between 101% and 150%. An unset value reads as 102%. Each market's target is the daily Monte Carlo value the keeper posts, 101% or more. On 13 Oct 2026 it ran from 101% (SPY and the FX markets) to 109.3% (TSLA) (see [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer)).
Waivers were earned with `donate_to_vault`. The app stopped offering donations on 9 Oct 2026. Waivers already earned still apply, and the app uses them automatically.
## What you receive
```
dev_tax = fee * dev_tax_pct / 100 // dev_tax_pct = 10
user cUSDC account -collateral_amount
collateral vault +(collateral_amount - dev_tax)
fee recipient +dev_tax
user L account +qty_L
user S account +qty_S
market.total_l_supply += qty_L
market.total_s_supply += qty_S
market.total_collateral += collateral_amount - dev_tax
```
The program then emits `PairedActionExecuted` with `action` 0, the amounts minted, `fee_amount` and `dev_tax`.
L and S are classic SPL tokens with 6 decimals and no freeze authority. You can transfer them, trade them on the [bin books](/flows/trade), or hold them in other programs. There is no lockup.
## See also
The inverse: burn both legs and receive cUSDC.
A full script with wallet setup, IDL load, mint and verify.
# Order book
Source: https://docs.continuum.markets/flows/order-book
Trade long and short tokens with other traders on an on-chain order book, at any hour, with market, limit, fill-or-kill and immediate-or-cancel orders, and a trading key that signs without a wallet prompt.
Every long and short token has an order book on [Manifest](https://github.com/Bonasa-Tech/manifest), a permissionless on-chain order book. For the 13 live markets that is 26 books, each quoting one token in cUSDC. You trade there with other traders. The protocol takes no side, charges no fee, and Manifest charges none either.
The books are open at any hour. They matter most while the underlying market is closed: the [bin books](/flows/trade#bin-books) keep trading then, but only up to a small capped size, and [committed orders](/flows/trade) can't settle because no price push arrives. On the order book, size is limited only by who is on the other side. While a market is closed the app shows the bin book's quotes and traders' orders as [one book](#one-book-while-closed).
Book prices never feed NAV, the oracle, γ or the CLP. A thin off-hours book is easy to push around, so nothing outside the book reads its prices.
## One book while closed
While a market is closed, the app shows the bin book's quotes and traders' orders in one ladder and trades them as one book.
* A market order takes the cheapest offers first when you buy, and the highest bids first when you sell, whichever source holds them. At the same price, traders' orders fill before the pool.
* When both sources fill part of an order, it goes out as one transaction from your [trading account](/flows/trading-account): the pool's share through `swap_pool`, the rest as an immediate-or-cancel order on the book. If either part fails, neither happens.
* The [protection band](#protection-band) caps traders' offers, not the pool's quotes. After outside markets move, the pool's remaining side can sit outside the band and still be the honest price. A trader's offer outside the band fills only when it is cheaper than a pool quote the order takes anyway; on a sale, only when it bids more than one.
* A limit order priced through the pool's quote takes from the pool first, at the pool's price and never past your limit, and the rest rests on the book. A maker-only order priced through the pool's quote is refused, since it would fill at once.
* Your own resting orders show in the ladder, marked as yours, and your new orders skip them. Manifest has no self-trade prevention, so when a new order would match one of yours, the app cancels yours first, in the same transaction, and says so. Fill or kill can't be combined with that cancel: cancel your order first.
While a market is open, a market order is a [committed order](/flows/trade) at the next official price, unless traders' orders fill all of it now for less when you buy, or for more when you sell.
## Orders
| Order | What it does |
| - | - |
| Market | Fills now against resting orders and, while the market is closed, the pool's quotes. Traders' orders fill no worse than the [protection band](#protection-band) allows. Whatever can't fill inside that price is cancelled, not left on the book. |
| Market, fill or kill | The same, but the whole amount fills or nothing does. |
| Limit, until the reopen | Rests at your price and ends when the market reopens, so it can't be filled at a stale price once the first new print arrives. This is the default while a market is closed. |
| Limit, until cancelled | Rests until it fills or you cancel it. |
| Limit, fill now | Fills what it can at your price or better, then cancels the rest. |
| Maker only | A resting limit order that is cancelled instead of filling against the book when placed. The app refuses one priced through the pool's quote. |
Partial fills are normal on a book: a limit order can fill in several pieces as other traders arrive, and whatever is left keeps resting. Fills happen at the resting order's price, oldest first at each price.
A buy is sized in cUSDC to spend and a sell in tokens. A resting buy sets aside its limit price times its size until it fills or ends; a resting sell sets aside the tokens.
## Protection band
A market order takes traders' orders no worse than the leg's last posted price, widened by how far the price could plausibly have gapped by the reopen:
```
band = max(1%, K · σ_day · √min(1, t / T)) + E capped at 25%
```
* `σ_day` is the market's daily volatility forecast: its realized daily volatility over the last 20 trading days, from the keeper's nightly run. Closed-market gaps scale with it almost one for one, so the band widens in a turbulent market and narrows in a calm one.
* `t` is the time since the last price post, and `T` the scheduled closed period, about 17.5 hours on a weeknight and 65.5 over a weekend. The band reaches its full width at the scheduled reopen.
* `K` is the 99th to 99.5th percentile of the gap measured in `σ_day`, over 2013–2026 (2019–2026 for FX and metals). It is 2 after a weeknight and 3 over a weekend or holiday for indices and single stocks, 2 for an FX weekend, and 1 for metals. A weekend gap is only about one and a half times a weeknight's, far less than its extra hours suggest.
* `E` is 15% for a single stock within a week of the days it has usually reported on since 2022, when its gaps run about five times larger. Tesla's quarterly delivery reports count too. Other nights, and indices, FX and metals, get none.
With a 1% daily forecast, an index gets a 2% band after a weeknight and 3% at a weekend's reopen. A single stock at 2.5% a day gets 5% on an ordinary night and 20% in an earnings week. A limit order can sit anywhere. The app only warns when your price is outside the band.
The report days are the usual ones, not a calendar, so a release far from them gets no allowance, and neither does unscheduled company news: a market order at the reopen price can then be refused, and a limit order still goes in.
The pool's own quotes aren't held to the band. After outside markets move, the keeper pulls the quotes a trader could exploit, and the ones left on that side, further out, can honestly sit outside the band. A market order's pool share is bounded by its slippage instead.
## Trading account and trading key
Book orders go through your [trading account](/flows/trading-account), the same account that trades the pool and committed orders. One wallet approval sets it up and puts a trading key in your browser. For 30 days the key places and cancels orders without a wallet prompt. It can't send funds anywhere except your own wallet.
A stolen key could still trade badly with an accomplice on the other side of the book. So the program holds every key-signed order to a wider backstop band, `3 · σ · √(t / 30s) + 10%` clamped to 10%–25% around the posted price. Orders your wallet signs are not checked.
If your trading account can't cover an order, the app moves the difference from your wallet in the same transaction and asks for one approval. After a fill, proceeds stay in the account, ready for the next order; **Withdraw** sends them to your wallet, with no prompt.
## Stops, take-profits and TWAP
These run in your browser tab, not on-chain. They only work while a tab with the app is open and online.
* A stop loss or take profit sends a market order when the best bid (for a sale) or best ask (for a buy) reaches your price: across the pool and traders' orders while the market is closed, and across traders' orders and a committed order's price while it's open. Your own orders don't count.
* A TWAP splits an order into equal parts over a period you choose.
Each fill is a market order signed by your trading key and filled like one you place by hand, with 1% slippage. They draw only on the trading account: a stop that would need a wallet approval fails instead of waiting for one.
## Spam and overload
Anyone can place orders on a Manifest book, so spam is the main risk to plan for.
* Resting orders cost the placer rent. Every resting order takes an 80-byte block of the book account. Whoever grows the account pays about 0.00056 SOL per block, and it isn't refunded.
* Expired orders are cleared by the next trade. An order past its last slot stays in the account until a trade walks into it, and that trade pays the compute to remove it. Measured against both builds of Manifest, removal costs 382–722 compute units per order. One transaction clears about 1,900–3,600 of them.
* A side can be blocked, at a price. Someone who parks more expired orders at the top of a side than one transaction can clear would block new trades on that side. Doing it costs them roughly 1–2 SOL of rent per side, which they never get back.
* Recovery is cheap. Funds in a blocked book are never stuck: withdrawals and cancels don't walk the book. The recovery is to list a fresh book for that token, about 0.007 SOL.
* Global orders fill like any other. A Manifest global order is backed by one deposit per token, shared across books, and its funds move only when it is matched, so a taker must pass that token's global accounts. Without them Manifest stops an immediate-or-cancel order at the first global order and fails a resting order that would cross one. The app passes them whenever the side an order takes holds a global order, and the trading program checks every address against the market.
## Addresses
| | |
| - | - |
| Manifest | `MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms` |
| `trading` program | `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA` (devnet) |
| Book addresses | One long and one short book per market, kept in the app's own list. The `trading` program checks that a book trades the right token against cUSDC (`NotOurMarket`). |
| IDL | [`idl/trading.json`](https://github.com/continuum-markets/continuum-docs/blob/main/idl/trading.json) |
Trading account PDAs, under the `trading` program:
| Account | Seeds |
| - | - |
| Rules | `["account", owner]` |
| Trader (holds funds and seats, pays rent) | `["trader", owner]` |
# Reading on-chain state
Source: https://docs.continuum.markets/flows/read-state
Read prices, NAV, freshness, bin book depth, positions and pending orders from devnet.
Everything the app shows comes from on-chain accounts. This page covers the reads you need for a UI, a bot or a monitoring dashboard. The samples run against `https://api.devnet.solana.com` with the published IDLs (`idl/mint_redeem.json` and `idl/clp.json` in the docs repo).
## What lives where
| Data | Account | Program |
| - | - | - |
| Symbol, mints, fees, price, confidence, risk state, γ, supplies, collateral | `Market` PDA `[b"market", symbol]` | mint\_redeem |
| Swap guards, commit spread, rolling-window counter, OI cap | `Clp` PDA `[b"clp", market]` | clp |
| Book centre, bins and depth, one book per leg | `Pool` PDA `[b"pool", market, [side]]` | clp |
| Pending committed orders | `Order` PDA `[b"order", user, market, nonce]` | clp |
| Swap revenue from keeper and external traders | `RevenueAttribution` PDA `[b"revenue_attribution"]` | clp |
| A user's L, S and cUSDC balances | The user's token accounts | SPL Token |
The price is `Market.user_twap_price`. Despite the name, it holds the keeper's latest spot price, scaled to 1e6 and adjusted for corporate actions. The oracle program's `OracleConfig` accounts are not updated on devnet, so don't read prices from them.
`Clp.oi_cap` is set (250,000 cUSDC on every market as of 10 Oct 2026), but nothing enforces it. `Clp.current_oi` is not maintained and reads 0 on every market.
## Fetch one market
```typescript theme={null}
import { Program, BN } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import mintRedeemIdl from "./mint_redeem.json"; // idl/ in the docs repo
import clpIdl from "./clp.json";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const mintRedeem = new Program(mintRedeemIdl, { connection }); // read-only: no wallet needed
const clp = new Program(clpIdl, { connection });
const GAMMA_ONE = new BN(1_000_000_000);
const MAX_PRICE_AGE_SECS = 300; // mint and redeem revert with TwapNotAvailable past this
// Each leg's NAV in 1e6 units, rounded like the program. null while the market has no price.
function getNav(market: any): { lNav: BN; sNav: BN } | null {
const p: BN = market.userTwapPrice; // spot price, corporate-action adjusted
if (p.isZero()) return null;
const gamma = market.sGammaIndex.isZero() ? GAMMA_ONE : BN.min(market.sGammaIndex, GAMMA_ONE);
const k = market.initialLPrice.mul(market.initialSPrice);
return { lNav: p, sNav: k.div(p).mul(gamma).div(GAMMA_ONE) };
}
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
mintRedeem.programId,
);
const [clpPda] = PublicKey.findProgramAddressSync(
[Buffer.from("clp"), marketPda.toBuffer()],
clp.programId,
);
const [marketInfo, clpInfo] = await connection.getMultipleAccountsInfo([marketPda, clpPda]); // one RPC call
const market = mintRedeem.coder.accounts.decode("market", marketInfo!.data);
const clpAccount = clp.coder.accounts.decode("clp", clpInfo!.data);
const nav = getNav(market);
const priceAgeSec = Math.floor(Date.now() / 1000) - market.twapUpdatedAt.toNumber();
const riskState = Object.keys(market.riskState)[0]; // "normal" | "proxyMode" | "stress" | "recovery"
console.log({
symbol: market.assetSymbol,
riskState,
lNav: nav && nav.lNav.toNumber() / 1e6,
sNav: nav && nav.sNav.toNumber() / 1e6,
gamma: market.sGammaIndex.toNumber() / 1e9,
confidenceBps: nav && +((market.oracleConfidence.toNumber() / nav.lNav.toNumber()) * 10_000).toFixed(1),
priceAgeSec,
mintRedeemOpen: nav !== null && priceAgeSec <= MAX_PRICE_AGE_SECS && riskState !== "stress",
feeBps: { mint: market.mintFeeBps, redeem: market.redeemFeeBps },
totalCollateral: market.totalCollateral.toNumber() / 1e6,
committedOrders: clpAccount.swapGuards.enabled && !clpAccount.swapGuards.frozen,
commitSpreadBps: Math.max(clpAccount.commitSpreadBps, 10),
});
```
Output on devnet at 19:30 UTC on Saturday 10 Oct 2026 (Node 26):
```
{
symbol: 'QQQ',
riskState: 'proxyMode',
lNav: 751.27,
sNav: 304.599737,
gamma: 0.993214606,
confidenceBps: 2.5,
priceAgeSec: 83754,
mintRedeemOpen: false,
feeBps: { mint: 18, redeem: 18 },
totalCollateral: 1883044.749247,
committedOrders: true,
commitSpreadBps: 18
}
```
`totalCollateral` is this market's own collateral ledger, the figure the solvency checks read. Each market has its own vault, and on 11 Oct 2026 every vault's token balance equaled its ledger. A plain token transfer into a vault raises its balance without changing the ledger, so use `market.totalCollateral` for solvency.
`committedOrders` reads `swap_guards` from the `Clp` account. It is true for every live market since 11 Oct 2026. A committed order still needs a fresh price push to settle. The bin books are the reverse: they fill only while `riskState` isn't `"normal"` and `priceAgeSec` is over 300, and `pool_swap` fails with `PoolClosedWhileLive` otherwise.
## NAV
NAV is derived, not stored:
```
L_NAV = P
S_NAV = K / P * γ // K = initial_l_price * initial_s_price, γ = s_gamma_index / 1e9
```
`P` is `user_twap_price`, and `getNav` above computes both legs with the program's integer rounding. A price of 0 means the market has no price yet. Mint and redeem revert then, and the app leaves the market out. Don't fall back to the `initial_l_price` field.
Mint and redeem don't fill at NAV. They quote each leg against the user by the confidence band; see [Mint](/flows/mint#math) and [Redeem](/flows/redeem#math).
## Price freshness and risk
`twapUpdatedAt` holds the publish time of the price the keeper pushed, or the push's block time if the keeper gave none. Mint and redeem revert with `TwapNotAvailable` when that is more than 300 s ago or the price is 0, and `Stress` blocks both. The `priceAgeSec` and `mintRedeemOpen` fields in the sample check exactly these conditions. While the underlying market is closed the keeper normally withholds pushes, which is why the output above shows a closed market on a Saturday.
`oracleConfidence` is the keeper's confidence band in price units (1e6), from a GJR-GARCH volatility model. The program widens mint and redeem quotes by the band times the risk-state multiplier (1.5× in `ProxyMode`) on every market with worst-case quoting on. Before a push, the keeper checks each price against Hyperliquid and holds the push if they disagree.
## Batch-fetch all 13 markets
One `getMultipleAccountsInfo` call returns every live market:
```typescript theme={null}
const SYMBOLS = ["QQQ", "SPY", "XAU", "XAG", "NVDA", "AAPL", "EUR",
"MSFT", "TSLA", "AMZN", "GOOGL", "GBP", "JPY"];
const marketPdas = SYMBOLS.map((symbol) =>
PublicKey.findProgramAddressSync([Buffer.from("market"), Buffer.from(symbol)], mintRedeem.programId)[0],
);
const infos = await connection.getMultipleAccountsInfo(marketPdas); // one RPC call
const markets = infos.map((info) => (info ? mintRedeem.coder.accounts.decode("market", info.data) : null));
const now = Math.floor(Date.now() / 1000);
console.table(markets.map((m) => m && {
symbol: m.assetSymbol,
lNav: m.userTwapPrice.toNumber() / 1e6,
priceAgeSec: now - m.twapUpdatedAt.toNumber(),
mintFeeBps: m.mintFeeBps,
totalCollateral: Math.round(m.totalCollateral.toNumber() / 1e6),
}));
```
Output at the same time:
```
┌─────────┬─────────┬────────────┬─────────────┬────────────┬─────────────────┐
│ (index) │ symbol │ lNav │ priceAgeSec │ mintFeeBps │ totalCollateral │
├─────────┼─────────┼────────────┼─────────────┼────────────┼─────────────────┤
│ 0 │ 'QQQ' │ 751.27 │ 83754 │ 18 │ 1883045 │
│ 1 │ 'SPY' │ 778.569999 │ 84054 │ 16 │ 1691837 │
│ 2 │ 'XAU' │ 4193.83 │ 35701 │ 18 │ 1575888 │
│ 3 │ 'XAG' │ 60.8052 │ 34761 │ 25 │ 1279808 │
│ 4 │ 'NVDA' │ 229.28 │ 84654 │ 31 │ 1269810 │
│ 5 │ 'AAPL' │ 336.64 │ 83754 │ 22 │ 1270252 │
│ 6 │ 'EUR' │ 1.120099 │ 81114 │ 10 │ 1271043 │
│ 7 │ 'MSFT' │ 535.069999 │ 84654 │ 29 │ 1249813 │
│ 8 │ 'TSLA' │ 382.7 │ 84654 │ 38 │ 1249813 │
│ 9 │ 'AMZN' │ 262.43 │ 84654 │ 32 │ 1249813 │
│ 10 │ 'GOOGL' │ 351.659999 │ 84654 │ 32 │ 1249813 │
│ 11 │ 'GBP' │ 1.3232 │ 73589 │ 10 │ 1249813 │
│ 12 │ 'JPY' │ 158.287 │ 71799 │ 10 │ 1249813 │
└─────────┴─────────┴────────────┴─────────────┴────────────┴─────────────────┘
```
## Bin books
Each market has two books, one per leg. Derive their addresses from the market PDA:
```typescript theme={null}
const poolPdas = marketPdas.flatMap((m) =>
[0, 1].map((side) => // 0 = long book, 1 = short book
PublicKey.findProgramAddressSync(
[Buffer.from("pool"), m.toBuffer(), Buffer.from([side])],
clp.programId,
)[0],
),
);
const pools = (await connection.getMultipleAccountsInfo(poolPdas)) // all 26 books, one call
.map((info) => (info ? clp.coder.accounts.decode("pool", info.data) : null));
const book = pools[0]; // QQQ long
const total = (bins: BN[]) => bins.slice(0, book.numBins).reduce((s, x) => s + x.toNumber(), 0) / 1e6;
const centre = book.basePrice1E6.toNumber() / 1e6;
console.log({
centre,
askTokens: total(book.asks),
askValue: Math.round(total(book.asks) * centre),
bidUsdc: total(book.bids),
coreStepBps: book.stepBps,
wingStepBps: book.wingStepBps,
coreBins: book.coreBins,
numBins: book.numBins,
});
```
Output for the QQQ long book at the same time:
```
{
centre: 751.27,
askTokens: 10.12728,
askValue: 7608,
bidUsdc: 5000,
coreStepBps: 20,
wingStepBps: 300,
coreBins: 3,
numBins: 12
}
```
`basePrice1E6` is the book's centre; Anchor turns `base_price_1e6` into `basePrice1E6`, with a capital E. Asks are tokens of the leg and bids are cUSDC. The keeper sets the centre from NAV, shifted by the CLP's inventory, and off-hours it posts a demand-sized free-float book (see [Book management](/keeper/book-management)). Compare the centre with NAV to see how far a book sits from it. To price a trade, walk the bins as in [Quote and swap](/flows/trade#quote-and-swap).
## User position value
```typescript theme={null}
import { getAssociatedTokenAddressSync, unpackAccount } from "@solana/spl-token";
const owner = new PublicKey("YOUR_WALLET_ADDRESS");
const atas = [market.longMint, market.shortMint].map((mint) => getAssociatedTokenAddressSync(mint, owner));
const [longInfo, shortInfo] = await connection.getMultipleAccountsInfo(atas);
const longUnits = longInfo ? unpackAccount(atas[0], longInfo).amount : 0n; // base units
const shortUnits = shortInfo ? unpackAccount(atas[1], shortInfo).amount : 0n;
const { lNav, sNav } = getNav(market)!;
const value = (longUnits * BigInt(lNav.toString()) + shortUnits * BigInt(sNav.toString())) / 1_000_000n;
console.log("Position value at NAV:", Number(value) / 1e6, "cUSDC");
```
This values the position at NAV. A redeem pays the worst-case quotes minus the fee, and a sale pays the book's prices. To value a single leg at the book instead, use its book's centre.
## Pending committed orders
There's no index of open orders, and some RPC providers, Helius among them, disable `getProgramAccounts`. Track your orders yourself. When you commit, store the order PDA or the `nonce`; the PDA's seeds are the user, the market and the nonce, so those three find it again.
```typescript theme={null}
const nonce = new BN("1791660000000"); // the nonce you passed to commit_swap
const [orderPda] = PublicKey.findProgramAddressSync(
[Buffer.from("order"), owner.toBuffer(), marketPda.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
clp.programId,
);
const orderInfo = await connection.getAccountInfo(orderPda);
const order = orderInfo && clp.coder.accounts.decode("order", orderInfo.data);
if (!order) {
console.log("closed (settled, refunded or cancelled), or never created");
} else if (Date.now() / 1000 > order.expiresAt.toNumber()) {
console.log("expired: the keeper refunds it within seconds; anyone can send cancel_swap");
} else {
console.log("pending until", new Date(order.expiresAt.toNumber() * 1000).toISOString());
}
```
Every outcome closes the order account, so a missing account means the order is finished. The closing transaction's event says how: `OrderSettled`, `OrderRefunded` or `OrderCancelled`. If you lost the nonce, the `OrderCommitted` event in your commit transaction carries the order address. Where `getProgramAccounts` works, you can filter `Order` accounts by `user` at byte offset 8 and `market` at offset 40.
## Subscribing to live updates
```typescript theme={null}
const subscription = connection.onAccountChange(marketPda, (info) => {
const m = mintRedeem.coder.accounts.decode("market", info.data);
console.log("price", m.userTwapPrice.toNumber() / 1e6, "published at", m.twapUpdatedAt.toNumber());
}, "confirmed");
// Later:
await connection.removeAccountChangeListener(subscription);
```
The Market account changes on every keeper price push, every mint and redeem, and whenever the keeper posts a fee, γ or the buffer target. The keeper's price cycle runs every 15 s by default, and it normally withholds pushes while the underlying market is closed. For book updates, subscribe to the two `Pool` accounts; each book is one account.
## Common pitfalls
* After a mint, redeem or swap, fetch again once the transaction is confirmed. Values read before it are stale.
* `getMultipleAccountsInfo` returns null for a book that doesn't exist. All 28 books of the 14 markets then listed existed on 10 Oct 2026; with MSTR closed, 26 books belong to live markets. Take addresses from [Addresses](/reference/addresses) or derive them as above.
* Amounts in account state are base units with 6 decimals, for cUSDC and for every L and S token. Divide by `10 ** 6` to display them.
## See also
What `mint_paired` does on chain.
Setting up a project to make these reads.
# Redeem paired tokens
Source: https://docs.continuum.markets/flows/redeem
Burn long and short tokens together and receive cUSDC at worst-case quotes.
`redeem_paired(l_amount, s_amount)` burns L and S from the user and pays their value in cUSDC from the market's collateral vault. Both amounts must be above zero. They don't have to match in quantity or value, but together they must be worth at least 10 cUSDC at the redeem quotes.
There is no single-leg redeem. With either amount at 0 the program fails with `InvalidAmount`. To get out of a leg you hold on its own, [sell it](#exiting-one-leg).
## Math
```
L_quote = P - c
S_quote = K / (P + c) * γ
gross = l_amount * L_quote + s_amount * S_quote // must be at least 10 cUSDC
payout = min(gross, gross * total_collateral / total_claims)
fee_bps = redeem_fee_bps * state_mult / 100
fee = payout * fee_bps / 10_000
return = payout - fee
```
`P`, `c`, `K`, γ and `state_mult` mean the same as on [Mint](/flows/mint#math): the spot price, the confidence band, the constant product, the short-leg decay and the risk-state multiplier (1.5× in `ProxyMode`). The quotes run against the user again: L is priced below its NAV at `P - c`, and S below its NAV at `K / (P + c) * γ`. Every division rounds down.
`total_claims` values every outstanding L and S at the same quotes. While the vault holds at least that much, `payout` equals `gross`. If it holds less, every redeemer gets the same pro-rata share (see [below](#under-collateralization-pro-rata-redemption)). The fee is charged on `payout`.
The fee is the same posted value as the mint fee: QQQ posted 18 bps on 10 Oct 2026 and charged 27 bps in `ProxyMode`. A tenth of the fee (`dev_tax`) goes to the fee recipient, and the rest stays in the vault. See [Mint fees](/flows/mint#fees).
A worked example with QQQ's last push before the weekend, at 20:15 UTC on 9 Oct 2026: L's NAV was 751.27 cUSDC and S's was 304.599737, with γ at 0.99321461. QQQ quotes at NAV because its worst-case quoting is off. At that price, redeeming 0.663742 QQQL with 1.637066 QQQS gives a gross of 997.299325 cUSDC, a 27 bps fee of 2.692708 and a return of 994.606617 cUSDC.
## Price freshness
Redeem has the same gate as mint. Outside `Normal` it reverts with `TwapNotAvailable` when the price is 0 or more than 300 seconds old. While the underlying market is closed the keeper normally withholds pushes, so redeem pauses until trading resumes. The [bin books](/flows/trade) keep quoting off-hours if you need to exit then.
## Accounts
Redeem takes the mint accounts, with the same names and checks, plus one ignored slot at the end.
| Account | Notes |
| - | - |
| `market` (mut) | Market PDA |
| `user` (signer, mut) | Owns the L and S being burned |
| `userCollateral` (mut) | Receives the cUSDC |
| `userLongAccount`, `userShortAccount` (mut) | The accounts the burns come from |
| `longMint`, `shortMint` (mut) | From the market |
| `collateralVault` (mut) | Pays out the cUSDC |
| `devTokenAccount` (mut) | The fee recipient's cUSDC account; receives `dev_tax` |
| `oracle` | `market.oracle_address` |
| `tokenProgram` | SPL Token |
| `collateralAuthority` (optional) | Ignored. The program never reads this slot and accepts any account. Pass `null`, and Anchor sends mint-redeem's program ID |
Every market's vault is owned by its Market PDA, which signs the payout. The slot is left over from the unified vault, which the upgrade of 11 Oct 2026 removed. The IDL published that day gives the slot no seeds, so Anchor can't fill it in for you. Pass `collateralAuthority: null`, as below.
## TypeScript
Set up `program`, `user`, `marketPda`, `market` and the four token accounts as on [Mint](/flows/mint#typescript), then:
```typescript theme={null}
await program.methods
.redeemPaired(new BN(663_742), new BN(1_637_066)) // 0.663742 QQQL and 1.637066 QQQS
.accounts({
market: marketPda,
user,
userCollateral,
userLongAccount,
userShortAccount,
longMint: market.longMint,
shortMint: market.shortMint,
collateralVault: market.collateralVault,
devTokenAccount,
oracle: market.oracleAddress,
tokenProgram: TOKEN_PROGRAM_ID,
collateralAuthority: null, // ignored slot
})
.rpc();
```
## Exiting one leg
If you minted and then sold one side, you hold the other side alone, and the program won't redeem it on its own. Sell it instead:
* With a [committed order](/flows/trade#committed-orders) while the market is open. It fills at NAV minus the market's commit spread, at a keeper price push after you commit.
* On its [bin book](/flows/trade#bin-books) with `pool_swap` while the market's price is stale. The fill is instant, at the book's prices, with slippage that grows with size.
If you hold both legs, in any ratio, one `redeem_paired` can burn all of them. Your profit or loss is all the cUSDC you received, from redeems and sales, minus what you paid to mint.
For a pair, compare the redeem fee with the cost of selling. QQQ's redeem fee was 27 bps as of 10 Oct 2026. A committed sale pays the commit spread, 18 bps on QQQ that day, one leg at a time. The bin books trade only while mint and redeem are frozen; there the first bins of a QQQ book sit one 20 bps core step from the book's centre, which the keeper shifts with the CLP's inventory, and larger sales walk into wider bins.
## Limits
There is no size cap. A redeem can't burn more than the market's outstanding supply of either leg (`InvalidAmount`). The payout plus `dev_tax` must fit within the market's liquid collateral, `total_collateral` minus `deployed_to_yield`, and within the vault's token balance (`InsufficientLiquidCollateral`). When it doesn't, [queue a redemption request](#when-the-vault-cannot-pay-yet) instead. The program doesn't check your balances; burning more than you hold fails inside SPL Token.
## When the vault cannot pay yet
`redeem_paired` pays only from cash in the vault. On mainnet the keeper lends most of a market's collateral and keeps a liquid share in the vault, so a large redeem can ask for more than the vault holds right now. It then reverts with `InsufficientLiquidCollateral` (6022). Nothing is lost; the redemption has to go through a request instead:
1. **Request.** `request_redemption(l_amount, s_amount)` moves your L and S into escrow accounts owned by a `RedemptionRequest` PDA. It runs the same checks as a redeem (market active, not in `Stress`, a price at most 300 s old, at least 10 cUSDC) but burns and pays nothing.
2. **Settle.** The keeper's redemption settler pays the largest part of the request that the vault's cash covers, using `settle_redemption`. If more is needed it recalls the market's lending deposits in the same transaction, and for what the venues can't release in time it takes a loan from the treasury (`advance_to_market`) within the market's advance cap. Each part is priced at the redeem NAVs and fee at the moment it settles, with the same pro-rata haircut as `redeem_paired`, and burned from escrow. A request can therefore fill in several parts.
3. **Cancel.** `cancel_redemption` returns whatever is still unpaid and closes the request, at any time. It needs no keeper and no fresh price.
The settler is a keeper task that runs only when `REDEMPTION_SETTLE_ENABLED` is true (default false; see [Running a keeper](/keeper/running)). While it is off, a request waits until you cancel it. The accounts are in [mint-redeem → RedemptionRequest](/programs/mint-redeem#redemptionrequest).
## Under-collateralization: pro-rata redemption
The vault is meant to stay over-collateralized. In the tail, a large move on a lopsided book can still push collateral below the value of all outstanding tokens. Redeem keeps paying in that case, but pro-rata: the `payout` line in [Math](#math) multiplies `gross` by `min(1, CR)`, where `CR = total_collateral / total_claims`.
A redeem can never pay out more than the vault holds, because `gross` can't exceed `total_claims`. Every redeemer gets the same `min(1, CR)` factor, and the ratio stays the same for everyone left, so redeeming early gains nothing. While `CR` is at or above 100% the factor is 1 and you receive the full quote.
## Behavior across risk states
| Risk state | Price | Fee and confidence multiplier | Redeem |
| - | - | - | - |
| `Normal` | The market's oracle account | 1× | Allowed |
| `ProxyMode` (every devnet market) | `user_twap_price`, at most 300 s old | 1.5× | Allowed |
| `Stress` | `user_twap_price`, at most 300 s old | 3× | Blocked by default |
| `Recovery` | `user_twap_price`, at most 300 s old | Falls linearly from 3× to 1× over the hour after `risk_state_updated_at` | Allowed |
`Stress` blocks redeem unless the market's `allow_redeem_in_stress` is true, and it is false on every live market. The `Recovery` multiplier behaves as described on [Mint](/flows/mint#behavior-across-risk-states).
## Errors
| Error | Code | Cause |
| - | - | - |
| `InvalidAmount` | 6001 | `l_amount` or `s_amount` is 0, an amount exceeds that leg's outstanding supply, or an account doesn't match the market |
| `BelowMinimum` | 6019 | The redeem is worth less than 10 cUSDC at the quotes |
| `TwapNotAvailable` | 6012 | The price is 0 or more than 300 s old. Usually the underlying market is closed |
| `RedeemNotAllowedInStress` | 6010 | The market is in `Stress` |
| `InsufficientLiquidCollateral` | 6022 | The payout plus `dev_tax` exceeds the market's liquid collateral or the vault balance. Queue a [redemption request](#when-the-vault-cannot-pay-yet), or retry after the keeper recalls from lending |
| `MarketNotActive` | 6000 | `market.is_active` is false |
| `OraclePriceUnavailable` | 6005 | A worst-case quote came out as 0 |
| `Unauthorized` | 6021 | `devTokenAccount` isn't owned by `market.fee_recipient` |
| SPL Token error `0x1` | | Your L or S balance is below the amount |
See the [full error catalog](/reference/errors).
## Fee waivers
`redeem_paired_with_waiver` takes the same accounts plus the user's `FeeWaiver` PDA and skips the fee under the conditions on [Mint](/flows/mint#fee-waivers).
## What you receive
```
dev_tax = fee * dev_tax_pct / 100 // dev_tax_pct = 10
user cUSDC account +return
collateral vault -(return + dev_tax)
fee recipient +dev_tax
user L account -l_amount (burned)
user S account -s_amount (burned)
market.total_l_supply -= l_amount
market.total_s_supply -= s_amount
market.total_collateral -= return + dev_tax
```
The rest of the fee never leaves the vault. The program then emits `PairedActionExecuted` with `action` 1, `gross_collateral_amount` (the payout), `collateral_amount` (the return), `fee_amount` and `dev_tax`.
## See also
The inverse: deposit cUSDC and receive both legs.
Committed orders and the bin books.
# Trade
Source: https://docs.continuum.markets/flows/trade
Buy and sell long or short tokens with committed orders while a market is open, and on the bin books while its price is stale.
Continuum trades L and S tokens on its own venues in the CLP program. Which one fills depends on whether the market's price is live:
| Venue | Instruction | When it fills | Price | Size |
| - | - | - | - | - |
| Committed order | `commit_swap`, then `settle_swap` | While the keeper pushes prices, at the first push after the commit | NAV ± `max(commit_spread_bps, 10)` bps | No cap; bounded by CLP inventory and the collateral floor |
| Bin book | `pool_swap` | Only while the price is stale: the risk state isn't `Normal` and the last push is more than 300 s old | Walks the book's bins, with real slippage | The standing depth, small by design |
Since 11 Oct 2026, open-hours trading is committed orders only. `pool_swap` fails with `PoolClosedWhileLive` whenever the market's price is live. The CLP's instant oracle swap, `clp_swap`, is [retired](#retired-instant-oracle-swap): the instruction keeps its accounts and arguments, but every call fails with `SwapDisabled`. Committed orders work on every live market.
The app places every trade from your [trading account](/flows/trading-account). While the market is open it uses committed orders, at any size, so every in-hours fill is priced by a price push that happens after you commit. While the market is closed or its price updates are paused, no price push would arrive to settle a committed order, so the app shows the bin book's quotes, which fill instantly up to its capped size, and traders' orders on the [order book](/flows/order-book), where size is limited only by who is on the other side, as one book. A market order takes the cheapest offers first when you buy and the highest bids first when you sell, from either, in one transaction. When the keeper has pulled one side of the bin book because outside markets moved, the app warns anyone about to trade into the side that still prices the close.
Mint and redeem open and close a *paired* position (see [Mint](/flows/mint) and [Redeem](/flows/redeem)). The venues on this page buy or sell one leg.
## Bin books
`pool_swap` trades against a native bin book. Each market has two, one per leg: QQQ's long book and QQQ's short book are separate `Pool` accounts. A book lives inline in its `Pool` account, with up to 16 bins and no rent per bin. `asks[i]` holds tokens of the leg for sale above the centre price `base_price_1e6`, and `bids[i]` holds cUSDC below it.
The keeper sets the centre from NAV, shifted by the CLP's inventory, and reshapes the book as the price moves. Off-hours it posts a demand-sized free-float book, and a guard lets it pull bins only on the stale side. See [Book management](/keeper/book-management).
Bins use two step sizes: a fine core step (`step_bps`) for the first `core_bins` bins on each side, then a wider wing step (`wing_step_bps`). Ask bin `i`, counting from 0, sits `i + 1` steps above the centre and bid bin `i` sits `i + 1` steps below it. Each step up multiplies the price by `1 + step / 10_000`, and each step down divides by it. On 10 Oct 2026 each QQQ book had 12 bins: 3 core bins 20 bps apart, then 300 bps wing steps. A swap walks the bins outward from the centre. If the book runs out, the swap fills what it can and charges only for that, and `min_out` still applies.
`pool_swap` charges no swap fee; you pay the distance from the centre. It fills only while the market floats: the market's risk state must not be `Normal`, and its last price push must be more than 300 s old, the same age at which mint and redeem freeze. In any other case it fails with `PoolClosedWhileLive`, and you trade with a committed order instead. Every devnet market runs in `ProxyMode`, so on devnet a book opens 300 s after the last push and closes again at the next one.
There is no pool-level kill switch. A new pool stays disabled until the keeper posts its first shape, and after that nothing disables or freezes it: no instruction sets `pool.frozen`, and `set_pool_shape` always re-enables the pool. Freezing the CLP's swap guards stops committed orders but not `pool_swap`. The keeper can still empty a book by posting all-zero bins, and the [rolling-window cap](#rolling-window-cap) can pause it.
### Accounts
`pool_swap` takes `PoolSwapParams { direction, amount_in, min_out }`. `direction` is 0 to buy the leg with cUSDC and 1 to sell it for cUSDC. `amount_in` is cUSDC on a buy and tokens of the leg on a sell, both with 6 decimals.
| Account | Notes |
| - | - |
| `user` (signer) | The trader |
| `userUsdc` (mut) | The trader's cUSDC account |
| `userSynth` (mut) | The trader's token account for the book's leg. Must exist |
| `pool` (mut) | Pool PDA, seeds `[b"pool", market, [side]]` under clp. `side` is 0 for long, 1 for short |
| `clp` (mut) | The market's CLP PDA, seeds `[b"clp", market]`. Holds the swap guards and the rolling-window counter |
| `synthVault` (mut) | The pool's token account for the leg (its ATA) |
| `usdcVault` (mut) | The pool's cUSDC account (its ATA) |
| `tokenProgram` | SPL Token |
| `revenueAttribution` (mut) | PDA `[b"revenue_attribution"]` under clp (`A8mK2pnagabEcnkSiWieEZyQjjNWFYm1Z91dUVMXe6a4`) |
| `market` | The book's Market PDA under mint\_redeem, read for its risk state and price age. Must come last |
### Setup
Both samples on this page start from this setup.
```typescript theme={null}
import { Program, BN } from "@coral-xyz/anchor";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import mintRedeemIdl from "./mint_redeem.json"; // idl/ in the docs repo
import clpIdl from "./clp.json";
const mintRedeem = new Program(mintRedeemIdl, provider); // provider: an AnchorProvider for the trader's wallet
const clp = new Program(clpIdl, provider);
const user = provider.wallet.publicKey;
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
mintRedeem.programId,
);
const market = await mintRedeem.account.market.fetch(marketPda);
const [clpPda] = PublicKey.findProgramAddressSync(
[Buffer.from("clp"), marketPda.toBuffer()],
clp.programId,
);
```
### Quote and swap
```typescript theme={null}
// Mirrors the program's bin walk, so `out` matches the fill exactly.
function quotePoolSwap(pool: any, direction: 0 | 1, amountIn: bigint) {
const bins = direction === 0 ? pool.asks : pool.bids;
let price = BigInt(pool.basePrice1E6.toString());
let out = 0n;
let left = amountIn;
for (let i = 0; i < pool.numBins && left > 0n; i++) {
const step = BigInt(i < pool.coreBins ? pool.stepBps : pool.wingStepBps || pool.stepBps);
price = direction === 0
? (price * (10_000n + step)) / 10_000n // ask bin i: i + 1 steps above the centre
: (price * 10_000n) / (10_000n + step); // bid bin i: i + 1 steps below it
const reserve = BigInt(bins[i].toString());
if (reserve === 0n) continue;
const full = direction === 0
? (reserve * price) / 1_000_000n // cUSDC that empties this ask bin
: (reserve * 1_000_000n) / price; // tokens that empty this bid bin
if (left >= full && full > 0n) {
out += reserve;
left -= full;
continue;
}
const part = direction === 0 ? (left * 1_000_000n) / price : (left * price) / 1_000_000n;
out += part < reserve ? part : reserve;
left = 0n;
}
return { out, spent: amountIn - left }; // spent < amountIn when the book runs out
}
const side = 0; // 0 = long leg, 1 = short leg
const direction = 0; // 0 = buy with cUSDC, 1 = sell for cUSDC
const amountIn = 100_000_000n; // 100 cUSDC
const [poolPda] = PublicKey.findProgramAddressSync(
[Buffer.from("pool"), marketPda.toBuffer(), Buffer.from([side])],
clp.programId,
);
const [revenueAttribution] = PublicKey.findProgramAddressSync(
[Buffer.from("revenue_attribution")],
clp.programId,
);
const pool = await clp.account.pool.fetch(poolPda);
const quote = quotePoolSwap(pool, direction, amountIn);
const minOut = (quote.out * 9_950n) / 10_000n; // 0.5% slippage tolerance
const userSynth = getAssociatedTokenAddressSync(pool.synthMint, user);
await clp.methods
.poolSwap({ direction, amountIn: new BN(amountIn.toString()), minOut: new BN(minOut.toString()) })
.accounts({
user,
userUsdc: getAssociatedTokenAddressSync(pool.usdcMint, user),
userSynth,
pool: poolPda,
clp: clpPda,
synthVault: getAssociatedTokenAddressSync(pool.synthMint, poolPda, true),
usdcVault: getAssociatedTokenAddressSync(pool.usdcMint, poolPda, true),
tokenProgram: TOKEN_PROGRAM_ID,
revenueAttribution,
market: marketPda, // the mint-redeem Market, last
})
.preInstructions([
createAssociatedTokenAccountIdempotentInstruction(user, userSynth, user, pool.synthMint),
])
.rpc();
```
Against an unchanged book, `quote.out` is exactly what the swap returns. On Saturday 10 Oct 2026, with the market closed, it quoted 0.132577 QQQL for 100 cUSDC on the QQQ long book, and a simulated `pool_swap` returned the same.
Standing depth is small on purpose. It is sized to demand rather than TVL, because the losses to informed traders grow with the depth deployed. On Saturday 10 Oct 2026 the QQQ long book held 5,000 cUSDC of bids and 10.13 QQQL (about 7,600 cUSDC) of asks. The single-stock books held 1,000 to 1,250 cUSDC a side. Depth has since been resized to demand, at least 500 cUSDC a side. Larger orders belong in a committed order once the market opens, or on the [order book](/flows/order-book).
## Rolling-window cap
`pool_swap` is held to one notional cap per market, kept in the market's `Clp` account. Each fill adds its cUSDC amount to `swap_window_used`, and a fill that would take it past `window_cap_usd` reverts with `WindowCapExceeded`. The count resets once `window_secs` have passed since the window opened. As of 11 Oct 2026, QQQ, SPY and XAU are capped at 1,000,000 cUSDC per 3,600 s. The other 11 markets have no cap set, and 0 means uncapped. Committed orders don't count toward the cap.
## Committed orders
You put your input in escrow under an order account, and the order fills at a keeper price push after your commit, at NAV ± the commit spread:
```
spread = max(commit_spread_bps, 10) // per market, in the Clp account
buy: out = amount_in / (NAV * (1 + spread / 10_000))
sell: out = amount_in * NAV * (1 - spread / 10_000)
```
NAV is the leg's NAV at the settling push: `P` for the long leg and `K / P * γ` for the short leg (see [Mint](/flows/mint#math)). The keeper posts `commit_spread_bps` as `clamp(half-spread, 10, 500)` from its volatility model. As of 10 Oct 2026 it was 18 bps on QQQ, 15 on SPY and 18 on XAU.
The price is set after you commit, so nobody can trade against a stale quote. That is why committed orders have no size cap and skip the rolling window. They are bounded by the price checks at settle, the collateral floor and the CLP's inventory.
Committed orders need the market's swap guards (`Clp.swap_guards`, set with `configure_swap`) to be enabled and not frozen. Since 11 Oct 2026 they are on for all 13 live markets, with the same settings:
| Guard | Setting | Effect at settle |
| - | - | - |
| `max_staleness_secs` | 600 | The price must be at most 600 s old |
| `max_confidence_bps` | 5,000 | The confidence band must be at most half the price, so in practice this check is off |
| `max_deviation_bps` | 10,000 | Off. The price write itself refuses a move of more than 25% per push |
| `collateral_floor_bps` | 10,000 | The market must be at least 100% collateralized at the settle price |
Their spread (`spread_bps`) and window cap were left unchanged. `spread_bps` priced the retired `clp_swap`, and no live instruction reads it.
`commit_swap(side, direction, amount_in, min_out, nonce)` moves your input (cUSDC on a buy, tokens of the leg on a sell) into an escrow token account owned by the order PDA. It needs the market active (`MarketInactive`) and its swap guards enabled (`SwapDisabled`) and not frozen (`SwapFrozen`). Your payout account must already exist. The program checks it at commit, so settlement can't fail on a missing account.
`settle_swap` becomes valid once `market.twap_updated_at` is later than the commit, in a later second. It stays valid until `expires_at`. Anyone can call it, and the keeper sends it after the first push that qualifies. It fills at the market's current price, which may come from a later push than the first one after your commit.
* If the output misses `min_out`, the order is refunded in full and closed.
* If the market is below its collateral floor (a 100% collateral ratio at the settle price), or the price fails the checks above, settle reverts. The order stays pending and fills at a later push, if one passes before it expires.
* If the CLP lacks inventory, settle reverts and the order stays pending while the keeper provisions it.
While the underlying market is closed the keeper normally withholds pushes, so an order committed then can't settle and expires.
An order expires 180 s after the commit. After that, anyone can send `cancel_swap`. It returns the escrow to the owner's token account and the rent to the owner, and needs neither of their signatures. Before expiry nobody can cancel (`OrderNotExpired`). Cancel checks nothing else (no price, keeper, freeze or market state), so escrowed funds can always be recovered after expiry. The keeper refunds every expired order about 5 s after expiry, and the app shows a **Refund now** button if an order is still open 60 s after it.
Settling, refunding and cancelling all close the order and escrow accounts and return their rent to you.
On devnet the fill price (`market.user_twap_price`) and the freshness check (`market.twap_updated_at`) both come from keeper pushes, so a committed order trusts the keeper's price.
### Order account
PDA seeds `[b"order", user, market, nonce]` under clp, with `nonce` as 8 little-endian bytes. `nonce` is any u64 you pick; a fresh one per order lets you keep several open at once. Keep it, or the order address, so you can find the order later (see [Reading state](/flows/read-state#pending-committed-orders)).
```rust theme={null}
pub struct Order {
pub user: Pubkey, // offset 8 (after the discriminator)
pub market: Pubkey, // offset 40
pub side: u8, // 0 = long, 1 = short
pub direction: u8, // 0 = buy, 1 = sell
pub status: u8, // offset 74; 0 = pending, the only stored state
pub bump: u8,
pub escrow_bump: u8,
pub amount_in: u64,
pub min_out: u64, // a miss refunds in full
pub created_at: i64, // settle needs twap_updated_at > created_at
pub expires_at: i64, // created_at + 180; settle up to it, cancel after it
pub nonce: u64,
}
```
### Accounts: `commit_swap`
| Account | Notes |
| - | - |
| `user` (signer, mut) | Pays rent for the order and escrow, which comes back when the order closes |
| `market` | Market PDA under mint\_redeem. Fixes the leg mints |
| `clp` | CLP PDA, seeds `[b"clp", market]` |
| `order` (init) | Order PDA, as above |
| `inMint` | Mint of the input: cUSDC on a buy, the leg's mint on a sell |
| `escrow` (init) | Token account, seeds `[b"escrow", order]`, owned by the order PDA |
| `userIn` (mut) | Your input token account |
| `userOut` | Your payout token account. Must exist at commit |
| `clpVault` | The CLP's cUSDC account (ATA of the CLP PDA) |
| `tokenProgram`, `systemProgram` | |
### Accounts: `settle_swap` and `cancel_swap`
`settle_swap` takes `cranker` (signer: anyone, who pays the transaction fee and gets nothing), `user` (`order.user`, who receives the rent), `order`, `escrow`, `clp`, `clpVault` and `clpSynth` (the CLP's cUSDC and leg inventory), `market`, `userOut`, `userIn` and `tokenProgram`. All but `cranker`, `market` and `tokenProgram` are mutable.
`cancel_swap` takes `user` (the order's owner, who receives the rent; not a signer), `order`, `escrow`, `userIn` (the owner's token account for the escrowed mint, which receives the refund) and `tokenProgram`. All but `tokenProgram` are mutable. Whoever sends the transaction pays its fee.
### Commit and cancel
This sample buys QQQ's long leg with 10,000 cUSDC, starting from the [setup](#setup) above. It works on any live market while the market's price is live.
```typescript theme={null}
const orderAmount = 10_000_000_000n; // 10,000 cUSDC
const nonce = new BN(Date.now()); // any u64; keep it to find the order later
const [orderPda] = PublicKey.findProgramAddressSync(
[Buffer.from("order"), user.toBuffer(), marketPda.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
clp.programId,
);
const [escrowPda] = PublicKey.findProgramAddressSync(
[Buffer.from("escrow"), orderPda.toBuffer()],
clp.programId,
);
// Estimate the fill at the current price. settle_swap reprices at the settling push.
const clpState = await clp.account.clp.fetch(clpPda);
const spreadBps = BigInt(Math.max(clpState.commitSpreadBps, 10));
const nav = BigInt(market.userTwapPrice.toString()); // long-leg NAV, 1e6
const price = (nav * (10_000n + spreadBps)) / 10_000n; // a buy pays NAV * (1 + spread)
const orderMinOut = (((orderAmount * 1_000_000n) / price) * 99n) / 100n; // 1% tolerance
const userIn = getAssociatedTokenAddressSync(market.collateralMint, user);
const userOut = getAssociatedTokenAddressSync(market.longMint, user); // must exist before the commit
await clp.methods
.commitSwap(0 /* long */, 0 /* buy */, new BN(orderAmount.toString()), new BN(orderMinOut.toString()), nonce)
.accounts({
user,
market: marketPda,
clp: clpPda,
order: orderPda,
inMint: market.collateralMint, // the leg's mint on a sell
escrow: escrowPda,
userIn,
userOut,
clpVault: getAssociatedTokenAddressSync(market.collateralMint, clpPda, true),
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.rpc();
// Wait for the keeper's settle: watch userOut or the OrderSettled event.
// The keeper refunds an order about 5 s after expiresAt (commit time + 180 s).
// If it is still open after that, anyone can send the refund; it goes to the owner:
await clp.methods
.cancelSwap()
.accounts({ user, order: orderPda, escrow: escrowPda, userIn, tokenProgram: TOKEN_PROGRAM_ID })
.rpc();
```
The order emits `OrderCommitted`, then one of `OrderSettled` (with `out`, `nav` and `base_price`), `OrderRefunded` (a `min_out` miss) or `OrderCancelled`. Fills also emit `SwapExecuted`, whose `route` is 1 for `pool_swap` and 2 for a committed settlement. Route 0 belonged to the retired `clp_swap`.
## Retired: instant oracle swap
`clp_swap(side, direction, amount_in, min_out)` filled instantly from CLP inventory at NAV ± `spread_bps`, with no book and no slippage, on QQQ, SPY and XAU. It filled at the last pushed price, which is a stale quote between pushes, and it was retired on 11 Oct 2026. The instruction keeps its accounts and arguments, so existing clients still decode it, but every call fails with `SwapDisabled`. Use a committed order instead.
## Errors
| Error | Code | Where | Cause |
| - | - | - | - |
| `SwapDisabled` | 6003 | `pool_swap` | The keeper hasn't posted the pool's first shape yet |
| `SwapDisabled` | 6003 | `commit_swap` | The market's CLP swap guards aren't enabled |
| `SwapDisabled` | 6003 | `clp_swap` | Always: the instant swap is retired |
| `SwapFrozen` | 6004 | `commit_swap`, `settle_swap` | The CLP's swap guards are frozen |
| `SwapAccountMismatch` | 6006 | all | An account has the wrong mint, owner or address |
| `SlippageExceeded` | 6008 | `pool_swap` | The output is below `min_out` |
| `OrderExpired` | 6009 | `settle_swap` | Past `expires_at`; the order is refunded instead |
| `OrderNotExpired` | 6010 | `cancel_swap` | Not yet past `expires_at` |
| `NoFreshOraclePush` | 6011 | `settle_swap` | No price push since the commit |
| `OracleStale`, `OracleConfidenceTooWide`, `OracleDeviationTooLarge` | 6012 to 6014 | `settle_swap` | A price check failed; the order waits for a later push |
| `WindowCapExceeded` | 6015 | `pool_swap` | The rolling-window cap is used up |
| `CollateralFloorBreach` | 6016 | `settle_swap` | The market is below its collateral floor; the order waits for a later push |
| `InsufficientFunds` | 6018 | `pool_swap`, `settle_swap` | The book has nothing on that side, or the CLP lacks the inventory |
| `PoolClosedWhileLive` | 6044 | `pool_swap` | The market's price is live; use a committed order |
See the [full error catalog](/reference/errors).
## See also
Books, NAV and pending orders, read from chain.
Every instruction and account.
How the keeper shapes and re-centres the books.
How the keeper keeps book prices close to NAV.
# Trading account
Source: https://docs.continuum.markets/flows/trading-account
One account per wallet that trades every venue, committed orders, the bin books and the order book, with a browser trading key that needs no wallet prompt and can only ever send funds back to your wallet.
Every trade in the app goes through your trading account. You set it up once, fund it from your wallet, and then trade with a trading key that lives in your browser and signs without a wallet prompt. Hyperliquid works the same way: a deposit, then trading with a key that can trade but can't take funds anywhere else.
The account trades every venue:
| Venue | When | Price |
| - | - | - |
| [Committed orders](/flows/trade#committed-orders) | While the market is open | The next official price, plus a spread |
| [Bin book](/flows/trade#bin-books) | While the market is closed | The pool's quote, from the last official price |
| [Order book](/flows/order-book) | Any hour | What other traders set |
Tokens you buy stay in the account until you withdraw them. They're the same SPL tokens as anywhere else, and moving them to your wallet takes one click.
## Setting up
One wallet approval does three things:
1. Opens the account: a record of who may trade for you, and a trader address that holds your funds, book seats and orders.
2. Puts a trading key in your browser, made by the browser's crypto API with a private key that can't be exported, and stored for this site only.
3. Sends about 0.03 SOL: rent for book seats, orders and committed orders, and fees for the key's transactions. You can take what's left back.
The key lasts 30 days, the program's maximum. **Turn off** revokes it at once and sends its unused fee SOL back.
## What the key can do
| | Your wallet | Trading key |
| - | - | - |
| Trade: market and limit orders, the pool, committed orders | Yes | Yes |
| Cancel book orders | Yes | Yes |
| Move funds between the account and a book | Yes | Yes |
| Withdraw | Yes, only to your wallet | Yes, only to your wallet |
| Move funds in from your wallet | Yes | No |
| Grant or revoke the key | Yes | No |
Nothing the key signs can send funds anywhere except your own wallet. The program checks that every withdrawal pays your wallet's own token account, whoever signs it.
A stolen key could still trade badly with an accomplice on the other side of a book. So the program holds every key-signed book order, and every key-signed pool fill, to a backstop band around the posted price: `3 · σ · √(t / 30s) + 10%`, clamped to 10%–25%. A pool fill is judged on what actually moved, so a key can't force a fill deep into the book either. Committed orders fill at the official price, so there's no one to trade with. Orders your wallet signs are not checked.
There's no per-order limit. The program supports one, but it would bound each order, not the total, so a thief would just place more orders. The band is what limits the damage.
## Funding a trade
A trade draws on the account first. A book order uses that book's seat first, then the account. If neither covers it, the app moves the difference in from your wallet in the same transaction, and your wallet signs that one instead of the key. When the account's rent SOL is low, a wallet-signed trade tops it back up in the same approval.
After a fill, proceeds stay in the account, ready for the next trade. A committed order that can't fill is refunded to the account.
## Where a market order goes
* While a market is closed, the pool's quotes and traders' orders are [one book](/flows/order-book#one-book-while-closed). A market order takes the cheapest offers first when you buy and the highest bids first when you sell, from either, in one transaction. At the same price, traders' orders fill first.
* While a market is open, a market order is a committed order, estimated at the current price; it fills at the next one. Traders' orders take it instead only when they fill all of it now for less when you buy, or for more when you sell. While committed orders pause for a top-up, or one of yours is pending, the book still trades.
* A limit order rests on the order book. One priced through the pool's quote takes from the pool first, then rests the rest.
The ticket shows what the order gets and where it fills from; there is no venue to pick.
## While a market is closed
The pool quotes from the last official price. When outside markets move past its spread (Hyperliquid's stock perps, pre- and after-hours quotes, index futures, Coinbase's gold and euro tokens), the keeper pulls the side a trader could profit from, and those quotes leave the book. The side left behind still prices the close, so the app warns anyone trading into it: after a rise, a sale gets the pool's bid from before the move, and a limit order can wait for a higher one.
## Your balances
The trading panel shows the account's cash and tokens for the market you're on. The portfolio counts everything the account holds: its token accounts, its seats on every book, and what its open book orders have set aside. Mint, redeem and burn still spend from your wallet, so withdraw first if you need tokens there.
## Closing the account
**Close account** returns everything to your wallet. It first sends every balance, in the account's token accounts and in its book seats, back to your wallet with the key. Then one wallet approval sends the key's unused fee SOL back and closes the account, its token accounts and its rent. It waits while you have open book orders or a committed order in flight.
One cost never comes back: each book seat, and each book slot an order once needed, is an 80-byte block of rent that Manifest keeps. The seats stay yours, and work again if you open the account later.
## Program
`trading`, `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA` (devnet). IDL: [`idl/trading.json`](https://github.com/continuum-markets/continuum-docs/blob/main/idl/trading.json).
| Account | Seeds |
| - | - |
| Rules (`TradingRules`) | `["account", owner]` |
| Trader (holds funds, seats and orders, pays rent) | `["trader", owner]` |
| Instruction | Signer | What it does |
| - | - | - |
| `open_account` | Owner | Creates the rules account |
| `set_session`, `revoke_session` | Owner | Grants or revokes the trading key |
| `deposit` | Owner | Wallet → account |
| `withdraw` | Owner or key | Account → the owner's own token account |
| `withdraw_sol` | Owner | Trader's rent SOL → owner |
| `close_account` | Owner | Closes the empty token accounts and the rules account, and sends their rent and the trader's SOL to the owner |
| `claim_seat`, `deposit_to_market`, `withdraw_from_market` | Owner or key | Manifest seats |
| `place_orders` | Owner or key | Manifest orders and cancels; up to two global groups (mint, global, global vault, market vault, token program) as remaining accounts, each checked against the market |
| `swap_pool` | Owner or key | The CLP's `pool_swap` from the account |
| `commit_order` | Owner or key | The CLP's `commit_swap` from the account; the order's owner is the trader address |
A committed order placed from the account is owned by the trader address, so settlement pays its token accounts and refunds return there, the same as for any other order.
# Devnet info
Source: https://docs.continuum.markets/get-started/devnet
Cluster, program IDs, the cUSDC faucet, the 13 live markets, and how prices and off-hours behave on devnet.
Continuum runs on Solana **devnet**. Plans for mainnet are on the [Roadmap](/markets/roadmap).
## Cluster
```
RPC: https://api.devnet.solana.com
WS: wss://api.devnet.solana.com
Cluster: devnet
```
The public devnet endpoint is rate-limited. For sustained work, use a private RPC such as Helius, Triton or QuickNode.
## Program IDs
| Program | Devnet ID |
| - | - |
| `mint-redeem` | `5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4` |
| `oracle` | `5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC` |
| `clp` | `8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES` |
| `registry` | `Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau` |
| `faucet` | `9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY` |
| `trading` | `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA` |
The program source is not public. IDLs generated from the deployed source are published in the [docs repo](https://github.com/continuum-markets/continuum-docs/tree/main/idl), and each one is also available raw at `https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/.json`. Mainnet IDs will differ.
The registry is deployed and initialized, but it holds no markets and nothing reads it yet. Take market addresses from [Addresses](/reference/addresses).
## cUSDC (devnet collateral)
```
Mint: B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE
Decimals: 6
```
cUSDC is a test token, **not** real USDC. Its mint authority is a PDA of the faucet program, and it has no freeze authority. Mainnet will use real USDC (`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`).
These docs call the devnet collateral **cUSDC** to keep it apart from real USDC. Code, environment variables and account names often say "USDC"; on devnet they mean the test mint above.
## Faucet
The [`faucet`](/programs/faucet) program drips 1,000 cUSDC to a wallet once every 24 hours. The live `FaucetState` has `drip_amount = 1000000000` and `cooldown_seconds = 86400`.
In the app, click **Get 1000 cUSDC** on [/faucet](https://continuum.markets/faucet). From code, call the `drip` instruction; see [Faucet program](/programs/faucet). The recipient pays rent for its `DripRecord` account on the first drip, and a second drip inside 24 hours fails with `CooldownNotElapsed`.
## Live markets
Thirteen markets are live on devnet (MSTR was closed on 12 Oct 2026). All their addresses (mints, books, vaults) are on [Addresses](/reference/addresses).
| Symbol | Asset | Long mint | Short mint |
| - | - | - | - |
| `QQQ` | Nasdaq-100 ETF | `E4xJxBPyDXXJK2uqF17cq9kch1xHCTBZ6vjMxKWKANgZ` | `BisvPibkUJqix3ZzaeHCxJ2W7KqoroFpnyf55nzFoicG` |
| `SPY` | SPDR S\&P 500 ETF | `8mVLeXHpchLvt3WZRhRrAZYJ4oU3e2crVF1QEomgdDQ8` | `3n5x1QaXSSw5ysAK5zkMo1XSm52TwzTBK48RAfkeKJiu` |
| `XAU` | Gold (spot) | `AoufiP54693dn9HKonVBn6W75tKe8eHq5ncTwjTz6i5c` | `9XjYivtQfhDitFJugsskP6LppL7JJzANWwc7kmybpd55` |
| `XAG` | Silver | `VETzCeHk2mobTaUATNRBcokKE3x8YrcRyGx3Vsz4prr` | `DnHSia6WX5jYPnRkSTfu4GtbrTw2CAdLWKRmdMsWPHU3` |
| `NVDA` | NVIDIA | `B8M3y6wHGFZidqYciVxGu27KYk2dLcqgBGNoUDXMVzNN` | `6n525VkawSkm6h1HxisHCpRssdbmazb1Q9S3Bi2f9emL` |
| `AAPL` | Apple | `7ofckkctk9wUpTn77zFv8t8yb3iBUSkuU3vmcXbaUboS` | `438bLh6shsVHXoQwtaRP56zWuAsLJK1o2u6FyRe6crYV` |
| `EUR` | EUR/USD | `FLDQtZ2adGWTcawQh6jziufJejkexoPA2uYimQTqA8z9` | `z5u5UYz5jWdbycMexwdYvkw56fLY1uvx5RZW3EtxRsM` |
| `MSFT` | Microsoft | `GaGvW4zuXk84dJwmTXFYrxJaY4otHsJPasc7P5sjdCrA` | `4T1yPzWCxijyJEKiLmjfHYFNMJeiwGf2GSGuAsKwr9Fp` |
| `TSLA` | Tesla | `4dt2EW6pNJRBup7gqxtsQF1GgNKR4RWoj5bSXuY5f1JH` | `71Mh4et7Xvf8wdQJJ3ZUk51wgyervZVdjN6A4bCkWFfG` |
| `AMZN` | Amazon | `3vnTkSkevAJ69Vs8PFRNzF1hKvSiFbiLZJetNyqUcLZN` | `5ESabwnWACsFyDE96JNwbLkTpKyBHyNYM96PHEBvuzPc` |
| `GOOGL` | Alphabet | `42kcUPz39qudU5qv9tgU6a3E7jmiJhKs7AoXDUcarKQ8` | `AGzVgTj53MwsJd8Ke2iyibRSyVLsqdJS3DzQWsSAxgdj` |
| `GBP` | GBP/USD | `HSGiyJGoAMKYmEG6cZkvnTko3B3BP6nnVCYuikBxRLSJ` | `F6dZpqSCevWEUdCvv6ARaePy5RnqkcVJBm7UubjV8fSG` |
| `JPY` | USD/JPY | `4NVo5FmKT1Z1Bid3RuRa9obpYgyusrQdxByTPLw6KtsT` | `VqgP5wUX6jZKEtWQiktmip8ntGz2eKCjdJ4ttjfo93a` |
OIL was withdrawn on 30 Aug 2026. Its `Market` account is still marked active, but the keeper no longer prices it, so mint and redeem on it revert. VXX was delisted in June 2026. Both left bin books on chain; ignore them.
## How prices work on devnet
### The keeper pushes every price
Every devnet market runs in the `ProxyMode` risk state, which multiplies fees and the confidence band by 1.5. In ProxyMode the programs price from `Market.user_twap_price`, which the keeper writes with mint-redeem `update_risk_state_v2`, together with `twap_updated_at`. Despite its name, `user_twap_price` is a spot price, scaled to 1e6 and adjusted for corporate actions. The oracle program's `OracleConfig` is not updated on devnet, so do not read it for live prices.
Pyth's public price endpoint now requires an API key, so the devnet keeper prices markets from a free public quote source. A GJR-GARCH volatility model sets the confidence band. Each price is checked against Hyperliquid before it is pushed, and a disagreement blocks the push. Hyperliquid can veto a price but never sets one; if it is unreachable, the price goes through unchecked. Mainnet will use Pyth (Pyth Pro).
### Committed orders while open, bin books while closed
Since 11 Oct 2026 every live market's `Clp` account has `swap_guards.enabled` set, so committed orders work on all 13. The instant oracle swap (`clp_swap`) was retired the same day and reverts with `SwapDisabled` everywhere. The bin books (`pool_swap`) don't check the flag, but fill only while a market's price is more than 300 s old, which on devnet's `ProxyMode` markets means while the underlying is closed. Otherwise they fail with `PoolClosedWhileLive`.
### No fresh price, no mint or redeem
Mint and redeem revert with `TwapNotAvailable` when `user_twap_price` is 0 or `twap_updated_at` is more than 300 s old. There is no fallback to `initial_l_price`. A keeper restart or outage longer than 300 s pauses mint and redeem in the same way.
### Off-hours
The keeper withholds a market's push once its source price is more than 120 s old. That happens when the underlying market closes: outside regular US trading hours for stocks and ETFs, and mainly at weekends for FX and metals. 300 s after the last push, mint and redeem freeze.
The bin books open. The keeper replaces each book with a free-float book: each side in one bin within 1% of the last price, sized to measured demand (\$500 a side until the keeper has measured flow), and limited by a risk cap on that market's share of the treasury. When other venues show the price has moved, it pulls the bins on the stale side. The first fresh push reopens mint, redeem and committed orders, closes the books to trading and re-centres them.
## Explorers
* [Solana Explorer](https://explorer.solana.com/?cluster=devnet)
* [Solscan](https://solscan.io/?cluster=devnet)
* [SolanaFM](https://solana.fm/?cluster=devnet)
## Reporting issues
* Discord: [join the server](https://discord.gg/U5hPCB8ufJ) and post in #help. New members can post ten minutes after joining.
* Errors in these docs: [continuum-markets/continuum-docs](https://github.com/continuum-markets/continuum-docs).
# Introduction
Source: https://docs.continuum.markets/get-started/introduction
Continuum is a Solana-native protocol for 24/7 synthetic exposure to real-world assets. Here's the mental model.
Continuum is a Solana protocol for **24/7 synthetic exposure to real-world assets**: stocks, indices, commodities and FX. You deposit collateral (cUSDC on devnet, USDC on mainnet) and receive paired long and short tokens priced from the asset's NAV. You can trade either leg on Continuum's own venues, hold it with no expiry, or redeem both legs together for collateral.
There is no funding rate, no liquidation and no leverage. The position **is** an SPL token, paid for in full. The short leg carries a [volatility decay](/concepts/nav#volatility-decay) instead of funding. It is charged on realized volatility rather than positioning, and longs carry no holding cost.
Mint your first paired position on devnet in five minutes.
The mental model: paired tokens, NAV, oracle, keeper, CLP.
TypeScript SDK setup, IDLs, PDAs, end-to-end examples.
On-chain instruction and account reference.
## What you can build
Long and short tokens are standard SPL tokens, so other programs and apps can use them directly. Some examples:
* **Wrapper UIs** that offer long-only or short-only flows on top of the paired primitive.
* **Structured products**, such as delta-neutral baskets or dispersion trades, that combine several markets.
* **Trading bots** that trade the bin books when they drift from NAV.
* **Portfolio analytics** that value positions from on-chain NAV, with no off-chain feed.
* **CPI integrations** where another Solana program holds, mints or redeems Continuum positions as part of a larger flow.
## How it works
A user calls `mint_paired(collateral_amount)` on the [`mint-redeem`](/programs/mint-redeem) program. The program reads the price that the keeper last pushed to the `Market` account and takes the mint fee. It sends the deposit to the market's collateral vault, except for a dev tax of 10% of the fee. Then it mints long (`L`) and short (`S`) tokens, each worth half of the deposit after the fee.
The long leg is priced at the asset price P, adjusted for any corporate action. The short leg is priced at `K / P × γ`, where `K = initial_l_price × initial_s_price` and γ is the short leg's volatility-decay index. So `L × S = K × γ`, and as one leg rises the other falls. Mint and redeem also quote each leg against the user by the oracle confidence band. This worst-case quoting is on for every market except QQQ (as of 10 Oct 2026).
To exit, the user calls `redeem_paired(l_amount, s_amount)` with both amounts above zero and receives collateral at NAV, less the redeem fee. In between, both tokens trade on Continuum's own venues: committed orders while the market is open, and a bin book per leg for instant trades while it is closed. A committed order fills at NAV ± the posted commit spread (at least 10 bps), at a price pushed after the order was placed.
A keeper run by the protocol pushes prices and re-centres the books. Mint and redeem need a price pushed in the last 300 s, so they pause while the underlying market is closed or the keeper is down. The books trade then.
## Where this doc lives in the stack
```
┌─────────────────────────────────────────────────────────────┐
│ You (developer) │
│ Frontend / bot / structured product / CPI program │
└──────────────────────────┬──────────────────────────────────┘
│ TypeScript SDK / Anchor / web3.js
▼
┌─────────────────────────────────────────────────────────────┐
│ Continuum on-chain programs │
│ mint-redeem · oracle · clp · registry · trading · faucet │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Solana base layer │
└─────────────────────────────────────────────────────────────┘
```
The keeper runs off-chain. It signs price pushes as each market's `keeper_authority` and book updates as the CLP authority. Your transactions go directly to the programs and never need its keys, but mint and redeem rely on its recent price push.
## Core invariants you can rely on
1. **Paired mint and redeem.** User mints always create both legs, and user redeems always burn both. The single-leg mint and redeem instructions require the keeper authority.
2. **NAV on chain.** `L = P` and `S = K / P × γ`. γ starts at 1, and `set_s_gamma_index` rejects any write that raises it.
3. **Fresh-price gate.** Mint and redeem revert with `TwapNotAvailable` when the pushed price is 0 or more than 300 s old. In the `Stress` risk state both are blocked by default.
4. **No redemption race.** If a vault ever falls below 100% collateralized, redeem pays each holder their NAV claim times the collateral ratio, so early redeemers cannot drain it.
See [Solvency invariants](/concepts/solvency) for the complete list.
## What's not here
* **No funding rate.** Nobody pays for being on the crowded side. The short leg's volatility decay charges realized volatility instead, and never touches longs.
* **No liquidations.** Nothing can close your position; you hold the token.
* **No leverage.** You pay the full value of both legs when you mint.
* **No outside AMM.** Trading happens on Continuum's own bin books and committed orders, and on a Manifest order book that traders reach through Continuum's `trading` program.
* **No user yield vaults.** The protocol funds the CLP's capital itself, and there are no user LP shares.
## Get oriented
* Just want to interact? → [Quickstart](/get-started/quickstart)
* Want the mental model first? → [How Continuum works](/concepts/overview)
* Want to start building? → [SDK setup](/build/sdk-setup)
* Want the on-chain instruction reference? → [Programs overview](/programs/overview)
# Quickstart
Source: https://docs.continuum.markets/get-started/quickstart
Get test cUSDC, mint your first paired position and make a trade, all in the browser on devnet. No code required.
Everything on this page happens in the app at [**continuum.markets**](https://continuum.markets), on Solana devnet with test tokens. It takes about five minutes and needs no code. For the programmatic flow, see [Mint and redeem example](/build/example-mint-redeem).
Mint and redeem need a price that the keeper pushed in the last 300 s. The keeper stops pushing a market's price while its underlying market is closed. For stocks and ETFs that means outside regular US trading hours; for FX and metals, mainly at weekends. Trades on the bin books keep working at those times.
Open [continuum.markets/faucet](https://continuum.markets/faucet). The app runs on devnet, and the header says so.
Set your wallet's network to **Devnet** (not Testnet or Mainnet), then click **Select Wallet** in the top bar. Any wallet that supports the Wallet Standard works, such as Phantom, Solflare or Backpack.
Your wallet needs a little devnet SOL for transaction fees. If it has none, use **Get devnet SOL** on the faucet card, which opens faucet.solana.com.
Click **Get 1000 cUSDC** and approve the transaction. Your wallet receives 1,000 cUSDC, Continuum's devnet collateral: a test token with 6 decimals, mint `B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE`.
Each wallet can drip once every 24 hours. Until then, the button shows the time left until the next drip.
Open [/mint](https://continuum.markets/mint). Under **Select asset**, pick a market such as QQQ, and enter an amount of cUSDC: at least 10, so try 100. The preview shows how many long and short tokens you will receive and the mint fee.
For example, at QQQ's last price on 9 Oct 2026 (\$751.27), 100 cUSDC mints about 0.0664 QQQL and 0.1637 QQQS. The fee is 0.27%: QQQ's posted fee of 18 bps, times 1.5 because every devnet market runs in ProxyMode.
Click **Mint paired tokens** and approve. Your wallet now holds both legs, paid for in full, with no margin and no liquidation. If the preview says **Minting needs a fresh price**, the market is closed; come back when it reopens.
Open [/portfolio](https://continuum.markets/portfolio). Under **Open Positions**, each market you hold shows its long and short balances and their value at NAV. The cards at the top show your total value and your collateral balance.
The legs are SPL tokens in your wallet, so they also appear in your wallet app and stay there across sessions.
Open the market page, for example [/pools/QQQ](https://continuum.markets/pools/QQQ). It shows a price chart, the depth of each leg's bin book, and the trade panel.
In the trade panel, choose **Long (QQQL)** or **Short (QQQS)**, then **Buy** or **Sell**. **Sell** appears only when you hold that leg. Enter an amount and submit. Selling your QQQS leaves you long QQQ (QQQL plus cUSDC); selling your QQQL leaves you short.
While the market is open, a market order is a committed order. It fills at NAV ± the posted commit spread, at the next price pushed after you submit, unless traders' orders on the [order book](/flows/order-book) fill all of it now for less when you buy, or for more when you sell. If a committed order can't fill within 3 minutes, the keeper returns your input automatically, and a **Refund now** button appears if it hasn't come back a minute after the deadline. While the market is closed, the panel says **Market closed. You can still trade.** The leg's bin book, which quotes from the last price up to a small size, and traders' orders then form [one book](/flows/order-book#one-book-while-closed): a market order takes the cheapest offers first when you buy, and the highest bids first when you sell.
To exit at NAV, go back to [/mint](https://continuum.markets/mint) and choose **Redeem**. Redeem burns both legs together, so you need some of each. Enter a cUSDC amount, and the app redeems the same fraction of each leg you hold. The value must be at least 10 cUSDC. If you sold one leg, buy some back first, or sell the remaining leg instead.
Click **Redeem paired tokens**. You receive cUSDC at NAV, less the redeem fee (also 0.27% on QQQ as of 10 Oct 2026). A round trip at an unchanged price costs about the two fees combined.
## What else is in the app
| Page | What it does |
| - | - |
| [/pools](https://continuum.markets/pools) | All 13 markets, with each leg's book price and spread. Click a market to open its page. |
| `/pools/` | Market page: price chart, book depth and trade panel. |
| [/mint](https://continuum.markets/mint) | Mint and redeem. [/redeem](https://continuum.markets/redeem) redirects here. |
| [/portfolio](https://continuum.markets/portfolio) | Balances and NAV value for every market you hold. |
| [/faucet](https://continuum.markets/faucet) | Devnet cUSDC drip, once per wallet every 24 hours. |
| [/learn](https://continuum.markets/learn) and [/guide](https://continuum.markets/guide) | A glossary and step-by-step guides. |
| [/compare](https://continuum.markets/compare) | How Continuum compares with Synthetix, Jupiter Perps, GMX, xStocks and wrapped tokens. |
`/earn` redirects to `/pools`.
## Watching the keeper work
On any market page you can see the protocol's book management:
* The reference price updates with each keeper price push.
* Each book re-centres once its inventory-shifted centre drifts past the reposition gate (20 bps by default).
* The depth chart shows the asks and bids each book has posted.
While the underlying market is closed, the keeper stops pushing prices and mint and redeem pause. The books switch to a smaller free-float book held at the last price. All the liquidity you see comes from the protocol; there are no outside LPs.
## Wallet metadata
Long and short mints carry Metaplex metadata (name, symbol and image). Wallets show them by name, for example "Continuum QQQ Long" (QQQL), rather than as bare addresses. cUSDC has metadata too. Some wallets cache token metadata, so a new icon can take a while to appear.
## When to use the app vs the SDK
The app covers what most users need:
* One-off mints and redeems
* Trading either leg, instantly or by committed order
* Watching prices and books
* Portfolio balances
* Voluntary burn of a single leg (advanced)
Use the programs directly when you need to:
* Automate flows (bots, schedulers, structured products)
* Build a custom UI on top of Continuum
* Integrate by CPI from another on-chain program
* Run a trading bot
The app is one client of the same on-chain programs.
## Where next
Why paired tokens, what NAV means, how the keeper works.
Cluster, RPCs, addresses, gotchas.
TypeScript SDK setup, IDLs, end-to-end script examples.
Long-only wrappers, arb bots, structured products, CPI integrations.
# Peg maintenance
Source: https://docs.continuum.markets/keeper/arb-paths
Why the venues stay near NAV, what bounds a stale book, and the legacy arbitrage loop that is off by default.
## Why the venues stay near NAV
Every venue prices off the same keeper-pushed price, so there is no outside pool to drift away from:
* **Committed orders** fill at NAV ± `max(commit_spread_bps, 10)` bps, at a price pushed after the commitment (any later push, not necessarily the first). The price did not exist when the order was placed, so there is no stale quote to trade against. Since 11 Oct 2026 every live market accepts them, and they are the protocol's only venue while a market's price is live.
* **The instant oracle swap** (`clp_swap`), which quoted NAV ± a fixed spread, was retired on 11 Oct 2026 and always fails.
* **The bin books** are the only venue whose price floats between keeper updates, and they fill only while the market's price is stale (outside `Normal`, more than 300 s old). The keeper re-centres each book once its inventory-shifted centre drifts past the reposition gate: 20 bps by default, cut further so the book moves before NAV reaches its first funded bin. A trader who lifts a stale bin pays the real slippage of the bin ladder. And turning a cheap leg into cUSDC through the protocol means buying the other leg too and redeeming the pair, because `redeem_paired` needs both legs; that round trip pays the redeem fee (10 to 39 bps as posted on 13 Oct 2026, times 1.5 in ProxyMode).
So any venue's mispricing is bounded by the larger of the reposition gate and the half-spread, plus that fee hurdle.
## What bounds a stale book
While prices are pushing, the books don't fill, so a gap inside one keeper cycle can't be traded against them. Off-hours, a move on another venue can leave bins in the money until the keeper's guard pulls the stale side, and a trader can lift them. That worst case is bounded:
* `pool_swap` fills only from pre-funded bins, so the loss is capped at the depth of those bins.
* The books are lean (see [Book management](/keeper/book-management)), and the next cycle re-centres and refills them.
* On QQQ, SPY and XAU, `pool_swap` is held to a rolling notional cap of \$1,000,000 per hour (as of 11 Oct 2026). The other 10 live markets have no such cap set (checked on chain, 13 Oct 2026).
* While a market is closed, its books are free-float books: one bin a side within 1% of the last price, sized to measured demand (\$500 a side until flow is measured) and limited by a risk cap on that market's share of the treasury. There are no fixed dollar ceilings.
Capping depth this way is what makes the books safe on volatile assets, where an uncapped venue quoting at NAV could be drained.
## The legacy arbitrage loop
The keeper runs no arbitrage loop by default. The arbitrage scanner, the boot-time sweep of leftover L and S in the keeper wallet, and the liquidity seeder belong to the legacy Meteora layer, which runs only with `LEGACY_METEORA_ENABLED=true` (default `false`).
Anyone can still trade against a drifted book without a keeper key: buy a leg on its book, buy the other leg on its own book, and redeem the pair with `redeem_paired`. Near the reposition gate that trade rarely clears the fee hurdle.
# Book management
Source: https://docs.continuum.markets/keeper/book-management
How the keeper centres, shapes, refills and trims the bin books, and what it posts while a market is closed.
Each market has two books, one per leg. A book lives entirely inside one `Pool` account: up to 16 bins stored inline, with no per-bin rent. The keeper's book manager centres each book near NAV and posts a bin shape derived from live volatility.
Since 11 Oct 2026 the books fill only while the market's price is stale: outside `Normal`, with the last push more than 300 s old. While the keeper is pushing prices, `pool_swap` fails with `PoolClosedWhileLive` and trades go through [committed orders](/flows/trade#committed-orders).
## The reposition loop
Every 30 s, for each market and side:
1. **Feed the volatility state.** One price sample per cycle goes into the spread engine. The state lives in memory, so a restart empties it, and until the ratchet has about 8 windows of data the spread falls back to its floors.
2. **Find the centre.** The target is NAV moved by the inventory centre-shift (on by default). When the public holds more L than S, the protocol is left holding S, so the keeper quotes the long book dearer and the short book cheaper. The shift is at most half the narrower leg's half-spread, never quotes inside the spread floor, and stays within the redeem-fee hurdle so a half-posted update cannot be arbitraged.
3. **Check the drift gate.** The book re-centres only when the target has moved from the posted base by at least the gate. `CLMM_REPOSITION_BPS` (default 20) is a ceiling: on the side a move threatens, the keeper cuts the gate so the book re-centres, with 1 bp to spare, before NAV comes within the spread floor (oracle confidence plus a fixed cost) of the first funded bin.
4. **Compute the shape.** The half-spread is the largest of the realized q90 ratchet, the slow auto-floor, oracle confidence plus cost, and the static floor (see [the keeper concept page](/concepts/keeper)). Bins inside it stay empty. Liquidity starts at the bin whose offset is nearest the half-spread and falls off by a factor of 0.7 per bin outward. The protective skew then widens the side a continuing trend would hit and never tightens the other. The short book mirrors this automatically, since `S = K / L × γ` moves inversely to L. A side that cannot clear the floor stays unposted.
5. **Post it.** `set_pool_shape(base, asks, bids)`. On chain, the asks must fit in the pool's synth vault and the bids in its cUSDC vault.
## Closed markets: the free-float book
When a market's underlying stops trading, the keeper stops pushing its price. Once the last push is more than 300 s old, the program rejects mint and redeem, and the keeper switches that market's books to free float. It posts one book per side, anchored at the last price with no inventory shift, with all of each side in one bin: the outermost funded bin within 1% of the last price. Each side is sized to one closed period's demand: at least \$500 (the default until the keeper has measured flow for the market), and never more than a risk cap on that market's share of the treasury. There are no fixed dollar ceilings. The keeper holds that book until a fresh price returns, then re-centres it whatever the drift. It records the price each side's book was posted at, beside its off-hours checkpoint, so a restart mid-episode adopts the book on chain as fills and the guard left it rather than posting a full one over it.
The free-float guard (on by default) watches venues that keep trading while the underlying is shut: US pre- and post-market prints, CME and CFE futures, Hyperliquid perps, and tokenized gold and euro (PAXG, EURC) on Coinbase. When they show a move of 10 bps or more, the guard pulls the stale side's bins: asks within the free-float half-spread of the highest price any venue shows, and bids within it of the lowest. Pulled bins return only after the venues have allowed it for 600 s without a break. The guard never moves the centre or adds depth, and with no venue reading it changes nothing.
## Inventory: refill, trim, bid cap
`pool_swap` fills only from pre-funded bins, so trading reshapes inventory. The keeper rebalances it every cycle, in both directions.
* The keeper no longer restocks drained asks by minting pairs from swept bids; that task was removed on 13 Oct 2026. Pairs for the CLP's stock are minted by imbalance control, with cash from the treasury (see [Keeper → Imbalance control](/concepts/keeper#imbalance-control-900s)).
* When a side's asks are worth more than 1.5× its bids, the keeper sweeps the excess back to CLP custody with `pool_sweep_synth`, where it can provision committed orders. Ask depth the bid side cannot absorb is exit capacity nobody can use, and it invites arbitrage.
* Each side's bid cUSDC is held at the depth its next free-float book will post (\$500 a side until the keeper has measured flow), and the pool vault's excess goes to the CLP vault. While the market is open, sells go through the [committed venue](/flows/trade) instead, which the keeper provisions at settlement.
## Why the books are lean
Toxic loss grows with deployed depth, while organic revenue is capped by demand, so depth beyond demand subsidizes arbitrage. That is why bid cUSDC is held at the next free-float book's depth and asks are trimmed to a ratio of the bids. `pool_swap` charges no swap fee, so a book earns only its spread; the protocol's other income is the mint and redeem fees and the committed-order spread.
# Keeper overview
Source: https://docs.continuum.markets/keeper/overview
The protocol's off-chain operator. It pushes prices, runs the bin books, settles committed orders and posts per-market parameters.
The keeper is an off-chain Rust service that the protocol runs. It pushes a reference price for each market, manages the 26 bin books, settles committed orders, moves inventory, and posts per-market parameters such as fees and the γ index. Its source code and binary are not published; these pages describe how the protocol's keeper behaves and how it is configured.
Most loops have an environment switch, but not all. γ posting has no switch of its own and stops only if the whole book manager is turned off. Lending collateral, which happens only on mainnet, and the legacy Meteora layer, with its arbitrage loop, ship turned off. See [Keeper configuration](/keeper/running) for variables and defaults.
## What it runs
| Task | Cadence | What it does |
| - | - | - |
| **Price push** | 5 s in the deployed image (`UPDATE_INTERVAL_MS`, code default 15 s) | Fetches each market's reference price, checks it against Hyperliquid, and writes it with mint-redeem `update_risk_state_v2` to `Market.user_twap_price` (a spot price despite the name, 1e6, corporate-action adjusted) and `twap_updated_at`. If the source price is over 120 s old or the fetch fails, it withholds the push. 300 s after the last push, mint and redeem revert with `TwapNotAvailable` and the books switch to free float. |
| **Book manager** | 30 s | Re-centres each book on its inventory-shifted centre once it drifts past the reposition gate, and posts the bin shape with `set_pool_shape`. See [Book management](/keeper/book-management). |
| **Inventory** | With the book manager | Trims asks above 1.5× the bid target. Each side's bid cUSDC stays at the depth its next free-float book will post (\$500 today): the excess is swept to the CLP vault, and bids that sellers spent are refilled from it (`clp_fund_pool_usdc`). |
| **Parameter posting** | With the book manager | Posts the mint and redeem fee, `clamp(fee_mult × half-spread, 10, 100)` bps (`update_market`), the committed-order spread, `clamp(half-spread, 10, 500)` bps (`set_commit_spread`), each market's fixed overcollateralization target (`set_buffer_target`, only when it differs from the value on chain) and the γ index (`set_s_gamma_index`). Unposted γ is saved to R2 or `DATA_DIR` and survives a restart, and a market with no γ write for a day is posted once at least 1e-6 is pending. |
| **Commit settler** | 20 s idle, 5 s while orders are pending | Finds pending committed orders and settles up to 10 per tick, largest first, simulating each settle first. A refund or a fill the CLP vault covers is sent as it is. A fill short of cash borrows from the treasury (`allocate_to_market`) and mints or redeems pairs if needed, in the same transaction as the settle where it fits, so a fill that fails takes its funding with it. Orders held back by the collateral floor or the oracle checks wait for a later push. It refunds expired orders about 5 s after expiry (`cancel_swap`). `settle_swap` is permissionless; the keeper is the cranker that is always running. |
| **Liquidity reallocator** | 30 s | Keeps each market's CLP vault between its working need and twice it: what its bid refills can spend in one tick, \$1,250 per market today (2 × \$500 × 1.25). Tops up from the treasury below the need and returns everything above 1.5× once a vault passes twice the need (`allocate_to_market`, `return_to_global`). |
| **Treasury backstop** | With the reallocator | Tops up a market's collateral from the treasury (`backstop_market`): at once below 100% at the stored price, and after 5 minutes short of its posted target by more than one confidence band. Fills to the target, at most 130% by default, and never takes a fill back. See [Solvency → Treasury backstop](/concepts/solvency#treasury-backstop). |
| **Cross-market rebalance** | With the book manager | Moves collateral from markets above their target ratio to markets below it (`rebalance_collateral`). It plans at the stored price the program checks and leaves the source one confidence band of margin. A destination within one band of its target is left alone, and past it is filled to target plus the band. Markets below 100% are filled first, and each source's excess is split in proportion to need. The program clamps each move to what the source holds above its own target, so a plan made at an old price moves less than requested and never drains the source. |
| **Redemption settler** | 60 s idle, 10 s while requests are pending | When `REDEMPTION_SETTLE_ENABLED` is true (default off), pays queued `request_redemption` requests in parts: from the vault's cash, with a recall from lending venues in the same transaction, and with a treasury advance for the rest. It repays an advance from the market's next recall. See [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet). |
| **Devnet treasury trim** | 5 min | Devnet only, and only when `DEVNET_TREASURY_TARGET_USD` is set: withdraws what the treasury policy's allowance allows, never past the target, and burns it. |
| **Health** | 300 s | Watches the SOL balances of both signers. Below a critical line it tops the wallet up (a devnet airdrop, a mainnet USDC-to-SOL swap of at most \$250, or a transfer from the other signer) and posts a Discord alert if the wallet is still below. |
The **yield allocator** runs every 300 s. It plans how much of each market's collateral to lend, and where, from live mainnet state of three USDC venues (Kamino, Save and Jupiter Lend Earn; MarginFi is disabled). It lends only on mainnet with `COLLATERAL_YIELD_ENABLED=true`, which is off by default, and elsewhere it only plans. On devnet, an emulator credits each market the interest that plan would earn. See [Keeper → Yield allocator](/concepts/keeper#yield-allocator-300s).
Every transaction that moves funds is first written to a journal in R2 or on a persistent disk. If a send ends with an unknown outcome, the keeper pauses those sends until it has reconciled the transaction and restarted. Price pushes skip the journal because repeating one is harmless.
## What it does not do
* **Set prices by hand.** Each half-spread is the largest of four terms: a realized q90 ratchet, a slow auto-floor based on 60-day volatility, oracle confidence plus a fixed cost, and a static per-market floor. No trained model, no discretionary input. See [the keeper concept page](/concepts/keeper).
* **Sit in the user's call path.** `mint_paired`, `redeem_paired`, `pool_swap`, `commit_swap` and `cancel_swap` need neither keeper key. Mint and redeem do need a price push from the last 300 s. If the keeper stops, its last price ages past 300 s, and outside `Normal` the books then trade at their last posted bins. Anyone, the owner included, can call `cancel_swap` for an unsettled committed order once its 180 s TTL has passed, and the escrow goes back to the owner.
## Signing roles
The keeper wallet is each market's `keeper_authority`. It pushes prices, posts buffer targets and the γ index, rebalances collateral, and can call the keeper-only single-leg instructions (`keeper_mint_single`, `keeper_redeem_single`, `keeper_burn_single`). It could also move collateral to a keeper-owned account for lending (`deploy_collateral_to_yield`) on a market with `yield_enabled` set. Every live market has it set as of 11 Oct 2026, but the keeper lends only on mainnet.
The CLP authority signs book shapes, sweeps, inventory moves and `set_commit_spread`. Fee posting goes through `update_market`, which requires the market's `authority`.
# Keeper configuration
Source: https://docs.continuum.markets/keeper/running
What the protocol's keeper needs, the environment variables it reads with their defaults, logging, and common failure modes.
The keeper's source and binary are not published. This page documents how the protocol's keeper is configured: what it needs, the environment variables it reads and their defaults, its logs, and how it fails. Integrators do not need any of it; mint, redeem and trades go straight to the programs.
## What it needs
* **Two funded keypairs.** The keeper wallet must be `market.keeper_authority` on every market it operates, and pays for price pushes and settlements. The CLP authority signs book shapes and inventory moves and pays rent for new pool accounts. On devnet it is also each market's admin `authority`, which fee posting needs.
* **RPC endpoints.** Market discovery uses `getAccountInfo`. The commit settler uses a filtered `getProgramAccounts` and websocket subscriptions, which some paid RPC tiers disable, so at least one endpoint must allow them.
* **The mint-redeem program ID.** Without `DEVNET_MINT_REDEEM_PROGRAM_ID` or `MINT_REDEEM_PROGRAM_ID`, market discovery fails and the keeper runs QQQ only, with a warning.
* **Storage for the transaction journal.** Every transaction that moves funds is journaled first, in Cloudflare R2 or under a persistent `DATA_DIR`. Without a working journal the keeper disables those transactions; price pushes still go out.
The protocol deploys the keeper as a Docker image built with Rust 1.88. The image sets `RUST_LOG=info` and `UPDATE_INTERVAL_MS=5000` and exposes port 8484. The process exits with a non-zero code as soon as any of its loops ends, so it runs under a supervisor that restarts it on failure.
## Environment variables
The keeper loads `.env` from its working directory, or the file named by `KEEPER_ENV_FILE` (which must come from the process environment). Per-network settings, such as the RPC URL, collateral mint and program IDs, are read with a `DEVNET_` or `MAINNET_` prefix first, then without it.
### Networks and keys
| Variable | Default | Meaning |
| - | - | - |
| `DEVNET_RPC_URL`, `MAINNET_RPC_URL` | unset | Comma-separated endpoints, first one primary. Set either or both; one process serves each. The official cluster URL is appended as the last fallback. |
| `RPC_URL` | `https://api.devnet.solana.com` | Single-network mode, used only when neither of the above is set. |
| `KEEPER_KEYPAIR_JSON`, `KEEPER_KEYPAIR` | unset, `~/.config/solana/id.json` | Keeper wallet as a JSON byte array (preferred) or a file path. If no key loads, the keeper runs read-only. |
| `CLP_AUTHORITY_KEYPAIR_JSON`, `CLP_AUTHORITY_KEYPAIR` | unset, `key/clp-authority.json` | CLP authority, the same way. If no key loads, CLP-authority instructions are signed by the keeper wallet and fail unless it is the CLP authority. |
| `USDC_MINT` | mainnet USDC on devnet; required on mainnet | Collateral mint. On devnet set it to cUSDC, `B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE`. |
| `MINT_REDEEM_PROGRAM_ID` | unset | Needed for market discovery. Transactions fall back to the devnet ID on devnet. |
| `ORACLE_PROGRAM_ID`, `CLP_PROGRAM_ID` | devnet IDs on devnet | Required on mainnet; the keeper will not start there without them. |
| `ACTIVE_MARKETS` | unset | Comma-separated symbols to pin. When unset, the keeper reads the `Market` account of each of the 14 symbols it supports and keeps those with `is_active = true`. |
On devnet, an unset `USDC_MINT` falls back to mainnet USDC. Every token-account derivation is then wrong, vault balances read as 0, and loops skip markets without an error. Always set `DEVNET_USDC_MINT` or `USDC_MINT` to cUSDC on devnet.
### Price source
| Variable | Default | Meaning |
| - | - | - |
| `UPDATE_INTERVAL_MS` | `15000` (`5000` in the image) | Price-push interval. |
| `PYTH_API_KEY` | unset | Pyth Pro key. Without it Pyth requests fail, and the keeper can price markets only from its fallback public quote source, which is switched on by `CNBC_FALLBACK_ENABLED` (off by default). With neither source answering, it withholds every push. |
| `CNBC_FALLBACK_ENABLED` | `false` | Allows the fallback public quote source. Without it, the keeper uses Pyth only. |
| `HERMES_URL` | `https://hermes.pyth.network` | Hermes base URL, for addressing a different host directly. |
| `HERMES_STALE_SECS` | `120` | Withhold a market's push when the source's own publish time is older than this, whichever source it came from. |
| `HL_CROSS_CHECK_ENABLED` | `true` | Compare each fresh print with Hyperliquid and withhold the push when they disagree. An unreachable Hyperliquid lets the print through; Hyperliquid never sets a price. |
| `CROSS_CHECK_BAND_BPS` | 30 FX, 60 metals, 100 stocks | Largest allowed disagreement. One value overrides all three. |
When the fallback source prices a market, the confidence band comes from a GJR-GARCH volatility model; with Pyth, the keeper uses the wider of Pyth's band and the model's. Mainnet will price from Pyth (Pyth Pro).
### Book manager and parameter posting
| Variable | Default | Meaning |
| - | - | - |
| `CLMM_POOLS_ENABLED` | `true` | The book-manager loop. Refills, rebalancing and all parameter posting run inside it, including γ, so `false` stops all of them. |
| `CLMM_POOL_INTERVAL_SECS` | `30` | Loop cadence, minimum 15. |
| `CLMM_REPOSITION_BPS`, `CLMM_REPOSITION_BPS_` | `20` | Largest reposition gate; the keeper cuts it per side to the first funded bin. |
| `CLMM_VOL_SPREAD_ENABLED` | `true` | Volatility-scaled spreads. `false` posts a plain geometric shape from the first bin, with no spread floor, skew or inventory shift. |
| `CLMM_SKEW_ENABLED` | `true` | Protective trend skew. |
| `CLMM_INVENTORY_SKEW_ENABLED` | `true` | Inventory centre-shift, tuned by `CLMM_INVENTORY_GAIN` (`1.0`) and `CLMM_INVENTORY_CAP_FRAC` (`0.5`). |
| `CLMM_SHAPE_KEEP_BPS` | `7000` | Per-bin decay of the live shape (0.7 per bin). |
| `CLMM_REFILL_ENABLED` | `true` | Ask refill, bid refill, trim and bid cap. |
| `CLMM_SKEW_GAIN` | `1.0` | Gain on the protective drift skew: the side a continuing trend would pick is widened by this multiple of the trend's z-score, and the other side is never tightened. |
| `CLMM_REFILL_DUST_USD` | `500` | Smallest bid refill, in dollars, worth a transaction. |
| `CLMM_BID_TARGET_USD` | unset | Overrides the bid cUSDC each side stages while the market is open. Unset, each side stages the depth its next free-float book will post (r55's rule, \$500 a side until the keeper has measured flow). The pool vault's cUSDC above the target is swept to the CLP vault, and a vault below it is refilled from there. |
| `CLMM_TRIM_RATIO` | `1.5` | Largest ask value as a multiple of the bid target. |
| `CLMM_FEE_SCALE_ENABLED` | `true` | Posts `clamp(fee_mult × half-spread, 10, 100)` bps as the mint and redeem fee when it moves by 5 bps or more. `fee_mult` is 1.0. |
| `CLMM_COMMIT_SPREAD_ENABLED` | `true` | Posts `clamp(half-spread, 10, 500)` bps as the committed-order spread when it moves by 2 bps or more. |
| `CLMM_BUFFER_POST_ENABLED` | `true` | Posts each market's overcollateralization target (see [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer)) when it differs from the value on chain. |
| `CLMM_BUFFER_TARGETS_BPS` | unset | Overrides the posted buffer for listed markets, as `SYM:bps` pairs (`NVDA:1000,TSLA:1200`), clamped to 100 to 5,000 bps. It wins over the daily target the keeper reads from R2 (`cr-targets/latest.json`, written by the daily Monte Carlo job), which wins over the fixed fallback table; a market missing from that table posts 100 bps. |
| `CLMM_REBALANCE_ENABLED` | `true` | Cross-market collateral rebalance. |
| `GAMMA_DECAY_COEF` | `1.0` | γ calibration. Must be in (0, 1]; any other value aborts boot. |
| `CLMM_GAMMA_DECAY_ENABLED` | removed | Setting it to any value aborts boot. γ decay has no switch. |
### Off-hours book
| Variable | Default | Meaning |
| - | - | - |
| `CLMM_FREEFLOAT_DMIN_USD` | `500` | Smallest depth, in dollars, a free-float book posts per side. The depth is `max(this, min(demand quantile, risk cap))`, with no static ceiling. The old `CLMM_FREEFLOAT_CAP_USD` and `CLMM_FREEFLOAT_CAP_USD_JUMP` caps are ignored. |
| `CLMM_FREEFLOAT_FLOW_USD_` | `0` | Uninformed flow per closed period across all four sides of market ``, in dollars. At 0 every side posts the minimum. |
| `FF_GUARD_ENABLED` | `true` | Free-float guard, which pulls stale-side bins when other venues show a move. |
| `FF_GUARD_MIN_MOVE_BPS` | `10` | Venue moves smaller than this count as noise. |
| `FF_GUARD_RESTORE_SECS` | `600` | How long venues must allow it before pulled bins return. |
### Committed orders
| Variable | Default | Meaning |
| - | - | - |
| `COMMIT_SETTLE_ENABLED` | `true` | The settlement cranker. It simulates each settle, draws any cash a fill needs beyond the CLP vault from the treasury in the same transaction where it fits, and refunds expired orders about 5 s after expiry. |
| `COMMIT_SETTLE_INTERVAL_SECS` | `20` | Idle cadence, minimum 5. It drops to 5 s while orders are pending. |
### Liquidity reallocator
| Variable | Default | Meaning |
| - | - | - |
| `LIQUIDITY_REALLOCATOR_MODE` | `live` | `off`, `dryrun` (logs the moves) or `live`. Keeps each market's CLP vault between its working need and twice that need, topping up from the treasury and returning the rest, and runs the treasury backstop (`backstop_market`). |
| `LIQUIDITY_REALLOCATOR_INTERVAL_SECS` | `30` | Cadence, minimum 30. The working need counts every consumer run that fits in one tick, so a longer tick widens the band. |
| `TREASURY_BACKSTOP_MAX_TARGET_BPS` | `13000` | Highest collateral ratio the treasury backstop fills a market to, in bps of claims, clamped to 10,000 to 15,000. Below it, a market is filled to its posted target. |
| `TREASURY_BACKSTOP_SKIP` | `MSTR` | Comma-separated markets the backstop fills only to 101%. |
| `WIND_DOWN_MARKETS` | `MSTR` | Comma-separated markets being wound down: no working cash or backstop, books emptied and swept back to the CLP, the 1% minimum buffer posted, and while the price is fresh the CLP's pairs redeemed (all but one) and the surplus collateral returned to the treasury. Empty disables it. |
### Treasury withdrawal rate
| Variable | Default | Meaning |
| - | - | - |
| `TREASURY_RATE_MODE` | `live` | `off`, `dryrun` (logs the plan) or `live`. Every 900 s it keeps the treasury's withdrawal limits where the whole balance can leave in `TREASURY_EXIT_SECS`, signing `set_treasury_limits` as the CLP authority. A raise waits out the policy's 24 h delay and is applied with `apply_treasury_limits` once due; a cut applies at once. It acts only when the limits are more than 10% off, never while the allowance is refilling, and never over a pending change it didn't make. |
| `TREASURY_EXIT_SECS` | `86400` | How long a full exit takes, clamped to 1 h to 30 days. The burst leaves at once and the rest at the rate, so the rate is the balance ÷ (exit + burst seconds). |
| `TREASURY_BURST_SECS` | `3600` | The burst, in seconds of the rate. |
### Imbalance control
| Variable | Default | Meaning |
| - | - | - |
| `IMBALANCE_CONTROL_MODE` | `live` | `off`, `dryrun` (logs the plan) or `live`. The daily collateral targets assume it runs live, and plan without it for any market whose imbalance leaves twice the band. Every 900 s, for each active market with a price under 240 s old, it redeems the CLP's surplus of the heavier leg with `vault_redeem_single` until the book's long and short values are back in balance. |
| `IMBALANCE_BAND` | `0.05` | How far the imbalance β = (L − S)/(L + S) may drift from its target before it acts. |
| `IMBALANCE_TARGETS` | empty | Per-market targets for β, as `SYM=β,…`. Unlisted markets target 0. |
| `IMBALANCE_MAX_USD` | `50000` | Most it redeems in one market in one tick. |
| `IMBALANCE_KEEP_BPS` | unset | Overrides the CLP's stock share per leg (bps of the market's claims, at most 500). Unset, it follows the daily job's value, or 50 if the file is missing or over 48 hours old. |
| `IMBALANCE_MIN_USD` | `100` | Smallest redemption or trim it sends. |
### Redemption settler
| Variable | Default | Meaning |
| - | - | - |
| `REDEMPTION_SETTLE_ENABLED` | `false` | Pays queued `request_redemption` requests, oldest first: `settle_redemption` from the vault's cash, then with a recall from the lending venues in the same transaction, then with a treasury advance (`advance_to_market`), and it repays an advance from the market's next recall. See [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet). |
| `REDEMPTION_SETTLE_INTERVAL_SECS` | `60` | Idle cadence, minimum 10. It drops to 10 s while work is pending. |
| `REDEMPTION_ADVANCE_ENABLED` | `true` | Allows treasury advances. The market's on-chain `max_advance_bps` still decides how much. |
| `REDEMPTION_SPLIT_RECALL` | `false` | Sends the recall and the settle as separate transactions. An escape hatch; off, they are one atomic transaction. |
| `REDEMPTION_ESTIMATE_PAD_BPS` | `100` | Margin added to the settler's estimate of what a part will pay. |
### Devnet treasury trim
| Variable | Default | Meaning |
| - | - | - |
| `DEVNET_TREASURY_TARGET_USD` | unset | Devnet only. When set, the keeper withdraws what the treasury policy's allowance allows every 5 minutes, never past this balance, and burns it in the same transaction. Unset or 0 is off. It never runs on mainnet. |
### Health and alerts
| Variable | Default | Meaning |
| - | - | - |
| `SOL_HEALTH_INTERVAL_SECS` | `300` | Balance check cadence, minimum 15. |
| `SOL_HEALTH_KEEPER_LOW_LAMPORTS`, `SOL_HEALTH_KEEPER_CRITICAL_LAMPORTS` | 0.5 SOL, 0.15 SOL | Keeper wallet. Below the low line the dashboard marks it low. Below the critical line the keeper tops it up, then alerts if it is still below. |
| `SOL_HEALTH_CLP_AUTHORITY_LOW_LAMPORTS`, `SOL_HEALTH_CLP_AUTHORITY_CRITICAL_LAMPORTS` | 1 SOL, 0.2 SOL | The same lines for the CLP authority. |
| `SOL_REFILL_BUFFER_LAMPORTS` | 1 SOL | Top-ups aim this far above the low line. |
| `SOL_AUTO_AIRDROP_ENABLED` | `true` | Devnet top-up by airdrop. |
| `SOL_REFILL_SLIPPAGE_BPS` | `100` | Slippage tolerance for the USDC-to-SOL swap. |
| `SOL_REFILL_SWAP_ENABLED` | `true` | Mainnet top-up: swaps the keeper wallet's own USDC to SOL through Jupiter, at most \$250 per swap (`SOL_REFILL_MAX_USDC`). |
| `DISCORD_BOT_TOKEN` and `DISCORD_NOTIF_CHANNEL_ID`, or `DISCORD_SOL_HEALTH_WEBHOOK_URL` | unset | Alert by bot (tried first) or by webhook. With neither set, alerting is off and the keeper logs that at startup. |
| `SOL_HEALTH_ALERT_COOLDOWN_SECS` | `3600` | At most one alert per wallet in this window. |
### Storage, dashboard and logs
| Variable | Default | Meaning |
| - | - | - |
| `R2_ACCOUNT_ID`, `R2_BUCKET`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` | unset | Journal in R2. Setting any one selects R2 and requires all four. |
| `DATA_DIR` | `/data` | Root for the journal and the γ and off-hours checkpoints when R2 is unset. It must persist across restarts and support OS file locks, and only one process can hold it. |
| `PORT`, then `DASHBOARD_PORT` | `8484` | The single dashboard port. |
| `KEEPER_PEERS` | unset | Dashboard URLs of other keeper instances (see below). |
| `RUST_LOG` | `info` | Log filter (see [Logging](#logging)). |
| `KEEPER_LOG_FILE` | unset | Write logs to this file instead of stderr. |
### Collateral yield
The allocator's limits come from the stress assumptions below and the treasury balance, which it reads on chain, rather than from fixed percentages. See [Keeper → Yield allocator](/concepts/keeper#yield-allocator-300s).
| Variable | Default | Meaning |
| - | - | - |
| `YIELD_ALLOCATOR_ENABLED` | `true` | Yield planning, on every cluster. The devnet emulator uses its mainnet snapshots, so turning it off also stops emulated yield. |
| `YIELD_ALLOCATOR_INTERVAL_SECS` | `300` | Planning cadence, at least 60 s. |
| `YIELD_STRESS_REDEEM_FRAC_PER_DAY` | `0.5` | Share of a market's collateral a stressed run can redeem in a day. |
| `YIELD_LIQUID_SHARE` | `0.03` | Measured liquid share. It is the fallback: while the nightly cr-targets file is under 48 h old, the allocator uses that job's liquid share instead (1% to 10%; a rise applies at once, a fall is limited). |
| `YIELD_RECALL_HORIZON_HOURS` | `0.1` | Time to get lent collateral back (6 minutes). Together with the stress rate above it gives only a small share, so the liquid share binds. |
| `YIELD_MIN_LIQUID_USD` | `0` | Liquid floor per market, in dollars. There is no fixed floor. |
| `YIELD_VENUE_LOSS_BUDGET` | `0.5` | Share of all markets' collateral above 100% that counts as first-loss capital, on top of the treasury. |
| `YIELD_VENUE_FAILURES_COVERED` | `1` | Venue failures to cover, 1 to 4. Each venue's total position across all markets is capped at first-loss capital divided by this number, on top of its share below. |
| `YIELD_VENUE_LOSS_SHARE_TIER1` | `0.5` | Largest share of first-loss capital one venue may hold if it has no lender losses on record: Kamino and Jupiter Lend. |
| `YIELD_VENUE_LOSS_SHARE_TIER2` | `0.25` | The same for every other venue: Save. MarginFi is disabled in the plan. |
| `YIELD_EXIT_LIQUIDITY_SHARE` | `0.5` | Largest share of a venue's withdrawable liquidity the protocol may hold. |
| `YIELD_SPOT_WEIGHT` | `0.25` | Weight of today's rate against the 30-day mean. |
| `YIELD_MIN_MARGINAL_APR` | `0.01` | Hurdle: no more is lent once the marginal rate falls below it. |
| `DEVNET_YIELD_EMULATION` | `true` (devnet only) | Emulated harvests at the rate the mainnet plan would earn. |
| `DEVNET_YIELD_INTERVAL_SECS` | `3600` | Emulator cadence. |
### Off by default
| Variable | Default | Meaning |
| - | - | - |
| `LEGACY_METEORA_ENABLED` | `false` | The legacy Meteora layer: DLMM position manager, depth balancer, LP-fee claimer, arbitrage scanner with its boot-time sweep, and liquidity seeder. |
| `COLLATERAL_YIELD_ENABLED` | `false` | Lending collateral. It acts only on mainnet USDC, and the market also needs `yield_enabled` and a non-zero `yield_ceiling_bps`. |
## Primary and standby
Several instances can run with the same keys. Set `KEEPER_PEERS` on each to the dashboard URLs of the others. Every 30 s each instance probes `/api/status/` on its peers, and the one with the oldest start time is primary. Standby instances still push prices, which is harmless to repeat, but skip the loops that move funds, such as the book manager and the commit settler. When the primary stops answering, the oldest remaining instance takes over at the next probe.
Each market has one `keeper_authority`; the market admin can rotate it with `update_keeper_authority` to split markets between operators. Bots without either key can run alongside: `settle_swap` is permissionless, and anyone can trade the books.
## Logging
The binary crate is named `keeper`, so log targets are its module paths: `keeper::oracle`, `keeper::clmm_pool`, `keeper::commit_settler`, `keeper::sol_health`, `keeper::tx`, `keeper::rpc`. A filter for `continuum_keeper` matches nothing and silences the keeper.
```env theme={null}
RUST_LOG=info,keeper::clmm_pool=debug,keeper::commit_settler=debug
```
`keeper::clmm_pool=debug` logs every reposition, refill and posting decision, and `keeper::commit_settler=debug` logs committed-order discovery and settlement. `keeper::rpc=warn` keeps only RPC failures.
A healthy devnet start logs lines like these (timestamps omitted):
```
INFO keeper: Starting Continuum Keeper Bot
INFO keeper: Env validated: γ decay UNCONDITIONAL, GAMMA_DECAY_COEF=1.0 (unset default)
INFO keeper::config: [discovery] QQQ → ACTIVE (F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5)
INFO keeper: Networks configured: devnet
INFO keeper: Dashboard port resolved to 8484 (from default(8484))
```
The dashboard serves `/healthz`, `/api/status/`, `/api/sol-health/` and `/api/pool-state/`.
## Common issues
### The keeper refuses to start
Remove `CLMM_GAMMA_DECAY_ENABLED` if it is set, and check that `GAMMA_DECAY_COEF` is a number in (0, 1]. On mainnet, also set the collateral mint and the oracle, mint-redeem and CLP program IDs; there is no fallback to devnet values.
### It runs only QQQ, skips markets, or fails with `UnauthorizedKeeper`
1. Set `DEVNET_MINT_REDEEM_PROGRAM_ID` or `MINT_REDEEM_PROGRAM_ID`. Without it, discovery fails and the keeper falls back to QQQ.
2. Set the collateral mint to cUSDC on devnet (see the warning above).
3. Check that `market.keeper_authority` is the keeper wallet's public key. The admin may have rotated it with `update_keeper_authority`.
4. On insufficient-lamports errors, check both wallets. Rent for new pool accounts comes from the CLP authority.
### "Financial transactions paused" or `UNCONFIRMED_TX_MAY_STILL_LAND`
An earlier send ended with an unknown outcome, so the keeper stopped every transaction that moves funds. It re-checks the journaled transaction every 30 s. Once the log shows "Reconciled financial transaction … restart keeper to rebuild plans and resume", restart it. If the journal is reported unavailable or already owned, configure R2 or a persistent `DATA_DIR`, and make sure no other process holds the same journal.
### RPC rate limits
List several endpoints. After 3 consecutive retriable failures (HTTP 429, 503 or a transport error) an endpoint is skipped for 30 s, then probed again.
### A book sits far from NAV
If the underlying market is closed, this is free float by design, and the first fresh price re-centres the book. If the market is open, check `CLMM_POOLS_ENABLED` and the `keeper::clmm_pool` logs, then the CLP vault balance if the book's depth is exhausted. A stale price while the market is open means the source is down or Hyperliquid is vetoing it; check the `keeper::oracle` logs.
### Leftover L or S in the keeper wallet
The boot-time sweep that cleared it belongs to the legacy layer and is off by default. Redeem matched amounts with `redeem_paired`, or a single leg with `keeper_redeem_single`, signed by the keeper wallet.
## See also
What the keeper runs and which keys it holds.
Why the venues stay near NAV and what bounds drift.
Reposition loop, free float, refill, trim and bid cap.
# Listing a market
Source: https://docs.continuum.markets/markets/listing
How a new asset becomes a Continuum market. Listing is an operator process run by the Continuum team; this page walks through each step and what it changes on chain.
The Continuum team lists markets as an operator process, and its listing scripts are not public. Listing needs the market authority, the GlobalClp authority and changes to the keeper and the app. This page describes each step, so you can see how a new market's accounts are built and when it becomes usable.
The on-chain [registry](/programs/registry) plays no part in listing today. It holds no markets and nothing reads it. New addresses are published on [Address registry](/reference/addresses).
## Choosing an asset
Continuum lists TradFi assets only: stocks, ETFs, metals and FX. An asset qualifies when all of these hold:
* A free public source quotes the same instrument the market tracks, with no API key. A proxy doesn't count. OIL was withdrawn on 30 Aug 2026 because its only keyless quote was the front-month WTI future, a different instrument that carries a roll basis.
* Hyperliquid lists the instrument, or an index the keeper can track it against, so that every price push can be checked.
* For mainnet, Pyth publishes a feed for it.
* The keeper has an entry for it in its volatility config. Its fees and spread floors depend on it.
## On-chain sequence
Create the long and short mints as classic SPL Token mints with 6 decimals and no freeze authority. Their mint authority is the Market PDA (`[b"market", symbol]` on mint-redeem), which can be derived before the market exists. The collateral vault is the cUSDC ATA of the Market PDA.
The mint-redeem program's upgrade authority (the admin) calls `initialize_market(params: InitMarketParams)`. Any other signer fails with `UnauthorizedInitializer` (6046):
```rust theme={null}
pub struct InitMarketParams {
pub asset_symbol: String, // at most 10 bytes
pub oracle_type: OracleType, // Continuum
pub initial_l_price: u64, // scaled by 1e6
pub initial_s_price: u64, // scaled by 1e6; equals initial_l_price on every live market
pub mint_fee_bps: u16, // at most 1000
pub redeem_fee_bps: u16, // at most 1000
pub keeper_authority: Pubkey,
}
```
With `OracleType::Continuum`, the `oracle` account must be the market's `OracleConfig` PDA (`[b"oracle_config", market]`), which the next step creates. The market starts active, in the `Normal` risk state, with `dev_tax_pct = 10`. Its first reference price is the initial L price. Collateral yield starts enabled (`yield_enabled = true`) with a ceiling of 0, so harvests work but nothing can be deployed until `configure_yield_ceiling` raises the ceiling.
On the oracle program, `initialize_oracle_config(max_confidence_interval, max_staleness, keeper_authority)` creates the `OracleConfig` PDA. `upsert_oracle_feed(priority, kind, is_active)` registers each Pyth price account as a feed. Mint and redeem check only the `OracleConfig` address. On devnet the keeper doesn't update it and writes prices to `Market.user_twap_price` instead.
On the clp program, `initialize_clp(oi_cap, oracle_config)` creates the per-market `Clp` PDA (`[b"clp", market]`). Every live market stores an OI cap of 250,000 cUSDC (`oi_cap = 250_000_000_000`), but nothing enforces it yet. The CLP vault and the CLP's L and S token accounts are ATAs of the `Clp` PDA. `allocate_to_market(amount)` moves capital from the GlobalClp vault into the CLP vault.
`init_pool(side, step_bps, num_bins)` creates each `Pool` PDA (`[b"pool", market, [side]]`, side 0 long, side 1 short), and only the GlobalClp authority can sign it. `configure_pool_geom(wing_step_bps, core_bins)` then sets the two-zone geometry. The pool's token vaults are its ATAs for the synth mint and for cUSDC.
To seed the books, the operator sends cUSDC to each pool's cUSDC vault for the bids. It also mints L and S as a pair, sending the L to the long book's synth vault and the S to the short book's. The first `set_pool_shape(base_price_1e6, asks, bids)` posts the bins and sets `enabled = true`. From then on, nothing can disable or freeze the pool.
The market moves to ProxyMode, where every devnet market runs. Committed orders stay off until the CLP authority sets `swap_guards.enabled` with `configure_swap`; since 11 Oct 2026 they are on for every live market. The books don't depend on this flag, but fill only while the market's price is stale. `create_token_metadata(name, symbol, uri)` writes the Metaplex metadata for each mint.
## Choosing the geometry
`init_pool` accepts a core step of 1 to 1,000 bps and up to 16 bins a side. `configure_pool_geom` requires the wing step to be at least the core step and `core_bins` to be at most `num_bins`; a wing step of 0 keeps a uniform ladder. The live books use these settings (10 Oct 2026):
| Asset class | Markets | Core step × core bins | Wing step | Bins a side |
| - | - | - | - | - |
| Index ETF | QQQ, SPY | 15 to 20 bps × 3 | 250 to 300 bps | 12 |
| Metal | XAU, XAG | 20 bps × 3 (XAU long book: 50 bps) | 300 bps | 12 (XAU long book: 6) |
| Single stock | NVDA, AAPL, MSFT, TSLA, AMZN, GOOGL | 25 to 35 bps × 3 | 400 to 500 bps | 12 |
| FX | EUR, GBP, JPY | 10 bps × 3 | 200 bps | 12 |
The books charge no swap fee, so the gap between the centre and the first funded bin is the price of an instant trade. A tight core step gives better prices but loses more to informed traders when the price gaps. That is why the volatile single stocks get wider steps than the index ETFs and FX.
## Keeper changes
The keeper's source isn't published. Before the protocol's keeper will price and run a new market, it needs these changes:
1. Add the symbol to the discovery candidate list (`DISCOVERY_CANDIDATES`). At boot the keeper reads each candidate's Market PDA and runs every market with `is_active = true`, unless `ACTIVE_MARKETS` overrides discovery.
2. Map the symbol to its price sources: the public quote source's symbol, the Pyth feed IDs for mainnet, and a benchmark ticker that seeds the volatility state at boot.
3. Map it to its Hyperliquid instrument for the price check. If Hyperliquid lists only the index, as for QQQ and SPY, flag it as an index proxy so the keeper learns the ratio.
4. Add its entry to `vol_coefs.json`, the keeper's hand-fit volatility config. A test in the keeper's suite fails until every candidate has one.
5. Add its buffer target to the keeper's per-market fallback table, sized like the others from the jump-diffusion study. The keeper's daily job is the primary source of each market's target, and a market missing from the fallback table posts 1% (100 bps), the program's minimum.
6. Redeploy. Discovery runs only at boot, so the keeper ignores a new market until it restarts.
## App changes
The app at continuum.markets reads a static address list, so a new market also needs:
* an entry in the app's market address list with the market's mints, vault, CLP accounts and books;
* an asset class in the app's market taxonomy, since the build fails for an unclassified market;
* a logo, plus the token images and metadata JSON that the on-chain metadata points to.
## When a new market becomes usable
| After | What works |
| - | - |
| `initialize_market` | The market exists and is active. Its first reference price is the initial L price, stamped at creation. Once that is 300 s old, mint and redeem revert with `TwapNotAvailable` until the keeper pushes a price. |
| First `set_pool_shape` | Both books are enabled and trade at the posted bins while the market's price is stale (outside `Normal`, more than 300 s old). |
| Keeper redeploy | The keeper discovers the market, pushes prices, posts fees, the committed-order spread and the buffer target, and manages the books. Mint and redeem work whenever its last price is no more than 300 s old. |
## Pausing and delisting
The market authority pauses a market with `update_market(is_active: Some(false))`. Mint and redeem then revert with `MarketNotActive`, and the keeper drops the market at its next restart. Existing L and S tokens stay valid SPL tokens, but they can't be redeemed until the market is reactivated.
Pausing doesn't touch the books. Nothing can disable or freeze a pool, so a paused market's books stay tradable until the operator sweeps their inventory back to the CLP with `pool_sweep_usdc` and `pool_sweep_synth`.
A full delist goes further. `vault_redeem_pairs` turns the CLP's L and S back into cUSDC, and `return_to_global` sends the capital back to the GlobalClp. `close_market_admin` can close the Market account only once its L and S supply, total collateral and deployed yield are all zero.
## See also
Markets currently active on devnet.
Planned markets.
Market structure, book geometry and trading hours.
# Live markets
Source: https://docs.continuum.markets/markets/live
The 13 markets live on Solana devnet, with every address each one needs and the fees and collateral read from chain on 12 Oct 2026.
Thirteen markets are live on Solana devnet, with 26 bin books between them. Together they hold about \$1.37M of collateral, the sum of `Market.total_collateral` on 12 Oct 2026 after the protocol's own holdings were cut to working size (see below). Mainnet markets will be added when mainnet launches. MSTR was wound down and paused on 12 Oct 2026 (see [MSTR](#mstr-microstrategy)). OIL was withdrawn on 30 Aug 2026 and VXX was delisted in June 2026, so neither appears here.
The cUSDC mint for all devnet markets is `B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE`. On mainnet the collateral will be USDC.
## At a glance
As of 12 Oct 2026:
| Market | Tracks | Initial price | Fee (bps) | Committed orders | Total collateral (cUSDC) |
| - | - | - | - | - | - |
| QQQ | Invesco QQQ (Nasdaq-100 ETF) | 480 | 18 | NAV ± 18 bps | 579,303 |
| SPY | SPDR S\&P 500 ETF | 715 | 16 | NAV ± 15 bps | 383,628 |
| XAU | Gold spot, USD per troy ounce | 4,683 | 18 | NAV ± 18 bps | 233,941 |
| XAG | Silver spot, USD per troy ounce | 64.84 | 25 | On | 15,947 |
| NVDA | NVIDIA | 210.71 | 31 | On | 16,761 |
| AAPL | Apple | 298.17 | 22 | On | 16,806 |
| EUR | EUR/USD | 1.14746 | 10 | On | 16,684 |
| MSFT | Microsoft | 483.31864 | 29 | On | 17,562 |
| TSLA | Tesla | 362.79505 | 38 | On | 17,895 |
| AMZN | Amazon | 258.62 | 32 | On | 17,346 |
| GOOGL | Alphabet (Class A) | 344.72516 | 32 | On | 17,451 |
| GBP | GBP/USD | 1.36438 | 10 | On | 16,880 |
| JPY | USD/JPY (yen per dollar) | 158.986 | 10 | On | 16,414 |
On 12 Oct 2026 the CLP's own holdings, which had been about \$16.6M across these markets, were cut first to about \$50,000 a leg per market and then, the same day, to 0.5% of each market's claims per leg, the share the keeper's nightly job now re-derives. Committed orders mint what they need when they settle. The CLP redeemed the rest at NAV with `vault_redeem_single`, so no holder's tokens changed value, and \$16.9M of collateral went back to the treasury. Most of what remains sits in the bin books. Each book was left balanced between its long and short values. QQQ, SPY and XAU hold more because some of their long tokens sit outside the CLP, and the CLP keeps short tokens to offset them.
Every market set `initial_l_price` and `initial_s_price` to the same value, so its constant K is that price squared. No market has been re-anchored yet.
The keeper posts one fee as both the mint fee and the redeem fee. All devnet markets run in ProxyMode, which charges 1.5× the posted fee, so QQQ's 18 bps costs 27 bps.
Committed orders fill at NAV plus or minus the market's committed spread, shown for QQQ, SPY and XAU. The keeper re-posts each spread as volatility moves, so read `Clp.commit_spread_bps` for the current value. Since 11 Oct 2026 committed orders are on for every live market, and they are the protocol's only venue while a market's price is live; traders' orders on the [order book](/flows/order-book) are open at any hour. MSTR was wound down and paused on 12 Oct 2026 (see [MSTR](#mstr-microstrategy)). The bin books trade only while a market's price is stale, and the CLP's instant `clp_swap` was retired on 11 Oct 2026. The JPY market tracks USD/JPY, so `JPYL` gains when the yen weakens against the dollar.
Each market's `Clp` stores an OI cap of 250,000 cUSDC, which nothing enforces yet. The bin books charge no swap fee.
On devnet the keeper prices every market from a public quote source. Before each push it checks the price against the Hyperliquid market named in that market's table below, and a disagreement blocks the push. Hyperliquid can veto a price but never sets one. Mainnet will use Pyth. See [Oracle](/concepts/oracle).
## Shared accounts
| | Address |
| - | - |
| **GlobalClp PDA** | `BcGR8kuGCiMhwonyztXZHEZtjJXxXVRgjwbn58bUfXtw` |
| **GlobalClp vault** (the treasury) | `5GSTKN4u5LfropcnQQoqFht48JMaF1SzZzBcgDNL5VKy` |
| **`TreasuryPolicy` PDA** | `58JeHAcMAAyEZWusVqmvoFNVqh37cTkmfbbP6bMHExWR` |
| **`TreasuryYield` PDA** (the treasury's lending ledger) | `6YrCADSTyTJPNrVhrZz5S2wTzBQSPM5fGUrGSpLTBJgY` |
| **`RevenueAttribution` PDA** (a `pool_swap` account, just before the market) | `A8mK2pnagabEcnkSiWieEZyQjjNWFYm1Z91dUVMXe6a4` |
Each market keeps its collateral in its own vault, the cUSDC ATA of its Market PDA, listed in its table below. QQQ, SPY and XAU moved there from the shared unified vault on 11 Oct 2026, and that vault now holds 0 cUSDC.
## QQQ: Nasdaq-100 ETF
| | Address |
| - | - |
| **Market PDA** | `F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5` |
| **Long mint** (`QQQL`) | `E4xJxBPyDXXJK2uqF17cq9kch1xHCTBZ6vjMxKWKANgZ` |
| **Short mint** (`QQQS`) | `BisvPibkUJqix3ZzaeHCxJ2W7KqoroFpnyf55nzFoicG` |
| **Oracle config** (`oracle` account) | `7uMgq2tpFW9FatCwAPtAjndvADCr3p8NiNFo7EZLSaV1` |
| **Collateral vault** | `CGEMQpzitYvEnWE7sggqD4aBMpTmXhckQPYMhzhy9oJm` |
| **CLP PDA** | `EEDKqU3C4wgWQBcgFagHTqEAjKNMCV4eVYyaUt9swKHc` |
| **CLP vault** | `8Doh7uAU95RkqZDx66C1643Q8xJeg4T8eYKMak6ac1En` |
| **Long book** | `DbPryNwJ2ThJPTzb7UMBoEQhR2QEqcHxenGA8JCqG9Qr` |
| **Short book** | `HUudWC8ykMCZg3CNs3TPhRmLczdrPVwmXXSYvvKAxKbB` |
| **Hyperliquid check** | `xyz:XYZ100`, a Nasdaq-100 index perp (the keeper learns the ETF-to-index ratio) |
## SPY: SPDR S\&P 500 ETF
| | Address |
| - | - |
| **Market PDA** | `GT1XPJmWNxFukQXVxCmT6c9VdLdR6Da54aU9m9QGfd8b` |
| **Long mint** (`SPYL`) | `8mVLeXHpchLvt3WZRhRrAZYJ4oU3e2crVF1QEomgdDQ8` |
| **Short mint** (`SPYS`) | `3n5x1QaXSSw5ysAK5zkMo1XSm52TwzTBK48RAfkeKJiu` |
| **Oracle config** (`oracle` account) | `CZnrCy19NmVT8JJHBE4Q7MFjkk25NaXdDguKsvqWg8uE` |
| **Collateral vault** | `DgL6UTux2gjRubznEnEQ1WprjajW5KYaegBEKgSncgPc` |
| **CLP PDA** | `F4eAvoAd2tUnpDz7dSqj2d4eCBzHLHYTvPmiwHNe1nnX` |
| **CLP vault** | `J5cj2Ba1bMsgK4qk2uis6asL6hUSwAd53kYepteBG94B` |
| **Long book** | `4UZmmZYrRfffQmjq2iEaNgkit72s8VS5cnhK5GeuKSt8` |
| **Short book** | `4JoXbxzAAeAWsQgTYXgWAvRqdR1RU2Tb3ywm2FNLKjAB` |
| **Hyperliquid check** | `xyz:SP500`, an S\&P 500 index perp (the keeper learns the ETF-to-index ratio) |
## XAU: Gold (spot)
| | Address |
| - | - |
| **Market PDA** | `8HVoUTS3iNdKSCHhvhc2FNRnXjd7yHcEARD6mTFjem7b` |
| **Long mint** (`XAUL`) | `AoufiP54693dn9HKonVBn6W75tKe8eHq5ncTwjTz6i5c` |
| **Short mint** (`XAUS`) | `9XjYivtQfhDitFJugsskP6LppL7JJzANWwc7kmybpd55` |
| **Oracle config** (`oracle` account) | `H3kvpKAQbriHGr6CZWWpXreUFa9ox4pdHPAoeqYSuLt4` |
| **Collateral vault** | `2Z9W8e64czfRDu7DoJmZbhZGbm6W7ERuUnXS7iWzU2xx` |
| **CLP PDA** | `DdUuCiNzv5A3XZJk5jj4vY8zGS1avcrcMu1MuR4J65BA` |
| **CLP vault** | `C6X4VEBggnwdC6Ju5tBMm8wxRiFekRZQrWSeX5Ja2kVn` |
| **Long book** | `5rEH7sYUS7kuqNiLZmCiQGwQLmQ1qqogRPavT9csKmHL` |
| **Short book** | `CW8nvKYPi7xgRtimCk1Gj4Db6bbXdqXngdDMxDA6rLHT` |
| **Hyperliquid check** | `xyz:GOLD` |
## XAG: Silver (spot)
| | Address |
| - | - |
| **Market PDA** | `6vX3BNEbeem7xjiTKuMC9uMDD3hxPxo9fgM834BucH3n` |
| **Long mint** (`XAGL`) | `VETzCeHk2mobTaUATNRBcokKE3x8YrcRyGx3Vsz4prr` |
| **Short mint** (`XAGS`) | `DnHSia6WX5jYPnRkSTfu4GtbrTw2CAdLWKRmdMsWPHU3` |
| **Oracle config** (`oracle` account) | `5wW4bJZ3vLtsGVDrfCuoAUBozzNgKDdp3urSRyERD6x3` |
| **Collateral vault** | `2Gy5SFgx7uV7ybuVR9DmbD5921z8iJrrEVDMfo5XqRFQ` |
| **CLP PDA** | `DKnAJi6m98Z9521tAQ7XbGFrxBsrnnmMXqfQSDLfxPk2` |
| **CLP vault** | `C6ne1mYqN5CKkgBMnV83q9yDv9RLDZdosZaavWzJp5X7` |
| **Long book** | `EVPrpQiPPXwDorKPrSop36WFg53wTujdGyFFb6j8VM9j` |
| **Short book** | `DsiYTmZcTSq3i4NfBfMFJBST7Kyess73yjVcRNJ8m5Fw` |
| **Hyperliquid check** | `xyz:SILVER` |
## NVDA: NVIDIA
| | Address |
| - | - |
| **Market PDA** | `3dadTP1Wce1CBrwxhCXor4HanjioauS7Qey3TFtpriVb` |
| **Long mint** (`NVDAL`) | `B8M3y6wHGFZidqYciVxGu27KYk2dLcqgBGNoUDXMVzNN` |
| **Short mint** (`NVDAS`) | `6n525VkawSkm6h1HxisHCpRssdbmazb1Q9S3Bi2f9emL` |
| **Oracle config** (`oracle` account) | `H3wyiSQg8BubRwEdRaHYCMVx4HMwtAYdjgqEfohJmSyd` |
| **Collateral vault** | `2rJ26he3T7VjjXVYoJRanx15pqWSERxHQCfSqh71uWMK` |
| **CLP PDA** | `HkyqDQ55Mhz3pHDMzeRVtiZBEwVQfz2yoVZ3e8cKJC7H` |
| **CLP vault** | `BupGi5KavHSB8bVHPzG4hnq7ZXRJySe7d8zAq1R4KpP4` |
| **Long book** | `6oBYXyw6bQqySqEEXdkYGtZxzBN2VXd8RR66sf9z7Wh4` |
| **Short book** | `8evyJuf3b6g3x8DHSXHjQYBvXFBHmyDEyycRpQ183mUn` |
| **Hyperliquid check** | `xyz:NVDA` |
## AAPL: Apple
| | Address |
| - | - |
| **Market PDA** | `4ZunzWTX3XAhdhPvjsfFLaXXzZSgMD7CPr9moC1XKpSA` |
| **Long mint** (`AAPLL`) | `7ofckkctk9wUpTn77zFv8t8yb3iBUSkuU3vmcXbaUboS` |
| **Short mint** (`AAPLS`) | `438bLh6shsVHXoQwtaRP56zWuAsLJK1o2u6FyRe6crYV` |
| **Oracle config** (`oracle` account) | `7YwnWSbwNxJhzKkt3WoAkJ7nxnzpcAFNBsQ7gyYgpEVe` |
| **Collateral vault** | `2qsaxGmZEBgh8WUcJisMb4pejMdAQJT6X8HD2VT3FMaD` |
| **CLP PDA** | `JAF3WYJGRA5aLofT1NEKG7Tzr5XWa4vH8mtaccc9K8VJ` |
| **CLP vault** | `AAwyDwCH4uXhRzAHfnLfVfMv4RncSXzt8RXXgFmPMVTR` |
| **Long book** | `DPFt4A1hGeaCWh8c8mVqT1JNG8vGUTqKsMejVmXVkfaE` |
| **Short book** | `6v9KZZY4uXaNHkmq2JSJHcbuntCCrXkCyrz8WZD3pGef` |
| **Hyperliquid check** | `xyz:AAPL` |
## EUR: Euro (EUR/USD)
| | Address |
| - | - |
| **Market PDA** | `CkXdq7RNCu6sCk7A3UZdBB1JNR8ESaBWqH8bdeMsUbYR` |
| **Long mint** (`EURL`) | `FLDQtZ2adGWTcawQh6jziufJejkexoPA2uYimQTqA8z9` |
| **Short mint** (`EURS`) | `z5u5UYz5jWdbycMexwdYvkw56fLY1uvx5RZW3EtxRsM` |
| **Oracle config** (`oracle` account) | `J4GhhLqUwbBEZ9E9C6ryGDD3GxbV9pzkWCXPrqjDhcGh` |
| **Collateral vault** | `C7Ur9siuwWB1MYkEyMQWE347jUxue8Xz1fvLfcnWguC5` |
| **CLP PDA** | `Esd4RB9qMfcmH8AAtiGgLLBXskZZ4xkcBExPGGTQPZj2` |
| **CLP vault** | `6LFwtV47WzjFQc86wigXUYVxECBwCmNt8LpRJanLJyNd` |
| **Long book** | `CSpjyN6YkFkhbLKux47pnw4KeWQAtWNgu6yBHTn7SsGe` |
| **Short book** | `8QJiubkx3xWxNGXrPSUjTsaTss6TcNBiMNT5xzrByZfp` |
| **Hyperliquid check** | `xyz:EUR` |
## MSFT: Microsoft
| | Address |
| - | - |
| **Market PDA** | `HzCTyT5rRVUme6JugGsvnyCQ4wrsyZ6WrbX5FfaMR3Q3` |
| **Long mint** (`MSFTL`) | `GaGvW4zuXk84dJwmTXFYrxJaY4otHsJPasc7P5sjdCrA` |
| **Short mint** (`MSFTS`) | `4T1yPzWCxijyJEKiLmjfHYFNMJeiwGf2GSGuAsKwr9Fp` |
| **Oracle config** (`oracle` account) | `71F1zwVKtTTTXbmpDHmF4Jg7YwxyWPY9aVvCTgQJWjKd` |
| **Collateral vault** | `6p6HP29C4yaKCg3NaK62RfMpFccu6ja8HSS13BC4nVGE` |
| **CLP PDA** | `3LFUK62KWYe5nz8Uhz4EbyTDpSZTmksMiFhMpBCJge6z` |
| **CLP vault** | `481HdR4j6EvhLGMsN6KvvfTPvEHG2pqEbyLVz5rmrJnj` |
| **Long book** | `CZPtcyprVZNTQYMfCoV7NyZM8LKyfBeA8c1Lk5NoKmEq` |
| **Short book** | `7aDvEwdSFtHyknZ2hc6fwnhnhgFMDp36U4fJEDX9gh7T` |
| **Hyperliquid check** | `xyz:MSFT` |
## TSLA: Tesla
| | Address |
| - | - |
| **Market PDA** | `HvPnCkZq2z163WFxTGCgp7V13qhrNpiZvDywAgQaPTQg` |
| **Long mint** (`TSLAL`) | `4dt2EW6pNJRBup7gqxtsQF1GgNKR4RWoj5bSXuY5f1JH` |
| **Short mint** (`TSLAS`) | `71Mh4et7Xvf8wdQJJ3ZUk51wgyervZVdjN6A4bCkWFfG` |
| **Oracle config** (`oracle` account) | `DSNRGdZUqkwH9HoYr7XLsMm2VZsY3sWXJNouAT85ghCM` |
| **Collateral vault** | `2kiAUxgp9UsTtLFyZySAT6vihc2dDfH23jKmiStiEYWu` |
| **CLP PDA** | `8B8a7fqBdgq2DQgiNdN3cQhHJda6pNJykh3DSt5EN4jB` |
| **CLP vault** | `HS2DhzxvtKTF432dMT93Tu7oto7mWGGJNQ5kAQVqnnrQ` |
| **Long book** | `BQcoDb7XfwYtnugVcRfW6VmigAGpW7dGBtgwQNLHPqGY` |
| **Short book** | `BHnZ49UvLUJokEpdJyXM8gAZgonCBCFoYfvVAx5Mj75n` |
| **Hyperliquid check** | `xyz:TSLA` |
## AMZN: Amazon
| | Address |
| - | - |
| **Market PDA** | `tEGPdiYZFyysq6YJeASdtzs6qzHUGKS96qp7XYJjBpj` |
| **Long mint** (`AMZNL`) | `3vnTkSkevAJ69Vs8PFRNzF1hKvSiFbiLZJetNyqUcLZN` |
| **Short mint** (`AMZNS`) | `5ESabwnWACsFyDE96JNwbLkTpKyBHyNYM96PHEBvuzPc` |
| **Oracle config** (`oracle` account) | `8ZUofpK6Dj7Y8SDT7kiLawZjEeQ3eWiptLPZMum4W1p6` |
| **Collateral vault** | `125Emh8gbBitA7h9PTwkSzLULZinA1GhvSPztZrgk9eT` |
| **CLP PDA** | `3S1RWBt4X4GW5Q9tvmZetgUrrbKDxwWoaFwGT1fFeKGS` |
| **CLP vault** | `F3VjE6jt47zRyVqGfHmiet2kVKd7gKh3NLNS3xrb5tPB` |
| **Long book** | `CHvNFzHqHbhT9BdinSBhww3CF4bMepL3zF2vLDUKTkDi` |
| **Short book** | `CZV1NDMVX8BPiKKTXSZsgS1VgahQDo1PXtsZdPB2tMGW` |
| **Hyperliquid check** | `xyz:AMZN` |
## GOOGL: Alphabet
| | Address |
| - | - |
| **Market PDA** | `7kmfunh8gUn2h9eafmJHUP6xeR8qM6yJZVEEyRzKiXhb` |
| **Long mint** (`GOOGLL`) | `42kcUPz39qudU5qv9tgU6a3E7jmiJhKs7AoXDUcarKQ8` |
| **Short mint** (`GOOGLS`) | `AGzVgTj53MwsJd8Ke2iyibRSyVLsqdJS3DzQWsSAxgdj` |
| **Oracle config** (`oracle` account) | `BLWwgcpbAMADpaA1YVTc2jy7ScxmSa7G47Xze75kiyuv` |
| **Collateral vault** | `7NHs4FXpKdtB41Z5LdYkCq3qTmCi6JG2j6eaA8nmNToX` |
| **CLP PDA** | `HRo8q96iUnTP8Secf7eqYfADkNVokLvfy4eg7DjpkqrN` |
| **CLP vault** | `6qe3HfoWrC2g7k8URTDrAJYVreCieQGZA8LBZZByoqEM` |
| **Long book** | `82RWToVd47PC7kd3ScNM4BwAiascY82oAcu4e4NhStvp` |
| **Short book** | `6CVRNQPruyE8LLQvdfiVwp6uEiqxcRYjCTD7wU9a2DS1` |
| **Hyperliquid check** | `xyz:GOOGL` |
## GBP: British pound (GBP/USD)
| | Address |
| - | - |
| **Market PDA** | `HjpaEbyXJBqzxP6B7eNnbZvtaUPXRubc5bEywzfMePqK` |
| **Long mint** (`GBPL`) | `HSGiyJGoAMKYmEG6cZkvnTko3B3BP6nnVCYuikBxRLSJ` |
| **Short mint** (`GBPS`) | `F6dZpqSCevWEUdCvv6ARaePy5RnqkcVJBm7UubjV8fSG` |
| **Oracle config** (`oracle` account) | `BkY1KehG2HZczmu6F1WpzJ2oHfqztESQviJjx7kDoXfn` |
| **Collateral vault** | `3gtGoZTrWbCVdD6vfwz2614aFwvS2JzgQmENadfR8gkF` |
| **CLP PDA** | `4gskdJfMfv6sCPqyWErXayUdzJBPLg5GyiZ6jSJ4EB8J` |
| **CLP vault** | `5rfH9Er5GFHGcK4wHvga8JRRCS1EAqJsWcnGWMJMVDB5` |
| **Long book** | `3e59vEqzQJasUiBHstdCHRKh9KJTtG83w3Mo5W1ASHDw` |
| **Short book** | `B1UyrycWcxpV9DPTnEhKLf1zfeomnJVVriST7TdKRBtR` |
| **Hyperliquid check** | `xyz:GBP` |
## JPY: Japanese yen (USD/JPY)
| | Address |
| - | - |
| **Market PDA** | `2hZtLxJtPeYjogUQFWeGYEKjJkWArEcf9iTtGkrChuKC` |
| **Long mint** (`JPYL`) | `4NVo5FmKT1Z1Bid3RuRa9obpYgyusrQdxByTPLw6KtsT` |
| **Short mint** (`JPYS`) | `VqgP5wUX6jZKEtWQiktmip8ntGz2eKCjdJ4ttjfo93a` |
| **Oracle config** (`oracle` account) | `7GhyT3C7nqnK9FbvpeiGYzNNQP2AvEDnMgL7FSW9cVg1` |
| **Collateral vault** | `F78vUS2GfNgn2NC41bLJSPCb2wZaPoGGGDFYfxhoLGfA` |
| **CLP PDA** | `HKzUKzNyHDyC5AgEYFFscE3PETBB3SfrfCKnt1hGLE1K` |
| **CLP vault** | `8PVXUQ6VuuYor9PH7vew3sY35civX2YSJH1jgCXEsaXU` |
| **Long book** | `NyL2zCi4am2Kiofr6NfNcVCyf23rmcnsqVgNaZPj7mf` |
| **Short book** | `FsDgk3EkBJbdqEP2C9uyh86qdjy4KpYpg1PTytW86FY4` |
| **Hyperliquid check** | `xyz:JPY`, also quoted as yen per dollar |
## MSTR: MicroStrategy
MSTR was wound down on 12 Oct 2026 and is paused (`is_active = false`), so mint and redeem revert with `MarketNotActive`. Every MSTR token was held by the protocol. The keeper emptied both books and returned their inventory and the CLP's working cash. One transaction then redeemed the CLP's pairs except one, released the collateral above 101% and paused the market. About \$1.36M went back to the treasury. One protocol-held pair and about \$249 of collateral remain.
| | Address |
| - | - |
| **Market PDA** | `66WAH37WCVp1xxSBf3gqJQB4zgQnH1JGRaFymBCSpK9Y` |
| **Long mint** (`MSTRL`) | `4cu47GCf1uFiLFY4M4pz5fxXaQNkFjFCSGZMryHKLx2G` |
| **Short mint** (`MSTRS`) | `2tQ9cyCZX4vmG9rUkonKAtW8oCrgPSZn9Z6rSbrPHT7h` |
| **Oracle config** (`oracle` account) | `BtYkLGtZAh9ifwiV2pyuBFMEwFFC5zUX22HfYpajAt84` |
| **Collateral vault** | `HKStxHs4kWPqHWWN1fkcSCZ6n6QHvbafwcURV6LTQ3Ju` |
| **CLP PDA** | `vR53HxPcBUkjuVzsLYofMGU4GKhy5QgTaJZMBDzjU7n` |
| **CLP vault** | `Gs5TSSLFRAriH27zXNy7WrKXzSwcFbJCTKnC7kqb7Qs4` |
| **Long book** | `G9xCpYdpCwyXw9Y5z2aMddDbVVvj1iy87RRozaAoY9LK` |
| **Short book** | `TS4njAZnRctsppgdTiHptAUNitoY6FjFMkXjqHKLQU7` |
| **Hyperliquid check** | `xyz:MSTR` |
## Programmatic access
Derive the PDAs from the market symbol rather than copying them by hand. [Address registry](/reference/addresses#derive-addresses-from-a-symbol) has a sample that returns every PDA and pool vault for a symbol. The mints, the collateral vault and the fee recipient aren't PDAs; read them from the Market account (`longMint`, `shortMint`, `collateralVault`, `feeRecipient`).
## Trading hours
Mint and redeem aren't 24/7. They revert with `TwapNotAvailable` once the keeper's last price is more than 300 s old, and the keeper pushes no prices while the underlying is closed. The bin books are the reverse: they fill only while mint and redeem are frozen, and the keeper then posts each side in one bin within 1% of the last price, sized to measured demand: \$500 a side while there is no measured flow. While prices are pushing, trades go through committed orders.
→ [Concepts → Markets → Trading hours](/concepts/markets#trading-hours)
## See also
How a new market is added.
Planned market additions.
RPC, faucet, behavior gotchas.
How to read live data from these accounts.
# Roadmap
Source: https://docs.continuum.markets/markets/roadmap
What's live, what shipped recently, what's next, and what's not coming.
Continuum's MVP runs on devnet. This page covers the live markets, recent releases, the planned market roster and the path to mainnet.
## What's live (devnet)
| Market | Asset | Live since |
| - | - | - |
| **QQQ** | Nasdaq-100 ETF | Apr 2026 |
| **SPY** | SPDR S\&P 500 ETF | Apr 2026 |
| **XAU** | Gold spot | Apr 2026 |
| **XAG** | Silver spot | Jun 2026 |
| **NVDA** | NVIDIA | Jun 2026 |
| **AAPL** | Apple | Jun 2026 |
| **EUR** | EUR/USD | Jun 2026 |
| **MSFT** | Microsoft | 23 Aug 2026 |
| **TSLA** | Tesla | 23 Aug 2026 |
| **AMZN** | Amazon | 23 Aug 2026 |
| **GOOGL** | Alphabet | 23 Aug 2026 |
| **GBP** | GBP/USD | 23 Aug 2026 |
| **JPY** | USD/JPY | 23 Aug 2026 |
| **MSTR** | MicroStrategy | 25 Aug 2026 |
MSTR was wound down and paused on 12 Oct 2026, so 13 of these markets are live, with 26 bin books and about \$1.4M of total collateral (12 Oct 2026, after the protocol's own holdings were cut to working size). The books charge no swap fee. See [Live markets](/markets/live) for every address.
Two markets have left the list. VXX was delisted in June 2026. OIL (WTI crude) was withdrawn on 30 Aug 2026. Its Pyth feed had stopped publishing, and the only keyless quote left was the front-month WTI future, a different instrument from the one the market tracked.
## Shipped since August
* Seven markets: MSFT, TSLA, AMZN, GOOGL, GBP and JPY on 23 Aug 2026, and MSTR on 25 Aug 2026.
* Corporate-action support. `apply_corporate_action` (authority-only and time-gated) maps the raw price through `price_adjust_num / price_adjust_den`, so a stock split leaves every leg's NAV unchanged. The ratio is armed ahead of the ex-date. No action is armed today.
* γ renewal with `reanchor_gamma`. Only the market authority can call it, and calls must be at least 14 days apart. It folds the accumulated γ into `initial_s_price` and resets γ to 1, which leaves every holder's claim the same at every price and lets γ keep charging. It does not reset the market's anchor to the spot price. No market has been re-anchored yet.
## Planned markets
Continuum's targeted launch is **TradFi only**. There are no crypto markets: for a crypto pair, the paired design adds nothing over an actual perp DEX.
| Class | Planned |
| - | - |
| Indices | SPX (S\&P 500 index), NDX (Nasdaq-100 index), FTSE 100, DAX, Russell 2000 |
| Equities | GME (GameStop), AMC (AMC Entertainment), META (Meta) |
| Commodities | BRENT (Brent crude), HG (copper) |
| Forex | USD/CHF |
Natural gas is blocked: there's no continuous feed for it, only dated futures contracts. Every listing faces the same test: the market needs a quote of the instrument it tracks, and a futures contract standing in for a spot price doesn't count.
The order of rollout depends on three things. The first is price sources: on devnet, a keyless public quote of the same instrument plus Hyperliquid coverage, and on mainnet, a Pyth feed. The second is liquidity, so deep and actively traded underlyings go first. The third is operator capital. The protocol is the only LP, so the operator funds every market's books and committed-order inventory.
## Mainnet path
Going from devnet to mainnet is gated on:
| Gate | Status |
| - | - |
| Security audits | Open. At least one external audit (Sec3, OtterSec or Neodyme) before launch. |
| Devnet stability under load | Open. About 4 weeks of devnet stress testing. |
| Bug bounty program | Open. Live before mainnet. |
| Multisig migration of upgrade authorities | Open. Squads (at least 3-of-5) for upgrade authority, plus a separate multisig for the CLP authority. |
| Enforced OI caps | Open. Each market's `Clp` stores an `oi_cap` (250,000 cUSDC on devnet), but nothing enforces it yet. Enforcement is planned. |
| Pyth (Pyth Pro) feed for every listed asset | Open. Checked per market before listing. |
| Gradual rollout | Plan agreed: one invite-only market first. |
Mainnet will launch with a single invite-only market, and expansion will follow what that market shows.
## Features behind feature flags
Keeper features that compile but are off by default:
| Feature | Flag | Notes |
| - | - | - |
| Idle-collateral yield | `COLLATERAL_YIELD_ENABLED=false` | The CLP can deploy idle vault balance into lending markets. Audit-gated. |
| Legacy Meteora layer | `LEGACY_METEORA_ENABLED=false` | DLMM position management, the arbitrage loop and that loop's MarginFi or Kamino flash-loan funding. The native bin books replaced it. |
| L/S direct pool arbitrage | `LS_POOLS` unset | Deprecated by the keeper's single-side mint and redeem. It stays disabled. |
## Features under design
* Governance activation, covering CNTM staking, listing votes and parameter votes. A program scaffold is deployed on devnet; the on-chain wiring is the remaining work.
* OI cap enforcement. Today `oi_cap` is checked only in an authority-only instruction that nothing calls.
* User-supplied LP shares. For now the protocol is the only LP.
* Multi-keeper coordination beyond the current primary and standby pair, for higher availability.
* Cross-market keeper coordination, with edge-weighted dispatching across operators.
* Vol-regime auto-tuning: per-asset calibration of the volatility thresholds.
* A public arb-bot SDK. This TypeScript and Rust library for external arb bots would package the keeper's spread-detection and execution logic.
## What's explicitly not planned
| Not planned | Why |
| - | - |
| Crypto markets | Use a real perp DEX. |
| Funding rate | The paired design has no funding payments. The short leg's [volatility decay](/concepts/nav) charges for the pair's convexity instead. |
| Liquidations | Paired positions are paid in full at mint. |
| Margin or leverage | Synthetic leverage through external borrowing is fine; native leverage is out of scope. |
## Tracking progress
* \#announcements on [Discord](https://discord.gg/U5hPCB8ufJ) for releases. It's an announcement channel, so you can follow it into your own server.
* [X @ContinuumMkts](https://x.com/ContinuumMkts) for major announcements.
## See also
What's live on devnet today.
How a market gets added.
# clp
Source: https://docs.continuum.markets/programs/clp
Continuous Liquidity Provider. The protocol's cUSDC vaults, the native bin books and committed orders.
The CLP program holds the protocol's market-making capital and runs both trading venues: committed orders, which fill at the next price push while a market is open, and native bin books, which fill instantly while its price is stale. It also mints and redeems its own L+S inventory through CPI into mint-redeem. A single `GlobalClp` PDA tracks funding and per-market allocations, and its cUSDC vault is the protocol treasury. Each market has a `Clp` PDA with its inventory, swap guards and committed-order spread, plus two `Pool` PDAs, one bin book per side.
```
Devnet ID: 8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES
```
Anyone can call `pool_swap`, `commit_swap` and `settle_swap`, and anyone can send `cancel_swap` once an order has expired. `clp_swap` is retired and always fails. The sections below give the signer every other instruction requires, usually `global_clp.authority`, the market's `clp.authority` or the pool's `pool.authority`.
## The two venues
| Venue | Instructions | When | Price | Fee |
| - | - | - | - | - |
| Committed order | `commit_swap`, then `settle_swap` | While the market's price is live | NAV ± `max(commit_spread_bps, 10)` bps at a price push after the commit | The spread |
| Bin book (instant) | `pool_swap` | Only while the market's price is stale | Walks the keeper's posted bins out from the book centre, with real slippage | None; you pay the bin spread |
A committed order fills at a price pushed after the commit, so the trader never trades against a quote seen before committing. Since 11 Oct 2026 committed orders are on for every live market and are the protocol's only venue while its price is live. `pool_swap` fills only while the market floats (its risk state isn't `Normal` and its last push is more than 300 s old) and fails with `PoolClosedWhileLive` otherwise. The keeper sizes the bin books to expected demand. The instant oracle swap, `clp_swap`, was retired the same day. See `SwapGuardsState` below.
## Accounts
### `GlobalClp` PDA
```
seeds = [b"global_clp"]
```
One per cluster (`BcGR8kuGCiMhwonyztXZHEZtjJXxXVRgjwbn58bUfXtw` on devnet). The global vault is this PDA's cUSDC associated token account (`5GSTKN4u5LfropcnQQoqFht48JMaF1SzZzBcgDNL5VKy` on devnet), and it is the protocol treasury. It held about 22.13M cUSDC after the keeper's returns of 11 Oct 2026 and 36.1M on 13 Oct 2026, as the devnet keeper burns it down. The 22.13M was: 5,534,820.82 from funding, plus idle CLP cash that the keeper returned to it, 12.99M on the first pass and 3.6M more once the instant swap was retired. See [Treasury](#treasury).
| Field | Type | Purpose |
| - | - | - |
| `total_deposits` | u64 | Principal added by `admin_fund`, net of `admin_withdraw`. Sets the `deposit_profit` tax tier |
| `last_admin_fund_at` | u64 | Unix time of the last `admin_fund`. Informational |
| `total_fees_accumulated` | u64 | Net `deposit_profit` plus recorded recall fees |
| `market_allocations` | `Vec` | cUSDC allocated per market (`market_id`, `allocated_usdc`); at most 10 entries |
| `protocol_buffer` | `ProtocolBuffer` | Yield accumulated, losses absorbed, balance, target (1,000 bps) and an above-target flag |
| `liquidity_seed_threshold` | u64 | Idle balance below which the keeper doesn't allocate (1,000 cUSDC on devnet) |
| `authority` | Pubkey | Global admin signer |
| `admin_wallet` | Pubkey | Owner of the account that receives the `deposit_profit` tax |
| `bump` | u8 | PDA bump |
`allocate_to_market` adds a `market_allocations` entry the first time it funds a market. The table is informational and holds 10 entries, fewer than the markets, so it never blocks a move. Once it is full, a market without an entry takes over one whose allocation has been fully returned. If there is none, the move still goes ahead, and the market's own `allocated_from_global` is the only record. Until the upgrade of 11 Oct 2026, funding an 11th market failed with `MaxDeploymentsExceeded`. As of 11 Oct 2026, five entries exist: QQQ, SPY, XAU, MSTR (closed on 12 Oct 2026, its allocation fully returned) and the delisted VXX market.
### `Clp` PDA
```
seeds = [b"clp", market]
```
One per market. Its operating vault (`clp_vault`) is this PDA's cUSDC associated token account. Fields in account order:
| Field | Type | Purpose |
| - | - | - |
| `market_id` | Pubkey | Bound Market PDA |
| `total_fees_collected` | u64 | Fees moved in by `collect_fees` |
| `oi_cap` / `current_oi` | u64 | Stored OI cap (250,000 cUSDC on every devnet market) and its mirror. Not enforced: only `update_oi` checks the cap, nothing calls it, and `current_oi` is 0 |
| `bump` | u8 | PDA bump |
| `inventory_budget` | `InventoryBudget` | Soft and hard bounds on the volatile share q, plus drawdown limits |
| `inventory_state` | `InventoryState` | Current q, drawdown, breach flags and peak value |
| `yield_strategy` | `YieldStrategyConfig` | Per-state deployment shares and reserves; off by default |
| `oracle_config` | Pubkey | The market's `OracleConfig` |
| `deployment_count` / `yield_deployments` / `total_deployed` | u8 / `Vec` (max 3) / u64 | Yield bookkeeping |
| `authority` | Pubkey | Signer for this market's CLP instructions |
| `vault_l_deployed` / `vault_s_deployed` | u64 | L and S outstanding from CLP minting |
| `total_vault_pairs_minted` | u64 | Lifetime cUSDC spent by `vault_mint_pairs` |
| `total_meteora_fees_earned` | u64 | Legacy Meteora LP fees |
| `meteora_long_pool`, `meteora_short_pool`, `meteora_long_position`, `meteora_short_position` | Pubkey | Legacy Meteora addresses |
| `allocated_from_global` | u64 | cUSDC allocated from the global vault |
| `swap_guards` | `SwapGuardsState` | Guards for committed orders (below) |
| `swap_window_start` / `swap_window_used` | i64 / u64 | Rolling notional-cap window for `pool_swap` |
| `swap_prev_price` | u64 | Price of the last committed fill (the deviation anchor) |
| `commit_spread_bps` | u16 | Committed-order half-spread posted by the keeper |
### `SwapGuardsState`
Stored in `Clp.swap_guards` and set with `configure_swap`. The all-zero default leaves committed orders off. `settle_swap` applies the staleness, confidence and deviation limits and the collateral floor.
| Field | Type | Purpose |
| - | - | - |
| `enabled` | bool | Required by `commit_swap` (`SwapDisabled`). `pool_swap` ignores it |
| `frozen` | bool | Blocks `commit_swap` and `settle_swap` (`SwapFrozen`). `pool_swap` and `cancel_swap` ignore it |
| `spread_bps` | u16 | Spread of the retired `clp_swap`. No live instruction reads it |
| `max_staleness_secs` | i64 | Maximum age of the Market price, in seconds (`OracleStale`) |
| `max_confidence_bps` | u16 | Maximum `Market.oracle_confidence`, in bps of price (`OracleConfidenceTooWide`) |
| `max_deviation_bps` | u16 | Maximum move from `swap_prev_price`, in bps (`OracleDeviationTooLarge`) |
| `window_cap_usd` | u64 | cUSDC notional allowed per window on `pool_swap`; 0 means no cap. Committed fills don't count |
| `window_secs` | i64 | Window length |
| `collateral_floor_bps` | u16 | Minimum market collateral ratio, at the settle price, for a committed fill |
Since 11 Oct 2026, all 13 live markets have guards enabled and not frozen, with the same limits: a 600 s staleness limit, a 5,000 bps confidence limit (off in practice), a 10,000 bps deviation limit (off; `update_risk_state_v2` already refuses a move of more than 25% per push) and a 10,000 bps (100%) collateral floor. Their spread and window cap were left as they were: QQQ, SPY and XAU keep a 30 bps spread and a cap of 1,000,000 cUSDC per hour, and the other markets have no cap.
### `Pool` PDA
```
seeds = [b"pool", market, [side]] // side: 0 = long, 1 = short
```
Two per market, so 26 for the 13 live markets. Each is a native bin book for one synth leg against cUSDC. The whole book lives in the account (up to 16 bins a side, `POOL_MAX_BINS`), so bins cost no extra rent. The pool PDA's two associated token accounts hold the reserves: synth for the asks and cUSDC for the bids.
| Field | Type | Purpose |
| - | - | - |
| `authority` | Pubkey | Signer for `set_pool_shape`, `configure_pool_geom` and `realloc_pool` |
| `market` / `side` | Pubkey / u8 | Binding |
| `synth_mint` / `usdc_mint` | Pubkey | Pair mints |
| `base_price_1e6` | u64 | Book centre (1e6). The keeper sets it from NAV, shifted for its inventory |
| `step_bps` | u32 | Core bin step (`r = 1 + step_bps/1e4`) |
| `num_bins` | u8 | Bins per side (at most 16) |
| `asks` | `[u64; 16]` | Synth reserve per ask bin, above the centre |
| `bids` | `[u64; 16]` | cUSDC reserve per bid bin, below the centre |
| `enabled` / `frozen` | bool | `pool_swap` needs `enabled` and not `frozen`. The first `set_pool_shape` sets `enabled`; no instruction clears it or sets `frozen` |
| `shape_updated_at` | i64 | Unix time of the last reposition |
| `bump` | u8 | PDA bump |
| `wing_step_bps` | u32 | Wing step for the 2-zone geometry (0 = uniform ladder) |
| `core_bins` | u8 | Number of fine core bins before the wings start |
There is no pool kill switch. Once a pool has a shape, nothing can disable it, though it fills only while its market floats (see `pool_swap` below).
**Bin geometry.** Ask bin `i` sits at `base · r^(i+1)` and bid bin `i` at `base · r^-(i+1)`. The centre itself is an empty mid, so the smallest round trip costs about two steps. With 2-zone geometry, the first `core_bins` steps use `step_bps` for fine pricing near the centre. The rest use `wing_step_bps`, so the same 16 bins can span a large move.
### `Order` PDA (committed orders)
```
order: seeds = [b"order", user, market, nonce_le_u64]
escrow: seeds = [b"escrow", order] // token account owned by the order PDA
```
Each order records what the user escrowed and when, and the fill uses a price pushed after `created_at`. The account is 117 bytes, with every field at a fixed offset.
| Field | Offset | Type | Purpose |
| - | - | - | - |
| `user` | 8 | Pubkey | Order owner |
| `market` | 40 | Pubkey | Bound market |
| `side` | 72 | u8 | 0 = long, 1 = short |
| `direction` | 73 | u8 | 0 = buy synth, 1 = sell synth |
| `status` | 74 | u8 | 0 = pending, the only stored state |
| `bump` / `escrow_bump` | 75 / 76 | u8 | PDA bumps |
| `amount_in` | 77 | u64 | Escrowed base units (cUSDC for buys, synth for sells) |
| `min_out` | 85 | u64 | Slippage floor; a miss at settle refunds instead of reverting |
| `created_at` | 93 | i64 | Commit time; settle needs `twap_updated_at > created_at` |
| `expires_at` | 101 | i64 | `created_at + 180`; settle owns ≤, cancel owns > |
| `nonce` | 109 | u64 | Client-chosen seed |
A client can find a wallet's orders with `getProgramAccounts` memcmp filters on `user` (offset 8) or `market` (offset 40). The keeper finds every pending order in one call, filtered on the 117-byte size, the `Order` discriminator and `status = 0` at offset 74. Every terminal path (fill, refund, cancel) empties the escrow, closes both the order and the escrow, and returns all rent to the user, so a second settle attempt fails at account load. Since 13 Oct 2026, tokens anyone sends to an escrow after the commit go back to the owner with the rest, so they can't stop it closing.
### `TreasuryPolicy` PDA
```
seeds = [b"treasury_policy"]
```
One per cluster (`58JeHAcMAAyEZWusVqmvoFNVqh37cTkmfbbP6bMHExWR` on devnet, 210 bytes). It holds the limits on `admin_withdraw` and `treasury_lend` and is created by `init_treasury_policy`. It replaced the `AdminWithdrawWindow` account, which the upgrade of 11 Oct 2026 removed.
| Field | Type | Purpose |
| - | - | - |
| `guardian` | Pubkey | Freeze-only key. It can freeze and unfreeze withdrawals, cancel a pending change and hand its role to a new key. It can't move funds |
| `destination` | Pubkey | The only token account `admin_withdraw` pays |
| `rate_per_sec` | u64 | Allowance refill rate, in cUSDC base units per second |
| `burst` | u64 | Most the allowance can hold, so the largest single withdrawal |
| `delay_secs` | i64 | Wait before a loosening can apply; at most 30 days |
| `allowance` | u64 | Withdrawable amount as of `last_accrual_at` |
| `last_accrual_at` | i64 | When `allowance` was last refilled |
| `frozen` | bool | Blocks `admin_withdraw` and `treasury_lend`. `treasury_recall` still works |
| `pending_rate_per_sec`, `pending_burst`, `pending_delay_secs`, `pending_destination`, `pending_guardian` | u64, u64, i64, Pubkey, Pubkey | A proposed loosening that is waiting out its delay |
| `pending_at` | i64 | When the pending change can apply; 0 if none |
| `bump` | u8 | PDA bump |
The program refills `allowance` only when an instruction touches the policy. To read what can be withdrawn now, compute `min(burst, allowance + rate_per_sec × (now − last_accrual_at))`.
The live policy as read on 13 Oct 2026:
* Rate: 256.78 cUSDC per second, about 924,403 cUSDC an hour. The keeper keeps the rate in proportion to the balance so that the whole treasury can leave in about 24 h; see [Keeper → Treasury withdrawal rate](/concepts/keeper#treasury-withdrawal-rate-900s). The earlier rate was 64.06 cUSDC per second.
* Burst: 924,402.61 cUSDC, one hour of refill.
* Delay: 24 h.
* Pending: a rate and a burst of 1,000,000,000 cUSDC (in effect no limit), due at 04:29 UTC on 14 Oct 2026. It stays pending until then; read `TreasuryPolicy` for the values in force.
### `TreasuryYield` PDA
```
seeds = [b"treasury_yield"]
```
One per cluster (`6YrCADSTyTJPNrVhrZz5S2wTzBQSPM5fGUrGSpLTBJgY` on devnet, 73 bytes), created by `init_treasury_yield` on 12 Oct 2026. It records the treasury cUSDC that is out at lending venues.
| Field | Type | Purpose |
| - | - | - |
| `by_venue` | \[u64; 5] | Principal out at each venue, by `YieldProtocol` index: 0 Kamino, 1 MarginFi, 2 Other, 3 Solend, 4 JupiterLend |
| `principal` | u64 | Sum of `by_venue` |
| `interest` | u64 | Interest returned to the treasury, all time |
| `updated_at` | i64 | Time of the last lend or recall |
| `bump` | u8 | PDA bump |
Devnet has no lending venues, so its ledger stays at zero. A 1 cUSDC lend and recall on 12 Oct 2026 tested the instructions and left it there.
### `RevenueAttribution` PDA
```
seeds = [b"revenue_attribution"]
```
One per cluster (`A8mK2pnagabEcnkSiWieEZyQjjNWFYm1Z91dUVMXe6a4` on devnet). It splits protocol revenue into keeper flow and external flow. `deposit_profit` and `vault_record_liquidity_recall` take it as their last account, and `pool_swap` takes it just before the market. Amounts are cUSDC base units.
| Field | Type | Purpose |
| - | - | - |
| `fees_external` | u64 | Spread captured on `pool_swap` fills by signers that aren't keeper keys |
| `fees_keeper` | u64 | Spread captured on keeper-signed `pool_swap` fills, plus net `deposit_profit` and recorded recall fees |
| `volume_external` / `volume_keeper` | u64 | cUSDC notional of `pool_swap` fills, split the same way |
| `deposit_by_source` | `[u64; 4]` | Net `deposit_profit` by `source` tag |
| `started_at` | i64 | When counting began |
| `keeper_authorities` | `[Pubkey; 4]` | Signers counted as keeper flow, in addition to `clp.authority`. Default pubkeys are empty slots |
| `bump` | u8 | PDA bump |
| `reserved` | `[u8; 64]` | Spare space for future counters |
A fill's spread capture is its distance from the book centre: cUSDC paid minus synth × `base_price_1e6` on a buy, and synth × `base_price_1e6` minus cUSDC received on a sell.
## Instructions
### Swaps
#### `pool_swap(params: PoolSwapParams)` (anyone)
`PoolSwapParams { direction: u8, amount_in: u64, min_out: u64 }`. Direction 0 buys synth with cUSDC, and 1 sells synth for cUSDC. A buy walks the ask bins up from `base·r`; a sell walks the bid bins down from `base/r`. The handler decrements reserves in place, so the stored book always matches the vault balances, and partial bin fills round in the pool's favour. If the book runs out first, the swap fills what it can and takes only what it used. There is no swap fee.
The pool must be enabled and not frozen (`SwapDisabled`), and `base_price_1e6` must be nonzero (`OracleStale`). The market must be floating: its `risk_state` isn't `Normal` and its `twap_updated_at` is more than 300 s old, the age at which mint and redeem freeze (`PoolClosedWhileLive` otherwise). Something must fill (`InsufficientFunds`), the output must reach `min_out` (`SlippageExceeded`), and the fill must fit the `Clp` window cap (`WindowCapExceeded`). `pool_swap` doesn't read `swap_guards.enabled`, `frozen` or the oracle guards: it trades on the book as posted. Off-hours the keeper keeps a demand-sized free-float book, with a guard that can only pull bins on the stale side. Until 11 Oct 2026 the books traded at any hour.
Accounts, in order:
| # | Account | Notes |
| - | - | - |
| 0 | `user` | Signer |
| 1 | `user_usdc` | User's cUSDC token account |
| 2 | `user_synth` | User's token account for the pool's synth mint |
| 3 | `pool` | `Pool` PDA |
| 4 | `clp` | PDA `[b"clp", pool.market]`; holds the window cap |
| 5 | `synth_vault` | The pool's synth ATA |
| 6 | `usdc_vault` | The pool's cUSDC ATA |
| 7 | `token_program` | SPL Token |
| 8 | `revenue_attribution` | PDA `[b"revenue_attribution"]` |
| 9 | `market` | The pool's mint-redeem `Market` PDA, read for its risk state and price age; must be last. Added on 11 Oct 2026 |
The pool PDA signs the outbound transfer. Each fill updates `RevenueAttribution` and emits `SwapExecuted` with route 1.
#### `clp_swap(side: u8, direction: u8, amount_in: u64, min_out: u64)` (retired)
Retired on 11 Oct 2026. It was the instant swap against CLP inventory at NAV ± `swap_guards.spread_bps`, with NAV from `Market.user_twap_price`. The instruction keeps its accounts (`user`, `user_usdc`, `user_synth`, `clp`, `clp_vault`, `clp_synth`, `market`, `token_program`) and arguments, so existing clients still decode it, but every call fails with `SwapDisabled`. Use a committed order instead. Its fills emitted `SwapExecuted` with route 0.
#### `commit_swap(side: u8, direction: u8, amount_in: u64, min_out: u64, nonce: u64)` (anyone)
Opens a committed order. It escrows `amount_in` (cUSDC for buys, synth for sells) and sets `expires_at = now + 180`. The user pays rent for the order and escrow accounts and gets it back on every terminal path. The payout account (`user_out`) must already exist, so settlement can't stall on it. It checks `market.is_active` (`MarketInactive`), `swap_guards.enabled` (`SwapDisabled`) and `frozen` (`SwapFrozen`). Accounts: `user`, `market`, `clp`, `order`, `in_mint`, `escrow`, `user_in`, `user_out`, `clp_vault`, `token_program`, `system_program`. Emits `OrderCommitted`.
#### `settle_swap()` (anyone)
Fills a pending order at NAV ± `max(clp.commit_spread_bps, 10)` bps, with NAV from `Market.user_twap_price` at settle time. The keeper cranks it in practice. It requires:
* `now ≤ expires_at`, else `OrderExpired`.
* `market.twap_updated_at > order.created_at`, else `NoFreshOraclePush`. Any push after the commit qualifies, not only the first.
* The oracle guards: not frozen, a price no older than `max_staleness_secs`, and confidence and deviation within limits.
It doesn't recheck `swap_guards.enabled`. Since 13 Oct 2026 a paused market (`market.is_active` false) refunds the order before any price check, so a pause stops fills at once.
| Condition | Result |
| - | - |
| Market paused (`market.is_active` false) | Full refund. The accounts close and `OrderRefunded` is emitted |
| An oracle guard fails | Revert (`SwapFrozen`, `OracleStale`, `OracleConfidenceTooWide` or `OracleDeviationTooLarge`). The order stays pending and can fill at a later push |
| Quote at or above `min_out` | Fill: `amount_in` to the CLP, payout to the user, anything else in the escrow back to the user, accounts close. Emits `OrderSettled` and `SwapExecuted` (route 2) |
| Quote below `min_out` | Full refund instead of a revert, so a runaway price can't leave the order retrying until expiry. The accounts close and `OrderRefunded` is emitted |
| Market below `collateral_floor_bps` at the settle price | Revert (`CollateralFloorBreach`). The order stays pending and can fill at a later push once the market is topped up |
| CLP inventory short | Revert. The order stays pending while the keeper adds inventory and retries |
The collateral floor is checked only on the fill path. A fill updates `swap_prev_price` but doesn't count toward the window cap. Accounts: `cranker` (signer), `user` (receives the rent), `order`, `escrow`, `clp`, `clp_vault`, `clp_synth`, `market`, `user_out`, `user_in`, `token_program`.
The keeper posts `commit_spread_bps` with `set_commit_spread`, as `clamp(half-spread, 10, 500)` from its volatility model, and re-posts it whenever it moves by 2 bps or more. As of 10 Oct 2026 it is 18 bps on QQQ, 15 on SPY and 18 on XAU.
#### `cancel_swap()` (anyone, after expiry)
Refunds an expired order once `now > expires_at` (else `OrderNotExpired`). Settle owns `now ≤ expires_at`, so there is no gap or overlap, and before expiry nobody can cancel. Since 11 Oct 2026 anyone may send it: `user` must be the order's owner but doesn't sign, and the escrow can go only to `user_in`, a token account the owner holds for the escrowed mint (`SwapAccountMismatch`). Nothing else is checked (no frozen, enabled, oracle or market-active gate), so escrowed funds can always be recovered after the 180 s TTL. Returns the escrow's whole balance and all rent to the owner and emits `OrderCancelled`. The keeper sends it for every expired order about 5 s after expiry. Accounts: `user`, `order`, `escrow`, `user_in`, `token_program`.
### Bin book management
#### `init_pool(side: u8, step_bps: u32, num_bins: u8)` (`global_clp.authority`)
Creates the `Pool` PDA, and the signer becomes `pool.authority`. `step_bps` must be in (0, 1000] and `num_bins` in \[1, 16]. The pool starts disabled with an empty book.
#### `set_pool_shape(base_price_1e6: u64, asks: Vec, bids: Vec)` (`pool.authority`)
The reposition. It sets the centre and every bin's reserve in one call, and sets `enabled = true`. Each vector must have exactly `num_bins` entries and sum to no more than the matching vault balance. The keeper centres the book on NAV shifted for its inventory, with a skewed shape.
#### `configure_pool_geom(wing_step_bps: u32, core_bins: u8)` (`pool.authority`)
Sets the 2-zone geometry. A nonzero `wing_step_bps` must be at least `step_bps`, and `core_bins` at most `num_bins`. `wing_step_bps = 0` restores the uniform ladder.
#### `realloc_pool()` (`pool.authority`)
Grows a pool created before the 2-zone fields existed. The new fields start at 0, which reads as the uniform ladder.
#### `pool_sweep_usdc(amount: u64)` / `pool_sweep_synth(amount: u64)` (`clp.authority`)
Move cUSDC out of a pool's bid vault, or synth out of its ask vault, back to the CLP. One-way buying turns ask inventory into cUSDC that collects in the pool; the sweep recycles it through `vault_mint_pairs` into fresh L+S. The synth sweep trims ask depth far beyond the bid side, and the trimmed synth supplies committed orders instead.
#### `clp_fund_pool_synth(amount: u64)` (`clp.authority`)
Moves synth from the CLP's L or S account into a pool's ask vault, the refill step after `vault_mint_pairs`. No counters change; the next `set_pool_shape` places the tokens in bins.
#### `clp_fund_pool_usdc(amount: u64)` (`clp.authority`)
Moves cUSDC from the market's CLP vault into a pool's bid vault: the bid-side refill. Sell flow spends a book's bid cUSDC. `pool_sweep_synth` takes back the synth the book bought, and this puts the cUSDC back. The source must be the CLP's own vault, the destination must be the pool's cUSDC vault, and the pool must belong to the CLP's market (`SwapAccountMismatch`). No counters change; the next `set_pool_shape` places the cUSDC in bins. Added 11 Oct 2026.
### Inventory minting (`clp.authority`)
The CLP mints and redeems through CPI into mint-redeem, signing as the `Clp` PDA.
* `vault_mint_pairs(usdc_amount: u64)` calls `mint_paired` with cUSDC from the market's CLP vault. The L+S land in the PDA's token accounts, and `vault_l_deployed`/`vault_s_deployed` grow by the amounts actually minted.
* `vault_redeem_pairs(l_amount: u64, s_amount: u64)` burns L+S held by the PDA and returns cUSDC to its vault.
* `vault_redeem_single(is_long: bool, token_amount: u64)` redeems one leg at NAV through mint-redeem's `redeem_single_for_vault`, which refuses to leave the market below 100% collateral.
* `clp_burn_excess(is_long: bool, amount: u64)` burns leftover L or S without any payout, through `permissionless_burn`.
The three vault instructions take 15 accounts. The upgrade of 11 Oct 2026 dropped the trailing `collateral_authority` account, and callers that still send 16 keep working because the extra account is ignored. Each one checks where tokens can go:
* `market` must be this CLP's own market (`MarketMismatch`).
* `clp_vault` must be owned by the `Clp` PDA and hold the market's collateral mint (`InvalidVault`).
* `clp_long_account` and `clp_short_account` must be owned by the `Clp` PDA and hold the market's L and S mints (`InvalidVault`).
### Capital flow
#### `initialize_global_clp(liquidity_seed_threshold: u64)`
One-time setup of `GlobalClp`. The signer becomes `authority`, the `admin_wallet` account is recorded, and the buffer target is set to 1,000 bps. It doesn't create the global vault token account.
#### `admin_fund(amount: u64)` (`global_clp.authority`)
Moves cUSDC from the admin into the global vault, adds it to `total_deposits` and stamps `last_admin_fund_at`.
#### `allocate_to_market(amount: u64)` / `return_to_global(amount: u64)` (`global_clp.authority`)
Move cUSDC between the global vault and a market's CLP vault, updating `allocated_from_global` and `market_allocations`. They move only between those two accounts. The global vault must be owned by the `GlobalClp` PDA, and the CLP vault must be owned by this market's `Clp` PDA and hold the same mint (`InvalidVault`).
#### `deposit_profit(params: DepositProfitParams)` (`global_clp.authority` or a registered keeper)
`DepositProfitParams { amount: u64, source: u8 }`. Since 13 Oct 2026 the `keeper` signer must be `global_clp.authority` or a wallet in `RevenueAttribution.keeper_authorities` (else `Unauthorized`). Moves `amount` of cUSDC out of `keeper_source`, a token account the `keeper` signer controls. A tiered tax goes to `admin_token_account`, which must be owned by `admin_wallet`. The rest goes to the global vault and `total_fees_accumulated`. The tier follows `total_deposits`: 15% below \$5M, 10% up to \$25M and 5% above. The net amount counts as keeper flow in `RevenueAttribution`, under `deposit_by_source[source]` (0 arb, 1 recycled LP fees, 2 yield harvest, 3 other; unknown tags count as 3). Accounts: `global_clp`, `keeper`, `keeper_source`, `global_vault`, `admin_token_account`, `token_program`, `revenue_attribution` (last).
#### `collect_fees(amount: u64)` (`clp.authority`)
Moves fees into the market's CLP vault and adds them to `total_fees_collected`.
#### `vault_record_liquidity_recall(params: VaultRecordLiquidityRecallParams)` (`global_clp.authority`)
Bookkeeping for a legacy Meteora recall; it moves no tokens. `VaultRecordLiquidityRecallParams { l_amount, s_amount, fees_earned }`. It subtracts the amounts from `vault_l_deployed` and `vault_s_deployed` (`InsufficientDeployment` if they're larger). It adds `fees_earned` to `total_meteora_fees_earned`, `total_fees_accumulated`, the protocol buffer and `RevenueAttribution.fees_keeper`. `revenue_attribution` is the last account.
### Treasury
The global vault is the protocol treasury. `admin_withdraw` pays cUSDC out of the protocol, within the limits in `TreasuryPolicy`. `treasury_lend` sends idle cUSDC to a lending venue within the same limits, and `treasury_recall` brings it back. `backstop_market` moves cUSDC into a market's collateral and has no limit, because the funds stay inside the protocol. The policy, `backstop_market` and the instructions that manage the policy were added on 11 Oct 2026, the three lending instructions on 12 Oct 2026, and `advance_to_market` on 13 Oct 2026.
#### `admin_withdraw(amount: u64)` (`global_clp.authority`)
Pays idle cUSDC from the global vault to the policy's `destination`. It works as a token bucket: the allowance refills at `rate_per_sec` up to `burst`, and each withdrawal spends it. A withdrawal must fit in the allowance (`TreasuryAllowanceExceeded`) and in the vault balance (`InsufficientLiquidFunds`), and must pay the policy destination (`InvalidTreasuryDestination`). It is refused while the policy is frozen (`TreasuryFrozen`). Capital allocated to markets must first come back through `return_to_global`.
Accounts: `global_clp`, `authority`, `treasury_policy`, `global_vault` (owned by the `GlobalClp` PDA, else `InvalidVault`), `destination`, `token_program`. Emits `TreasuryWithdrawn`.
On 11 Oct 2026 the bucket replaced the old 24 h cap and the 1 h hold after `admin_fund`, so `AdminWithdrawCapExceeded` (6033) is no longer returned.
#### `init_treasury_yield()` (`global_clp.authority`)
Creates `TreasuryYield` once, paid for by the signer. Accounts: `global_clp`, `authority`, `treasury_yield`, `system_program`.
#### `treasury_lend(params: TreasuryLendParams)` (`global_clp.authority`)
`TreasuryLendParams { amount, venue }`, where `venue` is the `YieldProtocol` index, 0 to 4 (else `InvalidAmount`). Moves `amount` cUSDC from the global vault to `lend_account`, the authority's own cUSDC account, which deposits it at the venue in the same transaction, as `deploy_collateral_to_yield` does for market collateral. It spends the `TreasuryPolicy` allowance exactly as `admin_withdraw` does, so lending can't move money out of the treasury faster than the policy allows. A lend must fit in the allowance (`TreasuryAllowanceExceeded`) and in the vault balance (`InsufficientLiquidFunds`), and is refused while the treasury is frozen (`TreasuryFrozen`). It adds `amount` to `by_venue[venue]` and `principal`.
Accounts: `global_clp`, `authority`, `treasury_policy`, `global_vault` (owned by the `GlobalClp` PDA, else `InvalidVault`), `lend_account` (owned by the authority, else `Unauthorized`, and holding the vault's mint, else `InvalidVault`), `treasury_yield`, `token_program`. It emits no event; the program log carries the amount, the venue and the principal out.
#### `treasury_recall(params: TreasuryRecallParams)` (`global_clp.authority`)
`TreasuryRecallParams { amount, principal, venue }`. Moves `amount` cUSDC from `lend_account` back to the global vault. `principal` of it repays what was lent at `venue` and the rest is booked as `interest`. `principal` can't exceed `amount` (`InvalidAmount`) or what is out at that venue (`InsufficientDeployment`). The returned principal restores the allowance its lend spent, up to `burst`. A recall works while the treasury is frozen, so money coming home is never blocked. Same accounts as `treasury_lend`.
Nothing calls these on a schedule yet. The keeper's yield allocator lends market collateral only, and devnet has no venues.
#### `advance_to_market(amount: u64)` (`global_clp.authority` and `market.authority`)
Lends `amount` cUSDC from the global vault into a market's collateral vault, so that a queued redemption can be paid. The signer must be both `GlobalClp.authority` and the market's authority. It is refused while the treasury is frozen (`TreasuryFrozen`), and it is not charged to the withdrawal allowance, because the cash stays protocol-owned until a settle pays a redeemer. It calls mint-redeem's `receive_advance` by CPI with the `GlobalClp` PDA as funder, so the market's `max_advance_bps` caps it (`AdvanceCapExceeded`) and the cash isn't counted in `total_collateral`. mint-redeem's `repay_advance` returns it to the global vault when the market's own recall lands.
Accounts: `global_clp`, `authority`, `treasury_policy`, `market`, `market_advance`, `collateral_vault`, `global_vault`, `mint_redeem_program`, `token_program`. Emits `MarketAdvanced`.
#### `init_treasury_policy(params: TreasuryPolicyParams)` (`global_clp.authority`)
`TreasuryPolicyParams { rate_per_sec, burst, delay_secs, guardian, destination }`. Creates `TreasuryPolicy` once, paid for by the signer. The allowance starts at 0. Rate and burst must be above 0, the delay must be 0 to 30 days, and a guardian must be set (`InvalidTreasuryParams`). The `destination` account must be the one named in the params and hold the global vault's mint (`InvalidTreasuryDestination`).
#### `set_treasury_limits(params: TreasuryLimitsParams)` (`global_clp.authority`)
`TreasuryLimitsParams { rate_per_sec, burst, delay_secs, destination, guardian }`, checked like `init_treasury_policy`.
* When every field is equal or tighter, the change applies at once. Tighter means a lower rate, a lower burst, a longer delay, the same destination and the same guardian. Emits `TreasuryLimitsApplied`.
* Any loosening becomes pending until now plus the current `delay_secs`. A new proposal replaces an older pending one. Emits `TreasuryLimitsProposed`.
`TreasuryLimitsProposed` carries `pending_at` and a `loosened` bitmask: 1 rate up, 2 burst up, 4 delay shorter, 8 new destination, 16 new guardian.
#### `apply_treasury_limits()` (anyone)
Applies the pending change once `pending_at` has passed. It fails with `NoPendingTreasuryChange` when nothing is pending and `TreasuryChangeNotReady` before `pending_at`. It also works while frozen.
#### `cancel_treasury_limits()` (`global_clp.authority` or the guardian)
Drops the pending change. Emits `TreasuryLimitsCancelled`.
#### `freeze_treasury()` (the guardian or `global_clp.authority`) and `unfreeze_treasury()` (the guardian only)
Set and clear `frozen`. Both emit `TreasuryFreezeChanged`.
#### `set_treasury_guardian(new_guardian: Pubkey)` (the guardian only)
Hands the guardian role to a new key at once. If a pending change kept the old guardian, it now names the new one, so applying it later can't restore the old key. Emits `TreasuryGuardianSet`.
#### `backstop_market(amount: u64)` (`global_clp.authority`)
Moves treasury cUSDC straight into a market's collateral vault and credits `total_collateral`. It calls mint-redeem's `fund_collateral_from` by CPI, with the `GlobalClp` PDA as funder. The signer is the global authority, which must also be the market's authority. It is not rate limited, because the funds stay inside the protocol, and it works while frozen.
The keeper's liquidity reallocator calls it: at once for a market below 100% collateral at its stored price, and after 5 minutes for one short of its posted target by more than one confidence band. It fills to the posted target, at most 130% by default, and never takes a fill back. See [Solvency → Treasury backstop](/concepts/solvency#treasury-backstop).
Accounts: `global_clp`, `authority`, `market`, `collateral_vault` (the market's), `global_vault`, `mint_redeem_program`, `token_program`. Emits `MarketBackstopped`, and mint-redeem emits `CollateralFunded`.
### Configuration
* `initialize_clp(oi_cap: u64, oracle_config: Pubkey)` creates a market's `Clp` with the default inventory budget, yield off and zeroed swap guards. The signer must be the Market's `authority` and becomes `clp.authority`.
* `configure_swap(params: ConfigureSwapParams)` (`clp.authority`) sets `SwapGuardsState`. `ConfigureSwapParams { enabled, frozen, spread_bps, max_staleness_secs, max_confidence_bps, max_deviation_bps, window_cap_usd, window_secs, collateral_floor_bps }` holds the separate arguments the instruction took until 11 Oct 2026, with the same fields in the same order, so the bytes on the wire are unchanged. Every field is optional, and `None` leaves it unchanged. `frozen = true` stops `commit_swap` and `settle_swap`, but not `pool_swap` or `cancel_swap`.
* `set_commit_spread(commit_spread_bps: u16)` (`clp.authority`) sets the committed-order half-spread, at most 1,000 bps (else `InvalidAmount`). `settle_swap` floors it at 10 bps.
* `update_oi_cap(new_cap: u64)` (`clp.authority`) sets the stored OI cap, which nothing enforces.
* `configure_inventory_budget(params: ConfigureInventoryBudgetParams)` and `configure_yield_strategy(params: ConfigureYieldStrategyParams)` (`clp.authority`) set the inventory bounds and the yield shares. Each struct holds the instruction's former arguments as optional fields, in the same order, so the bytes on the wire are unchanged: `ConfigureInventoryBudgetParams { soft_lower_bps, soft_upper_bps, hard_lower_bps, hard_upper_bps, max_drawdown_bps, target_drawdown_bps, enforce_hard_bounds }` and `ConfigureYieldStrategyParams { normal_deployment_bps, proxy_deployment_bps, stress_deployment_bps, recovery_deployment_bps, min_liquid_reserve, min_liquid_reserve_bps, oi_liquidity_buffer_bps, enabled }`.
* `configure_liquidity_seeder(threshold: Option)` and `realloc_clp()` (`global_clp.authority`) set the seed threshold and grow an older `Clp` to the current layout.
* `init_revenue_attribution(params: RevenueAttributionParams)` and `configure_revenue_attribution(params: RevenueAttributionParams)` (`global_clp.authority`) create the `RevenueAttribution` singleton and replace its `keeper_authorities`. `RevenueAttributionParams` is `{ keeper_authorities: [Pubkey; 4] }`. Configuring leaves the counters and `started_at` alone.
### Inventory checks
* `update_inventory(volatile_value: u64, stable_value: u64)` (`clp.authority`) recomputes q, drawdown and the breach flags.
* `update_oi(new_oi: u64)` (`clp.authority`) sets `current_oi` and fails with `OICapExceeded` above `oi_cap`. Nothing calls it today.
* `check_inventory_restrictions()` returns `hard_breach && enforce_hard_bounds`, and `get_yield_status()` returns `(total_deployed, max_deployable, can_deploy_more)`. Both are read-only and CPI-callable. `max_deployable` is still the old limit sized by the CLP's own capital, which `record_deployment` stopped applying on 12 Oct 2026.
### Yield (off)
`record_deployment` and `record_withdrawal` (`clp.authority`) and `record_harvest` (`global_clp.authority`) track deployments to lending venues. Since 12 Oct 2026 `record_deployment` refuses only when yield is off (`YieldStrategyDisabled`) or the market isn't Active (`ExceedsMaxDeployment`). It used to also cap a market's lending at a share of its CLP's own capital, a few hundred dollars per market after the stock cuts. A market's lending is now bounded by its `yield_ceiling_bps` in mint-redeem and by the keeper's venue caps. `add_yield_to_buffer` and `absorb_loss` (`global_clp.authority`) maintain the global protocol buffer. Yield is off by default (`yield_strategy.enabled = false`).
### Legacy Meteora instructions
`configure_meteora_pools`, `clear_meteora_positions`, `clp_set_meteora_position`, `clp_init_bin_array`, `clp_open_meteora_position`, `clp_topup_meteora_position`, `clp_remove_meteora_liquidity`, `clp_claim_lp_fees` and `clp_close_meteora_position` managed the v1 Meteora positions. They are legacy: every market now trades on native bin books. `InvalidPool`, `PoolNotConfigured` and most `InvalidPosition` errors come only from these instructions. `clp_remove_meteora_liquidity` and `clp_claim_lp_fees` pay only token accounts owned by the `Clp` PDA (`InvalidVault`).
## Events
`commit_swap` emits `OrderCommitted`, `settle_swap` emits `OrderSettled` or `OrderRefunded`, and `cancel_swap` emits `OrderCancelled`. The treasury instructions emit `TreasuryPolicyInitialized`, `TreasuryWithdrawn`, `TreasuryLimitsProposed`, `TreasuryLimitsApplied`, `TreasuryLimitsCancelled`, `TreasuryFreezeChanged`, `TreasuryGuardianSet`, `MarketBackstopped` and `MarketAdvanced`. Their fields are in the IDL. Every fill on any route also emits `SwapExecuted`:
| Field | Type | Meaning |
| - | - | - |
| `user` | Pubkey | Swap signer, or the order's owner for a committed fill |
| `market` | Pubkey | Market PDA |
| `collateral_mint` / `synth_mint` | Pubkey | cUSDC and the traded leg |
| `order` | Pubkey | Order PDA for a committed fill; default pubkey otherwise |
| `route` | u8 | 0 = `clp_swap` (retired), 1 = `pool_swap`, 2 = `settle_swap` fill |
| `side` / `direction` | u8 | 0 = long, 1 = short / 0 = buy synth, 1 = sell synth |
| `is_keeper` | bool | `user` is a keeper key: `clp.authority` or a `keeper_authorities` entry on route 1, `clp.authority` or `Market.keeper_authority` on routes 0 and 2 |
| `amount_in` / `amount_out` | u64 | Base units in and out |
| `collateral_amount` / `synth_amount` | u64 | The same two amounts, by token |
| `reference_price` | u64 | NAV (1e6) on routes 0 and 2; the book centre on route 1 |
| `spread_capture` | u64 | Execution spread against `reference_price`, in cUSDC base units. It isn't realized revenue |
## Errors
| Code | Name | Cause |
| - | - | - |
| 6000 | `Unauthorized` | Signer isn't the stored authority, or a token account has the wrong owner |
| 6001 | `WithdrawalLocked` | Unused (legacy LP lockup) |
| 6002 | `InvalidAmount` | Zero or out-of-range argument |
| 6003 | `SwapDisabled` | Every `clp_swap` (retired on 11 Oct 2026), `swap_guards.enabled` is false (`commit_swap`), or the pool isn't enabled (`pool_swap`) |
| 6004 | `SwapFrozen` | `swap_guards.frozen` is true (`commit_swap`, `settle_swap`) |
| 6005 | `InvalidSide` | Side or direction isn't 0 or 1 |
| 6006 | `SwapAccountMismatch` | A token account's mint or owner doesn't match the market or pool, or the order isn't pending |
| 6007 | `MarketInactive` | `market.is_active` is false (`commit_swap`) |
| 6008 | `SlippageExceeded` | Output below `min_out` (`pool_swap`; `settle_swap` refunds instead) |
| 6009 | `OrderExpired` | Settle after `expires_at`; use `cancel_swap` to refund |
| 6010 | `OrderNotExpired` | Cancel at or before `expires_at` |
| 6011 | `NoFreshOraclePush` | No price push since the commit yet |
| 6012 | `OracleStale` | Market price is 0 or older than `max_staleness_secs`, or the pool's `base_price_1e6` is 0 |
| 6013 | `OracleConfidenceTooWide` | `Market.oracle_confidence` above `max_confidence_bps` |
| 6014 | `OracleDeviationTooLarge` | Price moved more than `max_deviation_bps` from `swap_prev_price` |
| 6015 | `WindowCapExceeded` | Rolling notional cap hit (`pool_swap`) |
| 6016 | `CollateralFloorBreach` | Market collateral ratio below `collateral_floor_bps` at the settle price; the order stays pending |
| 6017 | `InsufficientShares` | Unused (legacy LP shares) |
| 6018 | `InsufficientFunds` | A vault, book or inventory can't cover the request |
| 6019 | `InsufficientLiquidFunds` | `admin_withdraw`, `treasury_lend` or `backstop_market` is larger than the global vault balance |
| 6020 | `OICapExceeded` | `update_oi` above `oi_cap` |
| 6021 | `MathOverflow` | Arithmetic overflow |
| 6022 | `InsufficientBuffer` | `absorb_loss` is larger than the buffer balance |
| 6023 | `HardBoundBreach` | Unused |
| 6024 | `YieldStrategyDisabled` | `record_deployment` while yield is off |
| 6025 | `ExceedsMaxDeployment` | `record_deployment` while the market isn't Active |
| 6026 | `MaxDeploymentsExceeded` | A fourth yield deployment. `allocate_to_market` no longer raises it as of 11 Oct 2026 |
| 6027 | `DeploymentNotFound` | Unknown yield protocol |
| 6028 | `InsufficientDeployment` | Withdrawal or recall larger than what is deployed |
| 6029 | `DeploymentTooHigh` | `configure_yield_strategy` share out of range |
| 6030 | `OracleReadError` | `OracleConfig` too short to read |
| 6031 | `InvalidPool` | Legacy Meteora instructions only |
| 6032 | `PoolNotConfigured` | Legacy Meteora instructions only |
| 6033 | `AdminWithdrawCapExceeded` | No longer returned. It reported the old 24 h cap, which `TreasuryPolicy` replaced on 11 Oct 2026 |
| 6034 | `InvalidPosition` | Legacy Meteora position mismatch, or a wrong account in `clp_burn_excess` |
| 6035 | `InvalidAccountData` | Unused |
| 6036 | `InvalidVault` | A token account isn't the protocol-owned account expected: the wrong owner or mint for the global vault, a CLP vault or the CLP's L and S accounts |
| 6037 | `TreasuryFrozen` | `admin_withdraw` or `treasury_lend` while the treasury is frozen |
| 6038 | `TreasuryAllowanceExceeded` | `admin_withdraw` or `treasury_lend` above the refilled allowance |
| 6039 | `InvalidTreasuryParams` | Rate or burst of 0, a delay outside 0 to 30 days, or no guardian |
| 6040 | `InvalidTreasuryDestination` | `admin_withdraw` to an account other than the policy destination, or a new destination that isn't the account passed or holds another mint |
| 6041 | `NoPendingTreasuryChange` | `apply_treasury_limits` with nothing pending |
| 6042 | `TreasuryChangeNotReady` | `apply_treasury_limits` before `pending_at` |
| 6043 | `MarketMismatch` | A vault instruction named a market other than the CLP's own |
| 6044 | `PoolClosedWhileLive` | `pool_swap` while the market's price is live: its risk state is `Normal`, or its last push is at most 300 s old |
→ [Full error catalog](/reference/errors)
## TypeScript
This reads QQQ's long book and builds a `pool_swap` that spends up to 10 cUSDC on QQQ-L. `provider` is set up as in [SDK setup](/build/sdk-setup), and the IDL is published in [`continuum-docs/idl`](https://github.com/continuum-markets/continuum-docs/tree/main/idl).
```typescript theme={null}
import { BN, Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import clpIdl from "./clp.json";
const clp: any = new Program(clpIdl as any, provider);
const pda = (...seeds: Buffer[]) => PublicKey.findProgramAddressSync(seeds, clp.programId)[0];
const market = new PublicKey("F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5"); // QQQ
const clpPda = pda(Buffer.from("clp"), market.toBuffer());
const longPool = pda(Buffer.from("pool"), market.toBuffer(), Buffer.from([0])); // 0 = long
const revenueAttribution = pda(Buffer.from("revenue_attribution"));
const [perMarket, pool] = await Promise.all([
clp.account.clp.fetch(clpPda),
clp.account.pool.fetch(longPool),
]);
// Anchor renders base_price_1e6 as basePrice1E6 (capital E).
console.log("Book centre:", pool.basePrice1E6.toNumber() / 1e6);
console.log("Committed spread (bps):", Math.max(perMarket.commitSpreadBps, 10));
console.log("Swap guards enabled:", perMarket.swapGuards.enabled);
// Build a pool_swap that spends up to 10 cUSDC on QQQ-L.
// The pool's vaults are the pool PDA's associated token accounts.
const user = provider.publicKey;
const ix = await clp.methods
.poolSwap({ direction: 0, amountIn: new BN(10_000_000), minOut: new BN(1) }) // set minOut from your own quote
.accountsStrict({
user,
userUsdc: getAssociatedTokenAddressSync(pool.usdcMint, user),
userSynth: getAssociatedTokenAddressSync(pool.synthMint, user),
pool: longPool,
clp: clpPda,
synthVault: getAssociatedTokenAddressSync(pool.synthMint, longPool, true),
usdcVault: getAssociatedTokenAddressSync(pool.usdcMint, longPool, true),
tokenProgram: TOKEN_PROGRAM_ID,
revenueAttribution,
market, // the mint-redeem Market, last
})
.instruction();
```
On 10 Oct 2026 the read printed a centre of 751.27, a committed spread of 18 bps and `true` for the swap guards. A simulation of the instruction filled 10 cUSDC for 0.013257 QQQ-L. That was a Saturday, with QQQ's price stale; while its price is live, the same instruction fails with `PoolClosedWhileLive`. Send it in a transaction that first creates the user's synth ATA if it doesn't exist.
## See also
Why the CLP exists, how capital flows and the inventory bounds.
Swapping on the bin book and placing committed orders.
# faucet (devnet)
Source: https://docs.continuum.markets/programs/faucet
Drips 1,000 cUSDC per wallet every 24 hours on devnet. There is no mainnet faucet.
The faucet program mints cUSDC, the devnet collateral token, to any wallet that asks, at most once per cooldown. The `drip` instruction exists only in the devnet build; on mainnet the collateral is real USDC. In the app, the button reads "Get 1000 cUSDC".
```
Devnet ID: 9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY
cUSDC mint: B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE
```
As of 10 Oct 2026 the cUSDC faucet drips 1,000 cUSDC (`drip_amount = 1_000_000_000`) with a 24 h cooldown (`cooldown_seconds = 86_400`). Amounts are in base units, and cUSDC has 6 decimals.
## Accounts
### `FaucetState` PDA
```
seeds = [b"faucet", mint]
```
One per mint. As of 10 Oct 2026 only cUSDC has one (`9U5jPkt46MhtqGt22yWQuKTUDKNUZXEZBekJNJuSMt1N`).
| Field | Type | Purpose |
| - | - | - |
| `mint` | Pubkey | Token the faucet mints |
| `authority` | Pubkey | Signer for `update_params` and `transfer_authority` |
| `drip_amount` | u64 | Base units per drip (1,000,000,000 = 1,000 cUSDC) |
| `cooldown_seconds` | i64 | Minimum time between two drips to one wallet |
| `total_dripped` | u64 | Lifetime base units minted by `drip` |
| `bump` | u8 | PDA bump |
| `mint_authority_bump` | u8 | Bump of the `mint_authority` PDA |
### `mint_authority` PDA
```
seeds = [b"mint_authority", mint]
```
Holds no data. It is the cUSDC mint authority (`HGtoCZ8VWGJvW5yiBai7VZictfrs3h7tmvFFJUoumoNw`) and signs the `mint_to` inside `drip`.
### `DripRecord` PDA
```
seeds = [b"drip", mint, recipient]
```
One per wallet, created on its first drip. The recipient pays its rent (0.0007366 SOL on devnet as of 10 Oct 2026).
| Field | Type | Purpose |
| - | - | - |
| `last_drip` | i64 | Unix time of the wallet's last drip |
| `bump` | u8 | PDA bump |
## Instructions
### `initialize(drip_amount: u64, cooldown_seconds: i64)`
Creates the `FaucetState` for a mint. Whoever calls it first for a mint becomes that faucet's `authority`. It doesn't touch the mint itself. Before anyone can drip, the mint's current authority must hand minting to the faucet's PDA:
```bash theme={null}
spl-token authorize mint
```
Accounts: `mint_authority`, `faucet_state`, `mint`, `authority`, `system_program`.
### `drip()` (devnet build only)
Mints `drip_amount` to the signer's token account. Fails with `CooldownNotElapsed` if that wallet's last drip was less than `cooldown_seconds` ago. The cooldown is per wallet: `DripRecord` is keyed by the recipient, who must also sign. The recipient pays the transaction fee and, on a first drip, the `DripRecord` rent. `recipient_ata` must already exist, hold this mint and be owned by the recipient.
Accounts, in order: `faucet_state`, `mint_authority`, `mint`, `drip_record`, `recipient_ata`, `recipient` (signer), `token_program`, `system_program`.
### `update_params(drip_amount: u64, cooldown_seconds: i64)` (faucet authority)
Replaces both values.
### `transfer_authority(new_authority: Pubkey)` (faucet authority)
Replaces `authority`.
## Errors
| Code | Name | Cause |
| - | - | - |
| 6000 | `CooldownNotElapsed` | The recipient dripped less than `cooldown_seconds` ago |
## TypeScript
This drips to the provider's wallet. `provider` is set up as in [SDK setup](/build/sdk-setup), and the IDL is published in [`continuum-docs/idl`](https://github.com/continuum-markets/continuum-docs/tree/main/idl).
```typescript theme={null}
import { Program } from "@coral-xyz/anchor";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import faucetIdl from "./faucet.json";
const faucet: any = new Program(faucetIdl as any, provider);
const cusdcMint = new PublicKey("B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE");
const recipient = provider.publicKey;
const pda = (...seeds: Buffer[]) => PublicKey.findProgramAddressSync(seeds, faucet.programId)[0];
const recipientAta = getAssociatedTokenAddressSync(cusdcMint, recipient);
await faucet.methods
.drip()
.accountsStrict({
faucetState: pda(Buffer.from("faucet"), cusdcMint.toBuffer()),
mintAuthority: pda(Buffer.from("mint_authority"), cusdcMint.toBuffer()),
mint: cusdcMint,
dripRecord: pda(Buffer.from("drip"), cusdcMint.toBuffer(), recipient.toBuffer()),
recipientAta,
recipient,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
// drip needs the ATA to exist already
.preInstructions([
createAssociatedTokenAccountIdempotentInstruction(recipient, recipientAta, recipient, cusdcMint),
])
.rpc();
const balance = await provider.connection.getTokenAccountBalance(recipientAta);
console.log("cUSDC balance:", balance.value.uiAmountString);
```
## Operator funding
On devnet the operator funds the CLP from the faucet. It sets `drip_amount` to the target and `cooldown_seconds` to 0 with `update_params`, drips once, then restores both values. It then sends the cUSDC to the global vault with a plain SPL transfer. That transfer bypasses `admin_fund`, so `GlobalClp.total_deposits` stays at 0 on devnet. Mainnet has no faucet, and the admin must hold real USDC.
## See also
Cluster, RPC and faucet usage.
A first mint that starts at the faucet.
# mint-redeem
Source: https://docs.continuum.markets/programs/mint-redeem
The Market account, paired mint and redeem at NAV, the keeper's price and γ writes, and market administration, grouped by who can call each instruction.
The mint-redeem program holds one `Market` account per synthetic asset, mints and burns its L and S tokens, and keeps the market's collateral ledger. Users call `mint_paired` and `redeem_paired`. The keeper writes the price, the confidence band, γ and the buffer target. The market authority configures and maintains the market.
```
Devnet ID: 5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4
```
## Accounts
### Market account
```
seeds = [b"market", asset_symbol] // raw symbol bytes, e.g. "QQQ"
```
The account is at most 899 bytes: an 8-byte discriminator plus `Market::INIT_SPACE`, which reserves room for a 10-byte symbol and five `collateral_vaults` entries. `collateral_vaults` is a variable-length `Vec` (empty on every live market), so the fields after it have no fixed byte offsets. Decode the account with the IDL rather than reading at offsets. Devnet markets are allocated 891 bytes (10 Oct 2026); the data fits because the list is empty.
Fields in IDL order. Prices and amounts are 1e6 fixed point (6 decimals).
| Field | Type | Meaning |
| - | - | - |
| `authority` | Pubkey | Market authority. Signs the admin instructions; set to the `initialize_market` signer. |
| `fee_recipient` | Pubkey | Owner of the cUSDC account that receives the dev tax (`devTokenAccount`). |
| `asset_symbol` | String | Ticker, at most 10 bytes. The PDA seed. |
| `oracle_address` | Pubkey | Account that mint and redeem require as `oracle`. On devnet it is the market's `OracleConfig` PDA. |
| `oracle_type` | OracleType | `Pyth`, `Switchboard` or `Continuum`. `Switchboard` is rejected; live markets use `Continuum`. |
| `long_mint` | Pubkey | L token mint. |
| `short_mint` | Pubkey | S token mint. |
| `collateral_mint` | Pubkey | Collateral mint (cUSDC on devnet). |
| `collateral_vault` | Pubkey | Token account that holds the collateral. On every live market it is the Market PDA's own cUSDC ATA. |
| `multi_collateral_enabled` | bool | Multi-collateral list switched on. False on every live market. |
| `collateral_vault_count` | u8 | Number of `collateral_vaults` entries. |
| `collateral_vaults` | Vec\ | Extra collateral types, at most 5. Mint and redeem don't read it. |
| `initial_l_price` | u64 | Genesis L price. With `initial_s_price` it sets K. |
| `initial_s_price` | u64 | Short anchor. `reanchor_gamma` folds γ into it. |
| `mint_fee_bps` | u16 | Base mint fee, at most 1,000. Posted by the keeper. |
| `redeem_fee_bps` | u16 | Base redeem fee, at most 1,000. Posted by the keeper. |
| `total_l_supply` | u64 | L supply ledger. |
| `total_s_supply` | u64 | S supply ledger. Paired flow keeps it equal to L supply; keeper single-side instructions let the two differ. |
| `total_collateral` | u64 | This market's collateral ledger and its solvency source of truth. The vault's token balance can differ from it. A plain token transfer into the vault raises the balance but not the ledger, and collateral deployed to yield leaves the vault but stays in the ledger. |
| `is_active` | bool | False blocks mint, redeem, donations and keeper single-side instructions. |
| `bump` | u8 | PDA bump. |
| `risk_state` | RiskState | `Normal`, `ProxyMode`, `Stress` or `Recovery`. Every devnet market is in `ProxyMode`. |
| `risk_state_updated_at` | i64 | Chain time of the last change of risk state, first set by `initialize_market`. Since 11 Oct 2026, a keeper write that keeps the same state leaves it unchanged, so the `Recovery` surcharge decays from the moment the market enters `Recovery`. |
| `risk_fee_config` | RiskFeeConfig | `proxy_fee_multiplier` (150), `stress_fee_multiplier` (300), `stress_mint_cap` (0), `allow_mint_in_stress` and `allow_redeem_in_stress` (both false). Values are the defaults and the live settings. |
| `user_twap_price` | u64 | The price P, written by the keeper. A spot price despite the name, adjusted for corporate actions. |
| `twap_updated_at` | i64 | Source publish time of that price. Mint and redeem need it to be at most 300 s old. |
| `worst_case_config` | WorstCaseQuotingConfig | `enabled`, plus confidence multipliers for `Normal`, `ProxyMode` and `Stress` (100, 150, 300). |
| `oracle_confidence` | u64 | Confidence band c, adjusted like the price. |
| `keeper_authority` | Pubkey | Keeper signer. |
| `dev_tax_pct` | u8 | Percent of each fee sent to the fee recipient (10 on every market). |
| `yield_ceiling_bps` | u16 | Largest share of `total_collateral` that may be deployed to yield. 0 until `configure_yield_ceiling` sets it. The ceiling may reach 10,000 bps (100%). Read on 13 Oct 2026, all 13 live markets are set to 10,000. |
| `deployed_to_yield` | u64 | Collateral deployed to yield. |
| `total_yield_harvested` | u64 | Cumulative harvested yield. |
| `yield_enabled` | bool | Gates `deploy_collateral_to_yield` and `harvest_collateral_yield`. `initialize_market` sets it to true. It is true on all 13 live markets as of 13 Oct 2026. |
| `buffer_target_bps` | u16 | Keeper-posted buffer over parity. The target ratio is 10,000 plus this value clamped to 100 to 5,000 bps; 0 reads as 200 (a 102% target). |
| `s_gamma_index` | u64 | γ at 1e9 scale. 0 reads as 1.0. Moves only down, never below 0.5. |
| `gamma_day_start_ts` | i64 | Start of the current γ day bucket (telemetry). |
| `gamma_day_decay_1e9` | u64 | γ decay inside the current day bucket. Telemetry only; no cap is enforced. |
| `gamma_last_write_ts` | i64 | Time of the last accepted γ write. |
| `gamma_epoch` | u32 | Incremented by `reanchor_gamma`. `set_s_gamma_index` must quote it. |
| `reanchor_count` | u32 | Re-anchors so far. |
| `genesis_s_price` | u64 | `initial_s_price` before the first re-anchor; 0 until one happens. |
| `last_reanchor_ts` | i64 | Time of the last re-anchor; 0 if none. |
| `short_payoff_cap_c_1e9` | u64 | Reserved. No instruction reads or writes it. |
| `price_adjust_num` | u64 | Cumulative corporate-action ratio, numerator. 0/0 reads as 1/1. |
| `price_adjust_den` | u64 | Cumulative corporate-action ratio, denominator. |
| `price_adjust_pending_num` | u64 | Armed ratio waiting for its ex-date, numerator. |
| `price_adjust_pending_den` | u64 | Armed ratio, denominator. |
| `price_adjust_effective_ts` | i64 | When the armed ratio takes effect; 0 if none is armed. |
| `price_adjust_last_effective_ts` | i64 | Most recent units boundary. Keeper prices with an earlier source time are rejected. |
Anchor TS decodes `gamma_day_decay_1e9` and `short_payoff_cap_c_1e9` as `gammaDayDecay1E9` and `shortPayoffCapC1E9`; every other field is plain camelCase (`userTwapPrice`).
### FeeWaiver
```
seeds = [b"fee_waiver", donor, market]
```
Created by the donor's first `donate_to_vault` on a market and read by the `_with_waiver` instructions. Fields: `donor`, `market`, `waiver_end_at` (unix seconds), `total_donated`, `total_gap_closed_bps` and `bump`.
### RedemptionRequest
```
seeds = [b"redemption", market, owner] // 223 bytes
escrows = [b"redemption_escrow", request, mint] // one token account per leg
```
One open request per owner and market. `request_redemption` creates it and moves the owner's L and S into two escrow token accounts owned by the request PDA. Fields: `owner`, `market`, `long_mint`, `short_mint`, `l_requested`, `s_requested`, `l_remaining`, `s_remaining`, the running totals `gross_paid`, `net_paid`, `fee_paid` and `dev_tax_paid`, `fills`, `created_at`, `last_fill_at` and the three bumps. The request closes, and its rent goes back to the owner, when it is paid in full or cancelled.
### MarketAdvance
```
seeds = [b"market_advance", market] // 123 bytes
```
The ledger of the treasury's loan to one market, created by `configure_market_advance`. Fields: `market`, `lender` (the treasury vault, the only account an advance can come from), `max_advance_bps` (0 turns advances off), `outstanding`, `total_advanced`, `total_repaid`, `total_written_off`, `last_advance_at`, `last_repay_at` and `bump`. Advance cash sits in the market's collateral vault but is not counted in `total_collateral`, so the collateral ratio and the redeem haircut never see it.
### The `collateral_authority` slot
Every market keeps its collateral in its own vault, owned by its Market PDA, and the Market PDA signs every payout. The upgrade of 11 Oct 2026 removed the unified vault from the program, along with the `CollateralAuthority` account type.
Seven instructions still have a `collateral_authority` account in its old position:
* `redeem_paired`
* `redeem_paired_with_waiver`, inside `base` and before `fee_waiver`
* `keeper_mint_single` and `keeper_redeem_single`
* `redeem_single_for_vault`
* `withdraw_excess_to_clp` and `deploy_collateral_to_yield`
It is now an ignored optional slot. The program never reads it and accepts any account. Pass mint-redeem's program ID there: in Anchor that is `collateralAuthority: null` in TypeScript or `collateral_authority: None` in a Rust CPI.
The four unified-vault instructions are gone: `init_collateral_authority`, `migrate_to_unified_vault`, `exit_unified_vault` and `sweep_unified_surplus`. Calling one now fails with Anchor error 101 (`InstructionFallbackNotFound`).
## Pricing and fees
### Price source
In `ProxyMode`, `Stress` and `Recovery`, mint and redeem price from `user_twap_price` and revert with `TwapNotAvailable` when it is 0 or `twap_updated_at` is more than 300 s old. The keeper doesn't push while the underlying market is closed, so mint and redeem revert until it reopens. In `Normal` the program reads the oracle account itself. A Pyth feed must then be at most 60 s old, with a confidence interval within 2% of the price; a `Continuum` `OracleConfig` must be unpaused, `Active` and fresh. No market runs in `Normal` today.
### NAV and worst-case quotes
With P the price above, L = P and S = K / P × γ, where K = `initial_l_price × initial_s_price` and γ = `s_gamma_index` / 1e9.
Mint and redeem quote each leg against the user. Let c be `oracle_confidence` times the state's confidence multiplier: 1.5× in `ProxyMode`, 3× in `Stress`, 1× in `Normal`, and in `Recovery` 3× falling to 1× over the hour after the market enters `Recovery` (`risk_state_updated_at`). A mint prices L at P + c and S at K / (P − c) × γ. A redeem prices L at P − c and S at K / (P + c) × γ. Worst-case quoting is on by default, and `configure_worst_case_quoting` can switch it off per market; as of 10 Oct 2026 it is off on QQQ.
### Fees
The charged fee is `mint_fee_bps` or `redeem_fee_bps` times the state's fee multiplier: 1× in `Normal`, 1.5× in `ProxyMode`, 3× in `Stress`, and in `Recovery` 3× falling to 1× over the hour after the market enters `Recovery` (`risk_state_updated_at`). The keeper posts both fees through `update_market` as `clamp(fee_mult × half-spread, 10, 100)` bps, using its own half-spread estimate for the market (`fee_mult` defaults to 1.0). As of 10 Oct 2026 the posted fees are QQQ 18, SPY 16, NVDA 31 and EUR, GBP and JPY 10 bps, so QQQ is charged 27 bps in `ProxyMode`.
The mint fee comes out of the deposit; the redeem fee comes out of the gross payout after any haircut. `dev_tax_pct` (10%) of each fee goes to the fee recipient's cUSDC account, the treasury on every live market since 12 Oct 2026. The rest stays in the vault and counts in `total_collateral`.
### Limits
Mint and redeem each need at least 10 cUSDC of value (`BelowMinimum`). `Stress` blocks both by default, and every live market keeps that default. `stress_mint_cap` applies only in `Stress` and is 0 (no cap).
## Instructions
Grouped by the signer the program checks. Anchor TS account names are camelCase versions of the IDL names (`user_long_account` becomes `userLongAccount`).
### Anyone
#### `mint_paired(collateral_amount: u64)`
Deposits `collateral_amount` cUSDC (at least 10 cUSDC) and mints L and S to the user, half of the net value to each leg at the mint NAVs. Accounts: `market`, `user`, `userCollateral`, `userLongAccount`, `userShortAccount`, `longMint`, `shortMint`, `collateralVault`, `devTokenAccount`, `oracle`, `tokenProgram`. The three user token accounts must already exist. See the [mint flow](/flows/mint).
#### `redeem_paired(l_amount: u64, s_amount: u64)`
Burns `l_amount` L and `s_amount` S and pays `l_amount × L + s_amount × S` at the redeem NAVs, less the fee. Both amounts must be above 0; they can differ. The accounts are those of `mint_paired` plus the ignored `collateralAuthority` slot; pass `null`. If the market's collateral ratio at those NAVs is under 100%, every redeemer is paid `nominal × collateral ratio`, so early redeemers can't drain the vault ahead of others. Above 100% the haircut does nothing. If the payout is larger than the market's liquid collateral (`total_collateral` minus `deployed_to_yield`) or the vault's balance, it fails with `InsufficientLiquidCollateral` (6022). Queue the redemption with `request_redemption` instead. See the [redeem flow](/flows/redeem).
#### `mint_paired_with_waiver` and `redeem_paired_with_waiver`
Same arguments as `mint_paired` and `redeem_paired`. The accounts are the base accounts nested under `base`, plus the caller's `feeWaiver`. The fee is 0 when the waiver hasn't expired, the price is at most 300 s old and the market's collateral ratio is at or above its target ratio (the daily Monte Carlo target the keeper posts for each market, 101% or more; see [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer)). Otherwise the normal fee applies.
#### `donate_to_vault(amount: u64)`
Adds cUSDC to a market's collateral without minting anything, and extends the donor's `FeeWaiver` by 6 s for each basis point of the gap to the target ratio that the donation closes. A donation to a market already at or above target extends nothing. It needs a price at most 300 s old. The instruction is still on-chain, but the app stopped offering it on 9 Oct 2026.
#### `request_redemption(params: RedemptionAmounts)`
Queues a paired redemption the vault cannot pay now. `RedemptionAmounts` holds `l_amount` and `s_amount`, both above 0. It runs the same checks as `redeem_paired` (market active, risk state, price freshness, the 10 cUSDC minimum), then moves the L and S into the request's escrow token accounts and creates the [`RedemptionRequest`](#redemptionrequest). Nothing is burned or paid yet. Signer: the owner. Accounts: `market`, `owner`, `request`, `escrowL`, `escrowS`, `ownerLongAccount`, `ownerShortAccount`, `longMint`, `shortMint`, `oracle`, `tokenProgram`, `systemProgram`. Emits `RedemptionRequested`. See the [redeem flow](/flows/redeem#when-the-vault-cannot-pay-yet).
#### `cancel_redemption()`
Returns the unpaid part of a request, with all rent, and closes it. Signer: the owner. It reads no `Market`, so it works while the market is paused, the price is stale or the keeper is down. It returns each escrow's actual balance, so atoms sent to an escrow by someone else can't block the close. Emits `RedemptionCancelled`.
#### `permissionless_burn(is_long: bool, amount: u64)`
Burns L or S from the signer's own token account with no payout, which lowers the supply ledger. A delegate can't call it on someone else's account. The CLP's `clp_burn_excess` uses it to retire inventory.
### Setup
#### `initialize_market(params: InitMarketParams)`
`InitMarketParams { asset_symbol, oracle_type, initial_l_price, initial_s_price, mint_fee_bps, redeem_fee_bps, keeper_authority }`. Only the mint-redeem program's upgrade authority can call it: the instruction takes the program and its `program_data` as its last two accounts and fails with `UnauthorizedInitializer` (6046) for any other signer. The signer becomes `market.authority` and `fee_recipient`. The symbol must be at most 10 bytes, both fees at most 1,000 bps, and both prices above 0. `oracle_type` must be `Pyth` or `Continuum`, and for `Continuum` the `oracle` account must be the market's `OracleConfig` PDA. The instruction doesn't create the L and S mints or the vault: they must already exist, with the Market PDA as mint authority of both mints. A new market starts in `Normal` with `user_twap_price = initial_l_price`, `dev_tax_pct` 10, the default risk-fee and worst-case settings, and a 1/1 price adjustment. It also sets `yield_enabled = true` and leaves `yield_ceiling_bps` at 0, so harvests can run at once but nothing can be deployed to a lending venue until `configure_yield_ceiling` raises the ceiling. The full sequence is in [listing a market](/programs/overview#listing-a-market-operator-process).
### Keeper (signer = `market.keeper_authority`)
#### `update_risk_state_v2(new_risk_state: RiskState, user_twap_price: u64, oracle_confidence: u64, source_publish_time: i64)`
The keeper's price write. It maps the price and the confidence through the corporate-action ratio, then stores them with the risk state and sets `twap_updated_at` to `source_publish_time`. It rejects a source time in the future, more than 300 s old or before the latest corporate-action boundary. It also rejects a price of 0, a move of more than 25% from the stored price, and any move out of `Stress` except to `Recovery`. `update_risk_state(new_risk_state, user_twap_price, oracle_confidence)` does the same without a source time and stamps `twap_updated_at` with the chain time.
#### `set_s_gamma_index(params: SetGammaParams)`
`SetGammaParams { s_gamma_index: u64, expected_epoch: u32 }` posts γ at 1e9 scale. Bad writes are rejected, never clamped: a value outside 0.5 to 1.0 (`GammaOutOfRange`), a value above the current γ (`GammaMustRatchetDown`), or an `expected_epoch` other than `gamma_epoch` (`GammaEpochMismatch`). There is no per-write or per-day cap. γ is always on; the keeper refuses to boot if `CLMM_GAMMA_DECAY_ENABLED` is set.
#### `set_buffer_target(buffer_target_bps: u16)`
Posts the market's buffer over parity, clamped to 100 to 5,000 bps (a target ratio of 101% to 150%). The keeper posts each market's daily Monte Carlo target, and only when it differs from the stored one; see [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer). The target gates only three things: the fee waiver, the waiver credit a donation earns for closing the gap to target, and how much excess a market can shed (`withdraw_excess_to_clp`, `rebalance_collateral`). Mints and redeems don't read it.
#### `settle_redemption(params: SettleRedemptionParams)`
Pays the largest part of a request that the vault's cash covers now, up to `max_l_amount` of L with the matching share of S. Prices, haircut, fee and dev tax work exactly as in `redeem_paired`. It burns that part from escrow, pays the owner and updates the request. Unlike `redeem_paired`, it may spend the cash of a treasury advance, but never more than the vault holds. It fails with `NothingToSettle` (6053) when the request is empty or no part of it fits. When the request is paid in full and the owner is passed, the escrows and the request close and the rent goes back to the owner. Emits `RedemptionSettled` and `PairedActionExecuted`.
#### `keeper_mint_single`, `keeper_redeem_single`, `keeper_burn_single`
`(is_long: bool, collateral_amount: u64)` for the mint and `(is_long: bool, token_amount: u64)` for the other two. They are a fee-free single-leg mint and redeem at the base NAV, with no worst-case band and no minimum, and a burn with no payout. The mint must leave the collateral ratio at or above the lower of its pre-trade value and 100% (`SingleSideMintStressFloor`). The redeem must leave it at or above 100%. Users have no single-leg redeem.
#### `rebalance_collateral(amount: u64)`
Moves up to `amount` of collateral from one market's vault to another's. The signer must be the keeper of both markets, and the two markets must differ (`InvalidAmount`). The move is capped at the smallest of the request, the source's collateral above its own target ratio at the stored price, and its liquid collateral (`total_collateral − deployed_to_yield`). A request above the cap moves the capped amount, and the log line shows both figures. It fails only when the source has nothing above target (`InsufficientCollateral`) or no liquid collateral (`InsufficientLiquidCollateral`). The source's price must be at most 300 s old.
The move is a plain token transfer from the source market's vault to the other market's vault, signed by the source Market PDA. If the two markets share a vault, the transfer would move nothing while both ledgers changed, so the program refuses it (`InvalidAmount`).
#### `deploy_collateral_to_yield`, `recall_collateral_from_yield`, `harvest_collateral_yield`
`(amount: u64, _protocol: u8)` for deploy and recall, `(yield_amount: u64)` for harvest. Deploy and recall move collateral to and from a lending venue through the keeper's token account. A deployment must stay within `yield_ceiling_bps` of `total_collateral`.
Harvest moves `yield_amount` from the keeper's token account into `market.collateral_vault` and adds it to `total_collateral` and `total_yield_harvested`, so harvested yield raises the market's collateral ratio. Collateral above the target can still reach the CLP through `withdraw_excess_to_clp`. Harvest's fourth account was named `clp_vault` and received the yield until 11 Oct 2026. It is now `collateral_vault`, in the same position, and must be the market's own vault.
Every live market has `yield_enabled = true`. Nothing is deployed on devnet: the keeper's emulator calls harvest every hour with cUSDC minted at the rate its mainnet lending plan would earn.
### CLP PDA (CPI only)
#### `redeem_single_for_vault(is_long: bool, token_amount: u64)`
Like `keeper_redeem_single`, but it burns from CLP-owned token accounts and pays the market's CLP vault. The signer must be the CLP PDA `[b"clp", market]`, so it runs only through the CLP program's `vault_redeem_single`.
### Market authority (signer = `market.authority`)
#### `update_market(mint_fee_bps?, redeem_fee_bps?, is_active?, dev_tax_pct?)`
Fees at most 1,000 bps each; `dev_tax_pct` at most 25. The keeper posts fees through this instruction.
#### `update_keeper_authority(new_keeper_authority)`, `update_market_oracle(oracle_type)`, `update_fee_recipient(new_recipient)`
Rotate the keeper signer, repoint the oracle account (validated like `initialize_market`), or change the fee recipient.
#### `configure_risk_fees(params: ConfigureRiskFeesParams)`
`ConfigureRiskFeesParams { proxy_fee_multiplier, stress_fee_multiplier, stress_mint_cap, allow_mint_in_stress, allow_redeem_in_stress }`, every field optional. Sets the fields of `risk_fee_config`; a field left `None` is unchanged. Multipliers use 100 = 1×. The struct holds the separate arguments the instruction took until 11 Oct 2026, with the same fields in the same order, so the bytes on the wire are unchanged.
#### `configure_worst_case_quoting(enabled?, normal_multiplier?, proxy_multiplier?, stress_multiplier?)`
Sets `worst_case_config`. Multipliers use 100 = 1×.
#### `configure_yield_ceiling(ceiling_bps?, enabled?)`
Sets `yield_ceiling_bps` (at most 10,000) and `yield_enabled`.
#### `reanchor_gamma()`
Folds γ into the short anchor: `initial_s_price` becomes `floor(initial_s_price × γ)`, γ resets to 1.0 and `gamma_epoch` goes up by one. Every holder's claim stays the same at every price; the fold only resets how far γ can still fall. It does not reset the anchor to the spot price. Calls must be at least 14 days apart. Emits `GammaReanchored`. As of 10 Oct 2026 no market has been re-anchored.
#### `apply_corporate_action(params: CorporateActionParams)`
`CorporateActionParams { ratio_num, ratio_den, effective_ts }`, as new shares to old shares (a 2-for-1 split is 2:1). It folds the ratio into `price_adjust_num` and `price_adjust_den` and touches no price, supply or collateral. From then on each incoming keeper price is multiplied by the ratio before it is stored, which keeps P continuous across the split. An `effective_ts` at or before now applies immediately. A later time, up to 400 days ahead, arms the ratio for the ex-date; only one action can be armed at a time. Ratios beyond 1,000:1 are rejected. Emits `CorporateActionApplied`. As of 10 Oct 2026 no action is armed.
#### `withdraw_excess_to_clp(amount: u64)`
Moves collateral above the market's target ratio into the market's CLP vault. It needs a price at most 300 s old and moves only liquid collateral.
#### `mark_yield_impairment(amount: u64)`
Writes down a lost yield deployment by reducing both `deployed_to_yield` and `total_collateral`, so the redeem haircut sees the loss.
#### `configure_market_advance(params: ConfigureAdvanceParams)`
Sets the market's advance cap, `max_advance_bps` of `total_collateral` (at most 10,000; 0 turns advances off), and its lender, creating the [`MarketAdvance`](#marketadvance) on first use. The lender must be the CLP treasury vault, the cUSDC account of the `GlobalClp` PDA (`InvalidAdvanceLender`). It can change only while nothing is outstanding (`AdvanceLenderLocked`). Emits `MarketAdvanceConfigured`.
#### `receive_advance(amount: u64)`
Takes a treasury advance into the collateral vault. Both `market.authority` and the `funder` who owns the lender account sign; the CLP's [`advance_to_market`](/programs/clp#treasury) calls it with the `GlobalClp` PDA as funder. The cash is not added to `total_collateral`. The total outstanding can't pass the cap (`AdvanceCapExceeded`). Emits `AdvanceReceived`.
#### `repay_advance(amount: u64)`
Pays advance cash back from the vault to the lender. Signer: `market.authority` or the keeper. It can't repay more than is outstanding (`AdvanceRepayExceedsOutstanding`), and the vault must keep the market's own liquid collateral (`AdvanceWouldTakeOwnLiquidity`). Emits `AdvanceRepaid`.
#### `write_off_advance(amount: u64)`
Charges a venue loss to the outstanding advance first, because the treasury is first-loss capital. It lowers `deployed_to_yield` and `outstanding` by `amount` and moves no cash. A loss larger than the advance goes through `mark_yield_impairment`, which refuses while any advance is outstanding (`AdvanceOutstanding`). Emits `AdvanceWrittenOff`.
#### `create_token_metadata(name, symbol, uri)`
Creates Metaplex metadata for the market's L or S mint. The Market PDA signs as mint authority; the signer pays and becomes update authority. Limits: name 32 bytes, symbol 10, URI 200.
#### `realloc_market()`
Grows a `Market` account to 899 bytes after an upgrade that adds fields, with the signer paying the extra rent. It checks the owner, the discriminator and the `authority` stored at bytes 8 to 40. An account that is already large enough is left alone.
#### `close_market_admin()`
Zero-fills a market account and returns its lamports to the signer. For an account in the current `Market` layout it fails with `MarketNotEmpty` unless L supply, S supply, `total_collateral` and `deployed_to_yield` are all 0.
#### `fund_collateral(amount: u64)`
Tops up a market's collateral from the authority's own cUSDC account. It transfers `amount` into `market.collateral_vault` and adds it to `total_collateral`. Unlike `donate_to_vault`, it grants no fee waiver and has no price check. Emits `CollateralFunded`.
#### `fund_collateral_from(amount: u64)`
`fund_collateral` with a separate funder, so a program PDA such as the CLP treasury can fund a market through CPI. Added on 11 Oct 2026. It moves `amount` cUSDC from the funder's account into the market's vault, adds it to `total_collateral` and emits `CollateralFunded`. Accounts, in order:
| # | Account | Rule |
| - | - | - |
| 0 | `market` (mut) | The Market PDA |
| 1 | `authority` (signer) | Must be `market.authority` (`Unauthorized`) |
| 2 | `funder` (signer) | Owns `funder_collateral`; can be a PDA signing through CPI |
| 3 | `collateral_vault` (mut) | Must be `market.collateral_vault` (`InvalidAmount`) |
| 4 | `funder_collateral` (mut) | Mint must be the collateral mint (`InvalidAmount`) and owner must be `funder` (`Unauthorized`) |
| 5 | `token_program` | SPL Token |
The CLP's [`backstop_market`](/programs/clp#treasury) calls it with the `GlobalClp` PDA as funder. The keeper sends that backstop when a market falls below 100% or stays short of its target.
#### `enable_multi_collateral()`, `add_collateral_vault(collateral_type, weight_bps)`, `update_collateral_vault(collateral_mint, is_active?, weight_bps?)`
Manage the `collateral_vaults` list. Mint and redeem don't read it, and it is empty on every market.
## Events
| Event | Emitted by | Fields |
| - | - | - |
| `PairedActionExecuted` | every paired mint and redeem, including the waiver variants and the CLP's CPI calls | `user`, `market`, `collateral_mint`, `long_mint`, `short_mint`, `action` (0 mint, 1 redeem), `is_keeper`, `collateral_amount` (paid in or out), `gross_collateral_amount` (before the fee), `long_amount`, `short_amount`, `fee_amount`, `dev_tax`, `long_decimals`, `short_decimals` |
| `GammaReanchored` | `reanchor_gamma` | `market`, `epoch`, `folded_gamma`, `old_initial_s_price`, `new_initial_s_price`, `genesis_s_price`, `reanchor_count`, `ts` |
| `CorporateActionApplied` | `apply_corporate_action` | `market`, `ratio_num`, `ratio_den`, `old_price_adjust_num`, `old_price_adjust_den`, `new_price_adjust_num`, `new_price_adjust_den`, `effective_ts`, `armed`, `price_at_apply`, `ts` |
| `CollateralFunded` | `fund_collateral` and `fund_collateral_from` | `market`, `amount`, `total_collateral` (after the top-up), `ts` |
| `RedemptionRequested` | `request_redemption` | `request`, `owner`, `market`, `l_amount`, `s_amount`, `ts` |
| `RedemptionSettled` | `settle_redemption` | `request`, `owner`, `market`, `l_amount`, `s_amount`, `gross`, `net`, `fee`, `dev_tax`, `l_remaining`, `s_remaining`, `ts` |
| `RedemptionCancelled` | `cancel_redemption` | `request`, `owner`, `market`, `l_returned`, `s_returned`, `ts` |
| `MarketAdvanceConfigured` | `configure_market_advance` | `market`, `lender`, `max_advance_bps` |
| `AdvanceReceived` | `receive_advance` | `market`, `amount`, `outstanding`, `cap`, `ts` |
| `AdvanceRepaid` | `repay_advance` | `market`, `amount`, `outstanding`, `ts` |
| `AdvanceWrittenOff` | `write_off_advance` | `market`, `amount`, `outstanding`, `deployed_to_yield`, `ts` |
`is_keeper` is true when the user is the market authority, the keeper or the market's CLP PDA.
## Errors
mint-redeem errors run from 6000 to 6059. Codes 6029 and 6030 are reserved placeholders that are never raised. Codes 6048 to 6052 belonged to the unified vault and are no longer returned; they stay in the enum so later codes don't shift. Codes 6053 to 6059 come from the redemption requests and the treasury advance. Every code, with its message and cause, is in [Errors](/reference/errors#mint-redeem).
## TypeScript
Read a market's price, NAV and freshness. The IDL and its generated type come from [IDL files](/build/idls-and-pdas#idl-files).
```typescript theme={null}
import { Program } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import idl from "./idl/mint_redeem.json" with { type: "json" };
import type { MintRedeem } from "./types/mint_redeem";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const program = new Program(idl as MintRedeem, { connection });
const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from("QQQ")],
program.programId,
);
const market = await program.account.market.fetch(marketPda);
const p = market.userTwapPrice.toNumber() / 1e6; // L NAV in cUSDC
const k = (market.initialLPrice.toNumber() / 1e6) * (market.initialSPrice.toNumber() / 1e6);
const gamma = market.sGammaIndex.isZero() ? 1 : market.sGammaIndex.toNumber() / 1e9;
const age = Math.floor(Date.now() / 1000) - market.twapUpdatedAt.toNumber();
console.log("risk state:", Object.keys(market.riskState)[0]);
console.log("L NAV:", p, "S NAV:", (k / p) * gamma);
console.log("price age:", age, "s", age > 300 ? "(mint and redeem revert)" : "");
```
These are base NAVs. Mint and redeem quote each leg against the user by the confidence band described in [pricing and fees](#pricing-and-fees). For a full mint and redeem script, see the [example](/build/example-mint-redeem).
## See also
Step-by-step `mint_paired` walkthrough.
Step-by-step `redeem_paired` walkthrough.
# oracle
Source: https://docs.continuum.markets/programs/oracle
Per-market store of Pyth observations with an Active/Passive health flag. The devnet keeper does not update it.
The oracle program keeps one `OracleConfig` account per market. It records the latest Pyth observation, a short time-weighted average that feeds a volatility estimate, and an `Active`/`Passive` health flag. Prices and confidence values here are scaled to 1e9: \$562.33 is stored as `562_330_000_000`.
```
Devnet ID: 5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC
```
Don't read `OracleConfig` for live prices. On devnet the keeper never calls `update_price_observation`. QQQ's config was last written on 4 Apr 2026, and the other 13 markets' configs (MSTR's included) have never been written (`last_price` is 0). Live prices are on the mint-redeem Market account: the keeper writes `Market.user_twap_price` with `update_risk_state_v2`. Despite its name, that field is a spot price, scaled to 1e6 and adjusted for corporate actions. Mainnet will price from Pyth (Pyth Pro).
## How mint-redeem uses it
Mint-redeem reads `OracleConfig` only for a market whose `oracle_type` is `Continuum`, and only in the Normal risk state. It takes `last_price / 1_000` as the 1e6 price. It reverts with `OraclePriceUnavailable` if the config is paused or `Passive`, if `last_price` is 0, or if `last_update_time` is more than `max_staleness` seconds old. In every other risk state it prices from `Market.user_twap_price`. All 13 live devnet markets are `Continuum` markets running in ProxyMode, so mint-redeem reads this account for none of them.
## Accounts
### `OracleConfig` PDA
```
seeds = [b"oracle_config", market]
```
One per market, 1,781 bytes. For a `Continuum` market, `Market.oracle_address` holds this PDA. Fields in account order:
| Field | Type | Purpose |
| - | - | - |
| `market_id` | Pubkey | Bound mint-redeem Market PDA |
| `admin_authority` | Pubkey | Signs configuration, feed, pause and realloc instructions |
| `pyth_oracle` | Pubkey | Pyth price account used by the latest update (default pubkey before the first one) |
| `switchboard_oracle` | Pubkey | Reserved, unused |
| `max_confidence_interval` | u64 | `get_price` rejects a wider Pyth confidence (1e9) |
| `max_staleness` | i64 | Seconds. `get_price` rejects an older Pyth print; mint-redeem rejects an older `last_update_time` |
| `is_paused` | bool | Emergency stop |
| `last_price` | u64 | Latest observed price (1e9) |
| `last_confidence` | u64 | Latest confidence (1e9) |
| `last_update_time` | i64 | Unix time the latest observation was written |
| `bump` | u8 | PDA bump |
| `keeper_authority` | Pubkey | Signs `update_price_observation` and `reset_volatility_counters` |
| `keeper_twap` | `TwapState` | Time-weighted average over the keeper window (60 s by default) |
| `user_twap` | `TwapState` | Legacy name: `last_twap` holds the latest spot price, not an average |
| `market_state` | `MarketState` | `Active` or `Passive` |
| `state_changed_at` | i64 | Unix time of the last state change |
| `health_config` | `OracleHealthConfig` | Thresholds for the health flag |
| `recent_volatility_bps` | u16 | Gap between the latest price and `keeper_twap.last_twap`, in bps (at most 10,000) |
| `recent_move_count` | u8 | Updates that moved the price by 50 bps or more (stops at 255) |
| `observation_index` | u8 | Next ring slot to overwrite |
| `observations` | `Vec` | Ring of the last 60 observations |
`TwapState` holds `cumulative_price` (u128), `cumulative_time` (u64), `last_twap` (u64), `last_update` (i64) and `window_seconds` (u32). `PriceObservation` holds `price` (u64, 1e9), `confidence` (u64, 1e9) and `timestamp` (i64). `OracleHealthConfig` holds `max_staleness_seconds` (i64, default 60), `max_confidence_bps` (u16, default 200) and `_reserved` (`[u8; 16]`); the first 8 bytes of `_reserved` store the Pyth publish time of the latest observation.
### `OracleFeed` PDA
```
seeds = [b"oracle_feed", oracle_config, pyth_oracle]
```
One per registered Pyth price account. `update_price_observation` accepts only a registered, active feed.
| Field | Type | Purpose |
| - | - | - |
| `oracle_config` | Pubkey | Bound config |
| `pyth_oracle` | Pubkey | Pyth price account |
| `priority` | u16 | Within a feed kind, lower values are tried first |
| `kind` | `OracleFeedKind` | `Primary`, `PreMarket`, `PostMarket`, `Overnight`, `Fallback` or `Custom` |
| `is_active` | bool | Inactive feeds are rejected (`InactiveOracleFeed`) |
| `bump` | u8 | PDA bump |
On any cluster other than devnet, the keeper picks a feed on each update. It tries `Primary` first, then the kind for the current US session (`Overnight` when the market is closed), then `Fallback`, `PreMarket`, `PostMarket`, `Overnight` and `Custom`. It uses the first feed that is trading, no older than `max_staleness` and within `health_config.max_confidence_bps`. If that feed isn't `Primary`, or the config is `Passive`, the keeper puts the market in ProxyMode rather than Normal. On devnet the keeper skips feed selection entirely.
## Instructions
### Setup (admin)
#### `initialize_oracle_config(max_confidence_interval: u64, max_staleness: i64, keeper_authority: Pubkey)`
Creates the `OracleConfig` for a mint-redeem Market. The signer must be the Market's `authority` (else `UnauthorizedAdmin`) and becomes `admin_authority`. The config starts `Active`, with a 60 s keeper window, a 300 s user window and the default health thresholds. Accounts: `oracle_config`, `market`, `authority`, `system_program`.
#### `upsert_oracle_feed(priority: u16, kind: OracleFeedKind, is_active: bool)`
Creates or updates the `OracleFeed` for one Pyth price account. The handler first parses the account as a Pyth price feed (`InvalidPythAccount` if it isn't one). `admin_authority` signs and pays. Accounts: `oracle_config`, `oracle_feed`, `pyth_oracle`, `admin_authority`, `system_program`.
#### `realloc_oracle_config()`
Grows a config to 1,781 bytes with zero fill and changes no data; `admin_authority` signs and pays. Configs created before the 9 Oct 2026 upgrade were 16 bytes short of a full observation ring. All 14 devnet configs have been grown.
### Updates (keeper)
#### `update_price_observation()`
Takes no arguments. Accounts: `oracle_config`, `oracle_feed`, `pyth_oracle`, and `keeper`, which must equal `keeper_authority` (else `UnauthorizedKeeper`). The instruction:
1. Fails with `OraclePaused` when paused. A second call in the same second returns without writing.
2. Reads the Pyth account with `get_price_unchecked` and rescales price and confidence to 1e9. It applies no staleness, confidence or move-size limit.
3. Stores the Pyth account in `pyth_oracle` and counts a move of 50 bps or more in `recent_move_count`.
4. Sets `market_state` to `Active` if the Pyth print is at most `health_config.max_staleness_seconds` old and its confidence, in bps of the previous `last_price`, is at most `max_confidence_bps`. Otherwise it sets `Passive`.
5. Appends the observation to the ring, recomputes `keeper_twap` and `recent_volatility_bps`, and copies the spot price into `user_twap.last_twap`.
6. Writes `last_price`, `last_confidence` and `last_update_time`, and stores the Pyth publish time in `health_config._reserved`.
It makes no CPI and doesn't touch the Market account.
#### `reset_volatility_counters()`
Keeper-signed, with the same accounts as `update_price_observation`, so it also needs a registered, active feed. It zeroes `recent_move_count` only and leaves `recent_volatility_bps` as is.
### Configuration (admin)
Each of these takes `oracle_config` and an `authority` signer that must equal `admin_authority`.
* `configure_twap_windows(keeper_window_seconds: Option, user_window_seconds: Option)` sets the keeper window (30 to 120 s) and the user window (300 to 600 s). A value outside its range fails with `InvalidTwapWindow`. The user window no longer affects anything.
* `configure_health_thresholds(max_staleness_seconds: Option, max_confidence_bps: Option)` sets the `Active`/`Passive` thresholds.
* `update_oracle_config(max_confidence_interval: Option, max_staleness: Option, is_paused: Option, keeper_authority: Option)` changes any subset; `None` leaves a field unchanged. It is the only way to unpause.
* `emergency_pause()` sets `is_paused = true`.
While paused, `update_price_observation`, `get_price`, `get_user_twap`, `get_user_price` and `get_keeper_price` fail with `OraclePaused`. Mint-redeem honours the pause only in the Normal risk state, so it has no effect on a market in ProxyMode.
### Read-only helpers
These return their value as Anchor return data, so another program can call them by CPI. All take only `oracle_config`, except `get_price`.
| Instruction | Returns |
| - | - |
| `get_price()` | The live Pyth price (1e9) from `pyth_oracle`, which must be the stored `pyth_oracle`. Fails on a confidence above `max_confidence_interval` (`ConfidenceTooHigh`), a print older than `max_staleness` (`PriceStale`) or a move of more than 20% from `last_price` (`PriceMovementTooLarge`). Writes nothing |
| `get_user_twap()`, `get_user_price()` | `user_twap.last_twap`, the latest spot price (`TwapNotReady` if 0) |
| `get_keeper_price()` | `keeper_twap.last_twap` (`TwapNotReady` if 0) |
| `get_keeper_twap()` | Deprecated; always fails with `TwapNotReady` |
| `get_market_state()` | `MarketState` |
| `get_oracle_health()` | `(MarketState, u16, u8, i64)`: state, `recent_volatility_bps`, `recent_move_count` and seconds since `last_update_time`. The IDL lists no return type for it, so decode the 12 bytes of return data yourself (little-endian) |
The deployed devnet program has no `force_price` instruction.
## Errors
| Code | Name | Cause |
| - | - | - |
| 6000 | `OraclePaused` | `is_paused` is true |
| 6001 | `InvalidPythAccount` | The account doesn't parse as a Pyth price feed |
| 6002 | `PriceUnavailable` | The Pyth price is zero or negative |
| 6003 | `ConfidenceTooHigh` | `get_price`: confidence above `max_confidence_interval` |
| 6004 | `PriceStale` | `get_price`: Pyth print older than `max_staleness` |
| 6005 | `PriceMovementTooLarge` | `get_price`: more than 20% from `last_price` |
| 6006 | `TwapNotReady` | The requested value is 0; `get_keeper_twap` always |
| 6007 | `InvalidTwapWindow` | Keeper window outside 30-120 s or user window outside 300-600 s |
| 6008 | `MarketStateRestriction` | Defined but never raised |
| 6009 | `UnauthorizedKeeper` | Signer isn't `keeper_authority` |
| 6010 | `UnauthorizedAdmin` | Signer isn't `admin_authority`, or isn't the Market's `authority` at initialization |
| 6011 | `InvalidRegisteredFeed` | The feed account doesn't match the config or the Pyth account |
| 6012 | `InactiveOracleFeed` | The feed has `is_active = false` |
| 6013 | `MathOverflow` | Arithmetic overflow |
→ [Full error catalog](/reference/errors)
## TypeScript
This compares QQQ's `OracleConfig` with the live price on its Market account. `provider` is set up as in [SDK setup](/build/sdk-setup), and the IDLs are published in [`continuum-docs/idl`](https://github.com/continuum-markets/continuum-docs/tree/main/idl).
```typescript theme={null}
import { Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import oracleIdl from "./oracle.json";
import mintRedeemIdl from "./mint_redeem.json";
const oracle: any = new Program(oracleIdl as any, provider);
const mintRedeem: any = new Program(mintRedeemIdl as any, provider);
const market = await mintRedeem.account.market.fetch(
new PublicKey("F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5"), // QQQ
);
// A Continuum-type market stores its OracleConfig PDA in oracleAddress.
const cfg = await oracle.account.oracleConfig.fetch(market.oracleAddress);
const iso = (t: { toNumber(): number }) => new Date(t.toNumber() * 1000).toISOString();
console.log("OracleConfig price:", cfg.lastPrice.toNumber() / 1e9, "written", iso(cfg.lastUpdateTime));
console.log("OracleConfig state:", Object.keys(cfg.marketState)[0]);
console.log("Market price:", market.userTwapPrice.toNumber() / 1e6, "written", iso(market.twapUpdatedAt));
```
Output on Saturday 10 Oct 2026, when the latest Market price was Friday's last push:
```
OracleConfig price: 562.33 written 2026-04-04T16:03:39.000Z
OracleConfig state: active
Market price: 751.27 written 2026-10-09T20:15:00.000Z
```
## See also
How the keeper prices markets and checks each print.
How ProxyMode, Stress and Recovery change fees and quotes.
# Programs overview
Source: https://docs.continuum.markets/programs/overview
The six Anchor programs behind Continuum, with devnet IDs, published IDLs, PDA seeds, and the signer each instruction requires.
Continuum runs on six programs built with Anchor 0.32.1. They are deployed on **devnet**; mainnet IDs will be added when mainnet launches.
| Program | Devnet ID | Role |
| - | - | - |
| [`mint-redeem`](/programs/mint-redeem) | `5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4` | One `Market` account per asset. Paired mint and redeem at NAV, the keeper's price, confidence and γ writes, and market administration. |
| [`clp`](/programs/clp) | `8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES` | Protocol liquidity: the global vault (the protocol treasury) and the per-market vaults, committed orders (`commit_swap`, `settle_swap`, enabled on every live market), two bin books per market (`pool_swap`, which fills only while the market's price is stale) and the keeper's inventory instructions. |
| [`oracle`](/programs/oracle) | `5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC` | A per-market `OracleConfig` and Pyth feed registry. It is not updated on devnet; markets are priced from `Market.user_twap_price`. |
| [`registry`](/programs/registry) | `Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau` | Symbol entries that point at mints and venues. Deployed and initialized, but it holds no markets and nothing reads it yet. |
| [`faucet`](/programs/faucet) (devnet only) | `9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY` | Drips 1,000 cUSDC per wallet every 24 h. |
| [`trading`](/flows/order-book) | `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA` | Trading accounts over the Manifest order books. The owner's wallet funds and withdraws; a trading key in the browser places and cancels orders within a price band and a per-order limit, and can never withdraw. It reads `Market` for the band's centre and holds nothing but the traders' own book funds. |
A seventh program, governance (`6es5KcjMWKGhVWrttUytYiZ3YELXe4HQyrmsMVdbVawT`), is deployed on devnet but inactive. None of the six programs, the keeper or the app calls it, and it has no published IDL.
## How they relate
```
keeper ── update_risk_state_v2 ──► mint-redeem Market.user_twap_price, oracle_confidence, risk_state
keeper ── set_pool_shape ────────► clp reprices the two bin books of each market
clp ───── CPI ───────────────────► mint-redeem mint_paired, redeem_paired,
redeem_single_for_vault, permissionless_burn,
fund_collateral_from (backstop_market),
receive_advance (advance_to_market)
clp ───── reads Market ──────────► mint-redeem market state and NAV for commit_swap, settle_swap;
risk state and price age for pool_swap
oracle one OracleConfig per market, not updated on devnet
registry standalone and empty
faucet ── mints ─────────────────► cUSDC devnet only
```
The keeper is the protocol's off-chain operator service; its source isn't published. mint-redeem never invokes the CLP program (`withdraw_excess_to_clp` only moves cUSDC into a CLP-owned token account). mint-redeem prices from the oracle account only in the `Normal` risk state, and every devnet market runs in `ProxyMode`, so devnet mint and redeem price from the keeper's write to `Market.user_twap_price`.
## IDLs
An IDL is the JSON description of a program's instructions, accounts, types, events and errors. The program source is private, so the IDLs are published in the docs repository instead, generated from the source of the deployed programs ([browse](https://github.com/continuum-markets/continuum-docs/tree/main/idl)). Each one is at `https://raw.githubusercontent.com/continuum-markets/continuum-docs/main/idl/.json`, where `` is `mint_redeem`, `clp`, `oracle`, `registry`, `faucet` or `trading`. Anchor 0.32 reads the program ID from each IDL's `address` field.
[IDLs and PDAs](/build/idls-and-pdas#idl-files) has a download script and the TypeScript setup. For Rust CPI there is no published crate: load the same IDL with Anchor's `declare_program!` (see [CPI](/build/cpi)).
## PDAs at a glance
| Program | Account | Seeds |
| - | - | - |
| mint-redeem | `Market` | `[b"market", symbol]`, raw symbol bytes such as `QQQ` |
| mint-redeem | `FeeWaiver` | `[b"fee_waiver", donor, market]` |
| mint-redeem | `RedemptionRequest` | `[b"redemption", market, owner]` |
| mint-redeem | redemption escrow (token account, one per leg) | `[b"redemption_escrow", request, mint]` |
| mint-redeem | `MarketAdvance` | `[b"market_advance", market]` |
| oracle | `OracleConfig` | `[b"oracle_config", market]` |
| oracle | `OracleFeed` | `[b"oracle_feed", oracle_config, pyth_price_account]` |
| clp | `GlobalClp` | `[b"global_clp"]` |
| clp | `Clp` (per market) | `[b"clp", market]` |
| clp | `Pool` (bin book) | `[b"pool", market, side]`, where `side` is one byte: 0 long, 1 short |
| clp | `Order` (committed order) | `[b"order", user, market, nonce]`, `nonce` as a little-endian u64 |
| clp | escrow (token account) | `[b"escrow", order]` |
| clp | `RevenueAttribution` | `[b"revenue_attribution"]` |
| clp | `TreasuryPolicy` | `[b"treasury_policy"]` |
| clp | `TreasuryYield` | `[b"treasury_yield"]` |
| registry | `RegistryState` | `[b"registry"]` |
| registry | `MarketEntry` | `[b"market", symbol]`, symbol uppercased and zero-padded to 16 bytes |
| faucet | `FaucetState` | `[b"faucet", mint]` |
| faucet | `DripRecord` | `[b"drip", mint, recipient]` |
| faucet | mint authority (signs only, holds no data) | `[b"mint_authority", mint]` |
| trading | trading account | `[b"account", owner]` |
| trading | trader (the account's Manifest identity) | `[b"trader", owner]` |
A script that derives all of these and checks them on devnet is on [IDLs and PDAs](/build/idls-and-pdas#pda-derivation-cheatsheet).
## Authority model
Each instruction that changes state checks its signer. The table follows the `has_one`, `address` and `require!` checks in the program source. `cancel_treasury_limits` and `freeze_treasury` accept either of two signers, so they appear in two rows.
| Signer | Instructions |
| - | - |
| Anyone | mint-redeem: `mint_paired`, `redeem_paired`, `mint_paired_with_waiver` and `redeem_paired_with_waiver` (with the signer's own waiver), `donate_to_vault`, `permissionless_burn` (owner of the burned account), `request_redemption` and `cancel_redemption` (the owner of the request). clp: `pool_swap`, `commit_swap`, `settle_swap` (any cranker), `cancel_swap` (anyone, after the order expires; the refund goes to the owner), `clp_swap` (retired; always fails), `apply_treasury_limits` (once the pending change's delay has passed). faucet: `drip`. |
| `market.keeper_authority` | mint-redeem: `update_risk_state`, `update_risk_state_v2`, `set_s_gamma_index`, `set_buffer_target`, `keeper_mint_single`, `keeper_redeem_single`, `keeper_burn_single`, `rebalance_collateral` (keeper of both markets), `deploy_collateral_to_yield`, `recall_collateral_from_yield`, `harvest_collateral_yield`, `settle_redemption`, and `repay_advance` (the market authority may also call it). |
| `market.authority` | mint-redeem: `update_market`, `update_keeper_authority`, `update_market_oracle`, `update_fee_recipient`, `configure_risk_fees`, `configure_worst_case_quoting`, `configure_yield_ceiling`, `enable_multi_collateral`, `add_collateral_vault`, `update_collateral_vault`, `withdraw_excess_to_clp`, `mark_yield_impairment`, `reanchor_gamma`, `apply_corporate_action`, `create_token_metadata`, `realloc_market`, `close_market_admin`, `fund_collateral`, `fund_collateral_from` and `receive_advance` (each with a second signer, the `funder`, who owns the cUSDC), `configure_market_advance`, `write_off_advance`, `repay_advance` (the keeper may also call it). oracle: `initialize_oracle_config`. clp: `initialize_clp`. |
| mint-redeem program upgrade authority | mint-redeem: `initialize_market`. |
| CLP PDA `[b"clp", market]` | mint-redeem: `redeem_single_for_vault`, reachable only through the CLP's `vault_redeem_single`. |
| `clp.authority` (per market) | clp: `configure_swap`, `set_commit_spread`, `configure_inventory_budget`, `configure_yield_strategy`, `update_inventory`, `update_oi`, `update_oi_cap`, `collect_fees`, `record_deployment`, `record_withdrawal`, `vault_mint_pairs`, `vault_redeem_pairs`, `vault_redeem_single`, `clp_burn_excess`, `pool_sweep_usdc`, `pool_sweep_synth`, `clp_fund_pool_synth`, `clp_fund_pool_usdc`, and the legacy Meteora instructions (`configure_meteora_pools`, `clear_meteora_positions`, `clp_set_meteora_position`, `clp_init_bin_array`, `clp_open_meteora_position`, `clp_topup_meteora_position`, `clp_remove_meteora_liquidity`, `clp_claim_lp_fees`, `clp_close_meteora_position`). |
| `pool.authority` (per bin book) | clp: `set_pool_shape`, `configure_pool_geom`, `realloc_pool`. |
| `GlobalClp.authority` | clp: `initialize_global_clp`, `init_pool`, `admin_fund`, `admin_withdraw`, `allocate_to_market`, `return_to_global`, `add_yield_to_buffer`, `absorb_loss`, `configure_liquidity_seeder`, `init_revenue_attribution`, `configure_revenue_attribution`, `realloc_clp`, `record_harvest`, `vault_record_liquidity_recall`, `init_treasury_policy`, `set_treasury_limits`, `cancel_treasury_limits`, `freeze_treasury`, `init_treasury_yield`, `treasury_lend`, `treasury_recall`, `backstop_market` and `advance_to_market` (the last two need the signer to also be the market's `market.authority`). |
| `GlobalClp.authority` or a wallet in `RevenueAttribution.keeper_authorities` | clp: `deposit_profit` (the signer's own funds, paid into the treasury vault, which must be owned by the GlobalClp). |
| `TreasuryPolicy.guardian` (freeze-only) | clp: `freeze_treasury`, `unfreeze_treasury` (guardian only), `cancel_treasury_limits`, `set_treasury_guardian` (guardian only). The guardian can't move funds. |
| `OracleConfig.admin_authority` | oracle: `update_oracle_config`, `configure_twap_windows`, `configure_health_thresholds`, `emergency_pause`, `upsert_oracle_feed`, `realloc_oracle_config`. |
| `OracleConfig.keeper_authority` | oracle: `update_price_observation`, `reset_volatility_counters`. |
| Registry authority | registry: `register_market`, `update_market`, `update_positions`, `set_active`, `transfer_authority`. |
| Faucet authority | faucet: `update_params`, `transfer_authority`. |
| Trading account owner | trading: `open_account`, `set_session`, `revoke_session`, `deposit`, `withdraw_sol`, `close_account`. |
| Owner or the account's active session key | trading: `withdraw` (pays only the owner's own token account), `claim_seat`, `deposit_to_market`, `withdraw_from_market`, `place_orders`, `swap_pool`, `commit_order`. A session key is bound by a price band and a per-order limit. |
The setup instructions (`initialize_*`, `init_*`) create program and market accounts. The Continuum team runs them when it lists a market; integrators never need them. The oracle's `get_*` instructions and the CLP's `check_inventory_restrictions` and `get_yield_status` are read-only views with no signer.
On devnet (10 Oct 2026) the Continuum team holds every admin role. `8xhPK1rGK4neoKo4fT8FxHQDZRiCnSYW9hvEdaVTxCni` is `market.authority` on all 14 markets (13 are live), and `9WXGdH5tPCprnATKxnScKt86TUZFYxqeMdejWRrp2RN4` is `market.keeper_authority` on every market. Both are stored on each `Market` account, so you can check them on chain.
## Errors
Each program numbers its errors from 6000, so one code means different things in different programs. Always read a code together with the program ID that raised it. The full tables, with causes, are on [Errors](/reference/errors):
* [mint-redeem errors](/reference/errors#mint-redeem)
* [clp errors](/reference/errors#clp)
* [oracle errors](/reference/errors#oracle)
* [registry errors](/reference/errors#registry)
* [faucet errors](/reference/errors#faucet)
* [trading errors](/reference/errors#trading)
## Choosing what to call
| Goal | Program | Instruction or account |
| - | - | - |
| Mint L and S | `mint-redeem` | `mint_paired` |
| Redeem L and S at NAV | `mint-redeem` | `redeem_paired`, with both amounts above 0 |
| Redeem more than the vault's cash can pay now | `mint-redeem` | `request_redemption`; the keeper pays it with `settle_redemption`, and `cancel_redemption` takes back the unpaid part |
| Read the price, NAV inputs and risk state | `mint-redeem` | fetch `Market` (`user_twap_price`, `twap_updated_at`, `oracle_confidence`, `s_gamma_index`) |
| Trade while the market is open, at NAV at a later keeper price push | `clp` | `commit_swap`, then `settle_swap`; `cancel_swap` refunds after the 180 s TTL |
| Trade instantly against a bin book while the market's price is stale | `clp` | `pool_swap` |
| Read book depth | `clp` | fetch the market's two `Pool` accounts |
| Get devnet cUSDC | `faucet` | `drip` |
There is no single-leg redeem: a lone L or S position exits through a committed order while the market is open, or through a bin book while its price is stale (see [Trade](/flows/trade)). `pool_swap` fails with `PoolClosedWhileLive` while the price is live. `clp_swap`, retired on 11 Oct 2026, reverts with `SwapDisabled` everywhere. Don't read `OracleConfig` for live prices, because the oracle program isn't updated on devnet.
## Listing a market (operator process)
Listing is an operator process; integrators get market addresses from [Addresses](/reference/addresses). The program calls run in this order, because each step needs accounts the previous one created.
Create the L and S mints (classic SPL Token, 6 decimals, the future Market PDA as mint authority, no freeze authority) and a cUSDC token account owned by the Market PDA (live markets use the PDA's associated token account). `initialize_market` creates none of these.
The program's upgrade authority calls `initialize_market(params: InitMarketParams)`, passing the program and its `program_data` as the last two accounts, with `asset_symbol` (at most 10 bytes), `oracle_type` (`Pyth` or `Continuum`), `initial_l_price`, `initial_s_price`, `mint_fee_bps`, `redeem_fee_bps` and `keeper_authority`. With `Continuum`, the `oracle` account must be the market's `OracleConfig` PDA, which doesn't exist yet. The signer becomes `market.authority`. The market starts with `yield_enabled = true` and a yield ceiling of 0.
`initialize_oracle_config(max_confidence_interval, max_staleness, keeper_authority)`, signed by `market.authority`, creates the `OracleConfig`. `upsert_oracle_feed(priority, kind, is_active)`, signed by the config's admin, registers a Pyth price account as a feed.
`initialize_clp(oi_cap, oracle_config)`, signed by `market.authority`, creates the market's `Clp` account.
`init_pool(side, step_bps, num_bins)`, signed by `GlobalClp.authority`, runs once for the long book (side 0) and once for the short book (side 1). A new book stays disabled until the keeper posts its first shape with `set_pool_shape`.
The keeper prices only markets on its configured list, so a new market needs a keeper configuration change before it gets prices and book shapes.
## See also
Market account, paired mint and redeem, keeper and admin instructions.
Vaults, bin books and committed orders.
IDL downloads and a PDA derivation script.
Bootstrapping a TypeScript project against these programs.
# registry
Source: https://docs.continuum.markets/programs/registry
On-chain index of markets by symbol (mints, venue, pools). Deployed and initialized on devnet, but empty and unused.
The registry program maps an asset symbol to its long and short mints, a venue tag and two pool addresses. It is deployed and initialized on devnet, but it holds no markets and nothing reads it. Mint-redeem and CLP derive every address from PDAs. Integrators should take addresses from [Addresses](/reference/addresses).
```
Devnet ID: Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau
```
As of 10 Oct 2026 the `RegistryState` PDA (`ED8joCbjr7hnWPgmiGspQ2LNpUJGfjobZYqEHD1PWyE1`) has `market_count = 0`, and no `MarketEntry` accounts exist. The only venue type is `MeteoraDlmm`, so the registry can't yet describe the native bin books that every market trades on.
## Accounts
### `RegistryState` PDA
```
seeds = [b"registry"]
```
| Field | Type | Purpose |
| - | - | - |
| `authority` | Pubkey | Signer for every instruction after `initialize` |
| `market_count` | u32 | Incremented by each `register_market` |
| `bump` | u8 | PDA bump |
### `MarketEntry` PDA
```
seeds = [b"market", symbol_bytes]
```
`symbol_bytes` is the symbol in uppercase, zero-padded to 16 bytes. `"qqq"` and `"QQQ"` both map to `b"QQQ\0\0\0\0\0\0\0\0\0\0\0\0\0"`.
| Field | Type | Purpose |
| - | - | - |
| `symbol` | `[u8; 16]` | Uppercase ticker, zero-padded |
| `long_mint` / `short_mint` | Pubkey | L and S mints |
| `venue` | `Venue` | Venue tag. The only variant is `MeteoraDlmm` |
| `long_pool` / `short_pool` | Pubkey | Per-side pool addresses |
| `long_position` / `short_position` | Pubkey | Liquidity position addresses (default pubkey if none) |
| `is_active` | bool | Set to `true` by `register_market`. No program reads it |
| `bump` | u8 | PDA bump |
## Instructions
Every instruction except `initialize` needs the `authority` stored in `RegistryState` as signer. Any other signer fails Anchor's `has_one` check (2001).
### `initialize()`
Creates `RegistryState` once and records the signer as `authority`. The signer must be the program's upgrade authority. Pass the program account as `program` and its ProgramData account as `program_data`. A ProgramData account that doesn't belong to this program fails with `InvalidProgramData`; any other signer fails with `UnauthorizedInitializer`. Accounts: `registry`, `authority`, `program`, `program_data`, `system_program`.
### `register_market(symbol: String, params: MarketParams)`
Creates the `MarketEntry` for `symbol`, sets `is_active = true` and increments `market_count`. `MarketParams` has seven fields: `long_mint`, `short_mint`, `venue`, `long_pool`, `short_pool`, `long_position` and `short_position`. The symbol is uppercased and must be at most 16 bytes (`SymbolTooLong`). Registering a symbol twice fails because its PDA already exists. Accounts: `registry`, `market_entry`, `authority`, `system_program`.
### `update_market(symbol: String, params: MarketParams)`
Overwrites all seven `MarketParams` fields of an existing entry, so pass every field, including the unchanged ones. `symbol` and `is_active` stay as they are.
### `update_positions(symbol: String, long_position: Pubkey, short_position: Pubkey)`
Overwrites only the two position fields.
### `set_active(symbol: String, is_active: bool)`
Sets the `is_active` flag. No program checks the flag, so setting it to `false` pauses nothing.
### `transfer_authority(new_authority: Pubkey)`
Replaces `authority`.
The published IDL carries no PDA seeds and no System Program address for this program, so a client must pass every account explicitly.
## Errors
| Code | Name | Cause |
| - | - | - |
| 6000 | `SymbolTooLong` | Symbol longer than 16 bytes after uppercasing |
| 6001 | `UnauthorizedInitializer` | The `initialize` signer isn't the program's upgrade authority |
| 6002 | `InvalidProgramData` | `program_data` isn't this program's ProgramData account |
→ [Full error catalog](/reference/errors)
## TypeScript
Reading the registry works today and shows that it is empty. `provider` is set up as in [SDK setup](/build/sdk-setup), and the IDL is published in [`continuum-docs/idl`](https://github.com/continuum-markets/continuum-docs/tree/main/idl).
```typescript theme={null}
import { Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import registryIdl from "./registry.json";
const registry: any = new Program(registryIdl as any, provider);
const pda = (...seeds: Buffer[]) => PublicKey.findProgramAddressSync(seeds, registry.programId)[0];
const state = await registry.account.registryState.fetch(pda(Buffer.from("registry")));
console.log("market_count:", state.marketCount);
// Entry PDA: uppercase symbol, zero-padded to 16 bytes.
const symbol = Buffer.alloc(16);
symbol.write("QQQ");
const entry = await registry.account.marketEntry.fetchNullable(pda(Buffer.from("market"), symbol));
console.log("QQQ entry:", entry);
const all = await registry.account.marketEntry.all();
console.log("entries:", all.length);
```
Output on 10 Oct 2026:
```
market_count: 0
QQQ entry: null
entries: 0
```
## Where to get market addresses
Use [Addresses](/reference/addresses) or [Live markets](/markets/live) for mints, Market PDAs and bin-book pools. You can also derive the Market, `Clp` and `Pool` PDAs from their seeds, as shown in [IDLs and PDAs](/build/idls-and-pdas).
## See also
Every devnet market with its addresses.
Program IDs, mints, PDAs and vaults.
# Address registry
Source: https://docs.continuum.markets/reference/addresses
Every devnet address Continuum uses, in one place. Programs, mints, markets, vaults, CLP accounts and bin books, checked on chain on 10 Oct 2026.
This is the public lookup table for Continuum's devnet deployment. Every address on this page was derived from its seeds or read from its Market account. Each one was checked on devnet on 10 Oct 2026: the account exists and is owned by the program shown. The collateral vaults were checked again on 11 Oct 2026, after QQQ, SPY and XAU moved to their own vaults.
Mainnet addresses will be added when mainnet launches. Until then, all addresses on this page are **devnet only**.
## Programs
| Program | Devnet ID |
| - | - |
| `mint-redeem` | `5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4` |
| `oracle` | `5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC` |
| `clp` | `8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES` |
| `registry` | `Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau` |
| `faucet` | `9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY` |
| `trading` ([order book](/flows/order-book)) | `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA` |
| Manifest (external order book) | `MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms` |
| `governance` (scaffolded, not active) | `6es5KcjMWKGhVWrttUytYiZ3YELXe4HQyrmsMVdbVawT` |
The IDLs for the six protocol programs are published in the [docs repository](https://github.com/continuum-markets/continuum-docs/tree/main/idl).
The registry's state account (`ED8joCbjr7hnWPgmiGspQ2LNpUJGfjobZYqEHD1PWyE1`, PDA `[b"registry"]`) is initialized but holds no markets, and nothing reads it yet. Take addresses from this page instead.
## Tokens
| Token | Mint | Decimals |
| - | - | - |
| **cUSDC** (devnet collateral) | `B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE` | 6 |
Every long and short mint is a classic SPL Token mint with 6 decimals. Its mint authority is the market's Market PDA, and it has no freeze authority. The token symbols are the market symbol plus `L` or `S` (`QQQL`, `QQQS`).
| Market | Long mint | Short mint |
| - | - | - |
| QQQ | `E4xJxBPyDXXJK2uqF17cq9kch1xHCTBZ6vjMxKWKANgZ` | `BisvPibkUJqix3ZzaeHCxJ2W7KqoroFpnyf55nzFoicG` |
| SPY | `8mVLeXHpchLvt3WZRhRrAZYJ4oU3e2crVF1QEomgdDQ8` | `3n5x1QaXSSw5ysAK5zkMo1XSm52TwzTBK48RAfkeKJiu` |
| XAU | `AoufiP54693dn9HKonVBn6W75tKe8eHq5ncTwjTz6i5c` | `9XjYivtQfhDitFJugsskP6LppL7JJzANWwc7kmybpd55` |
| XAG | `VETzCeHk2mobTaUATNRBcokKE3x8YrcRyGx3Vsz4prr` | `DnHSia6WX5jYPnRkSTfu4GtbrTw2CAdLWKRmdMsWPHU3` |
| NVDA | `B8M3y6wHGFZidqYciVxGu27KYk2dLcqgBGNoUDXMVzNN` | `6n525VkawSkm6h1HxisHCpRssdbmazb1Q9S3Bi2f9emL` |
| AAPL | `7ofckkctk9wUpTn77zFv8t8yb3iBUSkuU3vmcXbaUboS` | `438bLh6shsVHXoQwtaRP56zWuAsLJK1o2u6FyRe6crYV` |
| EUR | `FLDQtZ2adGWTcawQh6jziufJejkexoPA2uYimQTqA8z9` | `z5u5UYz5jWdbycMexwdYvkw56fLY1uvx5RZW3EtxRsM` |
| MSFT | `GaGvW4zuXk84dJwmTXFYrxJaY4otHsJPasc7P5sjdCrA` | `4T1yPzWCxijyJEKiLmjfHYFNMJeiwGf2GSGuAsKwr9Fp` |
| TSLA | `4dt2EW6pNJRBup7gqxtsQF1GgNKR4RWoj5bSXuY5f1JH` | `71Mh4et7Xvf8wdQJJ3ZUk51wgyervZVdjN6A4bCkWFfG` |
| AMZN | `3vnTkSkevAJ69Vs8PFRNzF1hKvSiFbiLZJetNyqUcLZN` | `5ESabwnWACsFyDE96JNwbLkTpKyBHyNYM96PHEBvuzPc` |
| GOOGL | `42kcUPz39qudU5qv9tgU6a3E7jmiJhKs7AoXDUcarKQ8` | `AGzVgTj53MwsJd8Ke2iyibRSyVLsqdJS3DzQWsSAxgdj` |
| GBP | `HSGiyJGoAMKYmEG6cZkvnTko3B3BP6nnVCYuikBxRLSJ` | `F6dZpqSCevWEUdCvv6ARaePy5RnqkcVJBm7UubjV8fSG` |
| JPY | `4NVo5FmKT1Z1Bid3RuRa9obpYgyusrQdxByTPLw6KtsT` | `VqgP5wUX6jZKEtWQiktmip8ntGz2eKCjdJ4ttjfo93a` |
| MSTR (closed 12 Oct 2026) | `4cu47GCf1uFiLFY4M4pz5fxXaQNkFjFCSGZMryHKLx2G` | `2tQ9cyCZX4vmG9rUkonKAtW8oCrgPSZn9Z6rSbrPHT7h` |
## Market and oracle config PDAs
The Market PDA uses seeds `[b"market", symbol]` on mint-redeem. The `OracleConfig` PDA uses `[b"oracle_config", market]` on the oracle program. Mint and redeem take the `OracleConfig` as their `oracle` account, and the program checks it against `Market.oracle_address`. On devnet the keeper doesn't update `OracleConfig`, so don't read prices from it. The live price is `Market.user_twap_price` (see [Oracle](/concepts/oracle)).
| Market | Market PDA | `OracleConfig` PDA |
| - | - | - |
| QQQ | `F12ostJBgYCzsdouwz5GG1qeBCwbmhNGucPBNCZCfPG5` | `7uMgq2tpFW9FatCwAPtAjndvADCr3p8NiNFo7EZLSaV1` |
| SPY | `GT1XPJmWNxFukQXVxCmT6c9VdLdR6Da54aU9m9QGfd8b` | `CZnrCy19NmVT8JJHBE4Q7MFjkk25NaXdDguKsvqWg8uE` |
| XAU | `8HVoUTS3iNdKSCHhvhc2FNRnXjd7yHcEARD6mTFjem7b` | `H3kvpKAQbriHGr6CZWWpXreUFa9ox4pdHPAoeqYSuLt4` |
| XAG | `6vX3BNEbeem7xjiTKuMC9uMDD3hxPxo9fgM834BucH3n` | `5wW4bJZ3vLtsGVDrfCuoAUBozzNgKDdp3urSRyERD6x3` |
| NVDA | `3dadTP1Wce1CBrwxhCXor4HanjioauS7Qey3TFtpriVb` | `H3wyiSQg8BubRwEdRaHYCMVx4HMwtAYdjgqEfohJmSyd` |
| AAPL | `4ZunzWTX3XAhdhPvjsfFLaXXzZSgMD7CPr9moC1XKpSA` | `7YwnWSbwNxJhzKkt3WoAkJ7nxnzpcAFNBsQ7gyYgpEVe` |
| EUR | `CkXdq7RNCu6sCk7A3UZdBB1JNR8ESaBWqH8bdeMsUbYR` | `J4GhhLqUwbBEZ9E9C6ryGDD3GxbV9pzkWCXPrqjDhcGh` |
| MSFT | `HzCTyT5rRVUme6JugGsvnyCQ4wrsyZ6WrbX5FfaMR3Q3` | `71F1zwVKtTTTXbmpDHmF4Jg7YwxyWPY9aVvCTgQJWjKd` |
| TSLA | `HvPnCkZq2z163WFxTGCgp7V13qhrNpiZvDywAgQaPTQg` | `DSNRGdZUqkwH9HoYr7XLsMm2VZsY3sWXJNouAT85ghCM` |
| AMZN | `tEGPdiYZFyysq6YJeASdtzs6qzHUGKS96qp7XYJjBpj` | `8ZUofpK6Dj7Y8SDT7kiLawZjEeQ3eWiptLPZMum4W1p6` |
| GOOGL | `7kmfunh8gUn2h9eafmJHUP6xeR8qM6yJZVEEyRzKiXhb` | `BLWwgcpbAMADpaA1YVTc2jy7ScxmSa7G47Xze75kiyuv` |
| GBP | `HjpaEbyXJBqzxP6B7eNnbZvtaUPXRubc5bEywzfMePqK` | `BkY1KehG2HZczmu6F1WpzJ2oHfqztESQviJjx7kDoXfn` |
| JPY | `2hZtLxJtPeYjogUQFWeGYEKjJkWArEcf9iTtGkrChuKC` | `7GhyT3C7nqnK9FbvpeiGYzNNQP2AvEDnMgL7FSW9cVg1` |
| MSTR (closed 12 Oct 2026) | `66WAH37WCVp1xxSBf3gqJQB4zgQnH1JGRaFymBCSpK9Y` | `BtYkLGtZAh9ifwiV2pyuBFMEwFFC5zUX22HfYpajAt84` |
OIL (withdrawn 30 Aug 2026) and VXX (delisted June 2026) are not on this page. Don't integrate against their accounts.
## Collateral vaults
Every market keeps its collateral in its own vault: the cUSDC associated token account (ATA) of the Market PDA.
| Market | Collateral vault |
| - | - |
| QQQ | `CGEMQpzitYvEnWE7sggqD4aBMpTmXhckQPYMhzhy9oJm` |
| SPY | `DgL6UTux2gjRubznEnEQ1WprjajW5KYaegBEKgSncgPc` |
| XAU | `2Z9W8e64czfRDu7DoJmZbhZGbm6W7ERuUnXS7iWzU2xx` |
| XAG | `2Gy5SFgx7uV7ybuVR9DmbD5921z8iJrrEVDMfo5XqRFQ` |
| NVDA | `2rJ26he3T7VjjXVYoJRanx15pqWSERxHQCfSqh71uWMK` |
| AAPL | `2qsaxGmZEBgh8WUcJisMb4pejMdAQJT6X8HD2VT3FMaD` |
| EUR | `C7Ur9siuwWB1MYkEyMQWE347jUxue8Xz1fvLfcnWguC5` |
| MSFT | `6p6HP29C4yaKCg3NaK62RfMpFccu6ja8HSS13BC4nVGE` |
| TSLA | `2kiAUxgp9UsTtLFyZySAT6vihc2dDfH23jKmiStiEYWu` |
| AMZN | `125Emh8gbBitA7h9PTwkSzLULZinA1GhvSPztZrgk9eT` |
| GOOGL | `7NHs4FXpKdtB41Z5LdYkCq3qTmCi6JG2j6eaA8nmNToX` |
| GBP | `3gtGoZTrWbCVdD6vfwz2614aFwvS2JzgQmENadfR8gkF` |
| JPY | `F78vUS2GfNgn2NC41bLJSPCb2wZaPoGGGDFYfxhoLGfA` |
| MSTR (closed 12 Oct 2026) | `HKStxHs4kWPqHWWN1fkcSCZ6n6QHvbafwcURV6LTQ3Ju` |
`Market.total_collateral` is each market's own ledger figure, and the solvency checks read it. On 11 Oct 2026 every vault's token balance equaled its market's ledger. A plain token transfer into a vault raises its balance without changing the ledger, so read the ledger for solvency.
QQQ, SPY and XAU used to share a unified vault, a PDA with seeds `[b"unified_vault"]` on mint-redeem whose token-account owner is the `CollateralAuthority` PDA (`[b"collateral_authority"]`). They moved to their own vaults on 11 Oct 2026, and the unified vault now holds 0 cUSDC. Both accounts still exist on chain, but since that day's upgrade mint-redeem has no code that reads or signs for either. `redeem_paired` and six other instructions keep a `collateral_authority` slot that the program ignores; pass mint-redeem's program ID there.
| | Address |
| - | - |
| Unified vault (retired) | `5a51yurKcqxy6rVAV1ykif5fDzhHdjgZs3EL7yncLrp8` |
| `CollateralAuthority` PDA | `7m3ngNMqhSj6FtWLe87CW6L1fhuqSvu4eqjof6qnMcA3` |
## Fee recipient
Mint and redeem take a `devTokenAccount`, which must be a cUSDC account owned by `Market.fee_recipient`. It receives the dev tax (10% of each fee). Read `fee_recipient` from the Market account rather than hard-coding it. Since 12 Oct 2026 every live market pays the dev tax to the treasury:
| Markets | `fee_recipient` | Its cUSDC ATA |
| - | - | - |
| All 13 live markets | `BcGR8kuGCiMhwonyztXZHEZtjJXxXVRgjwbn58bUfXtw` (GlobalClp PDA) | `5GSTKN4u5LfropcnQQoqFht48JMaF1SzZzBcgDNL5VKy` (the treasury) |
| MSTR (closed) | `8xhPK1rGK4neoKo4fT8FxHQDZRiCnSYW9hvEdaVTxCni` | `GB5Jqotcc27fHYXQy3VdAsbQ4NxaquQti3kZShSC2vmh` |
## CLP accounts
| | Seeds (clp program) | Address |
| - | - | - |
| GlobalClp PDA | `[b"global_clp"]` | `BcGR8kuGCiMhwonyztXZHEZtjJXxXVRgjwbn58bUfXtw` |
| GlobalClp vault (the treasury) | cUSDC ATA of the GlobalClp PDA | `5GSTKN4u5LfropcnQQoqFht48JMaF1SzZzBcgDNL5VKy` |
| `TreasuryPolicy` PDA | `[b"treasury_policy"]` | `58JeHAcMAAyEZWusVqmvoFNVqh37cTkmfbbP6bMHExWR` |
| `TreasuryYield` PDA (the treasury's lending ledger) | `[b"treasury_yield"]` | `6YrCADSTyTJPNrVhrZz5S2wTzBQSPM5fGUrGSpLTBJgY` |
| `RevenueAttribution` PDA | `[b"revenue_attribution"]` | `A8mK2pnagabEcnkSiWieEZyQjjNWFYm1Z91dUVMXe6a4` |
`pool_swap` takes the `RevenueAttribution` PDA after `token_program`, followed by the market's mint-redeem `Market` account, which comes last.
Every market has its own `Clp` PDA (seeds `[b"clp", market]`), including the four markets added in June 2026. Its vault is the cUSDC ATA of the `Clp` PDA. Each `Clp` stores an OI cap of 250,000 cUSDC, which nothing enforces yet.
| Market | `Clp` PDA | CLP vault |
| - | - | - |
| QQQ | `EEDKqU3C4wgWQBcgFagHTqEAjKNMCV4eVYyaUt9swKHc` | `8Doh7uAU95RkqZDx66C1643Q8xJeg4T8eYKMak6ac1En` |
| SPY | `F4eAvoAd2tUnpDz7dSqj2d4eCBzHLHYTvPmiwHNe1nnX` | `J5cj2Ba1bMsgK4qk2uis6asL6hUSwAd53kYepteBG94B` |
| XAU | `DdUuCiNzv5A3XZJk5jj4vY8zGS1avcrcMu1MuR4J65BA` | `C6X4VEBggnwdC6Ju5tBMm8wxRiFekRZQrWSeX5Ja2kVn` |
| XAG | `DKnAJi6m98Z9521tAQ7XbGFrxBsrnnmMXqfQSDLfxPk2` | `C6ne1mYqN5CKkgBMnV83q9yDv9RLDZdosZaavWzJp5X7` |
| NVDA | `HkyqDQ55Mhz3pHDMzeRVtiZBEwVQfz2yoVZ3e8cKJC7H` | `BupGi5KavHSB8bVHPzG4hnq7ZXRJySe7d8zAq1R4KpP4` |
| AAPL | `JAF3WYJGRA5aLofT1NEKG7Tzr5XWa4vH8mtaccc9K8VJ` | `AAwyDwCH4uXhRzAHfnLfVfMv4RncSXzt8RXXgFmPMVTR` |
| EUR | `Esd4RB9qMfcmH8AAtiGgLLBXskZZ4xkcBExPGGTQPZj2` | `6LFwtV47WzjFQc86wigXUYVxECBwCmNt8LpRJanLJyNd` |
| MSFT | `3LFUK62KWYe5nz8Uhz4EbyTDpSZTmksMiFhMpBCJge6z` | `481HdR4j6EvhLGMsN6KvvfTPvEHG2pqEbyLVz5rmrJnj` |
| TSLA | `8B8a7fqBdgq2DQgiNdN3cQhHJda6pNJykh3DSt5EN4jB` | `HS2DhzxvtKTF432dMT93Tu7oto7mWGGJNQ5kAQVqnnrQ` |
| AMZN | `3S1RWBt4X4GW5Q9tvmZetgUrrbKDxwWoaFwGT1fFeKGS` | `F3VjE6jt47zRyVqGfHmiet2kVKd7gKh3NLNS3xrb5tPB` |
| GOOGL | `HRo8q96iUnTP8Secf7eqYfADkNVokLvfy4eg7DjpkqrN` | `6qe3HfoWrC2g7k8URTDrAJYVreCieQGZA8LBZZByoqEM` |
| GBP | `4gskdJfMfv6sCPqyWErXayUdzJBPLg5GyiZ6jSJ4EB8J` | `5rfH9Er5GFHGcK4wHvga8JRRCS1EAqJsWcnGWMJMVDB5` |
| JPY | `HKzUKzNyHDyC5AgEYFFscE3PETBB3SfrfCKnt1hGLE1K` | `8PVXUQ6VuuYor9PH7vew3sY35civX2YSJH1jgCXEsaXU` |
| MSTR (closed 12 Oct 2026) | `vR53HxPcBUkjuVzsLYofMGU4GKhy5QgTaJZMBDzjU7n` | `Gs5TSSLFRAriH27zXNy7WrKXzSwcFbJCTKnC7kqb7Qs4` |
## Bin books
Each market has one `Pool` PDA per side, with seeds `[b"pool", market, [side]]` on the clp program. Side 0 is the long book (L against cUSDC) and side 1 is the short book (S against cUSDC). A pool's `synthVault` and `usdcVault` are the pool PDA's ATAs for its synth mint and for cUSDC.
| Market | Long book (side 0) | Short book (side 1) |
| - | - | - |
| QQQ | `DbPryNwJ2ThJPTzb7UMBoEQhR2QEqcHxenGA8JCqG9Qr` | `HUudWC8ykMCZg3CNs3TPhRmLczdrPVwmXXSYvvKAxKbB` |
| SPY | `4UZmmZYrRfffQmjq2iEaNgkit72s8VS5cnhK5GeuKSt8` | `4JoXbxzAAeAWsQgTYXgWAvRqdR1RU2Tb3ywm2FNLKjAB` |
| XAU | `5rEH7sYUS7kuqNiLZmCiQGwQLmQ1qqogRPavT9csKmHL` | `CW8nvKYPi7xgRtimCk1Gj4Db6bbXdqXngdDMxDA6rLHT` |
| XAG | `EVPrpQiPPXwDorKPrSop36WFg53wTujdGyFFb6j8VM9j` | `DsiYTmZcTSq3i4NfBfMFJBST7Kyess73yjVcRNJ8m5Fw` |
| NVDA | `6oBYXyw6bQqySqEEXdkYGtZxzBN2VXd8RR66sf9z7Wh4` | `8evyJuf3b6g3x8DHSXHjQYBvXFBHmyDEyycRpQ183mUn` |
| AAPL | `DPFt4A1hGeaCWh8c8mVqT1JNG8vGUTqKsMejVmXVkfaE` | `6v9KZZY4uXaNHkmq2JSJHcbuntCCrXkCyrz8WZD3pGef` |
| EUR | `CSpjyN6YkFkhbLKux47pnw4KeWQAtWNgu6yBHTn7SsGe` | `8QJiubkx3xWxNGXrPSUjTsaTss6TcNBiMNT5xzrByZfp` |
| MSFT | `CZPtcyprVZNTQYMfCoV7NyZM8LKyfBeA8c1Lk5NoKmEq` | `7aDvEwdSFtHyknZ2hc6fwnhnhgFMDp36U4fJEDX9gh7T` |
| TSLA | `BQcoDb7XfwYtnugVcRfW6VmigAGpW7dGBtgwQNLHPqGY` | `BHnZ49UvLUJokEpdJyXM8gAZgonCBCFoYfvVAx5Mj75n` |
| AMZN | `CHvNFzHqHbhT9BdinSBhww3CF4bMepL3zF2vLDUKTkDi` | `CZV1NDMVX8BPiKKTXSZsgS1VgahQDo1PXtsZdPB2tMGW` |
| GOOGL | `82RWToVd47PC7kd3ScNM4BwAiascY82oAcu4e4NhStvp` | `6CVRNQPruyE8LLQvdfiVwp6uEiqxcRYjCTD7wU9a2DS1` |
| GBP | `3e59vEqzQJasUiBHstdCHRKh9KJTtG83w3Mo5W1ASHDw` | `B1UyrycWcxpV9DPTnEhKLf1zfeomnJVVriST7TdKRBtR` |
| JPY | `NyL2zCi4am2Kiofr6NfNcVCyf23rmcnsqVgNaZPj7mf` | `FsDgk3EkBJbdqEP2C9uyh86qdjy4KpYpg1PTytW86FY4` |
| MSTR (closed 12 Oct 2026) | `G9xCpYdpCwyXw9Y5z2aMddDbVVvj1iy87RRozaAoY9LK` | `TS4njAZnRctsppgdTiHptAUNitoY6FjFMkXjqHKLQU7` |
The books charge no swap fee. As of 10 Oct 2026 the core steps run from 10 to 50 bps and the wing steps from 200 to 700 bps. Every book has 12 bid bins and 12 ask bins, and the first 3 on each side are core bins. The XAU long book is the exception, with 6 a side. Per-market geometry is on [Markets](/concepts/markets#live-markets).
## Derive addresses from a symbol
Every PDA above comes from the market symbol. The mints aren't PDAs, so pass them in from the table above or read them from the Market account (`longMint`, `shortMint`). The same goes for `collateralVault` and `feeRecipient`.
```typescript theme={null}
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
const MINT_REDEEM = new PublicKey("5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4");
const CLP = new PublicKey("8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES");
const ORACLE = new PublicKey("5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC");
const CUSDC = new PublicKey("B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE");
const pda = (seeds: Buffer[], programId: PublicKey) =>
PublicKey.findProgramAddressSync(seeds, programId)[0];
export function marketAddresses(symbol: string, longMint: PublicKey, shortMint: PublicKey) {
const market = pda([Buffer.from("market"), Buffer.from(symbol)], MINT_REDEEM);
const clp = pda([Buffer.from("clp"), market.toBuffer()], CLP);
const pool = (side: number, synthMint: PublicKey) => {
const address = pda([Buffer.from("pool"), market.toBuffer(), Buffer.from([side])], CLP);
return {
address,
synthVault: getAssociatedTokenAddressSync(synthMint, address, true),
usdcVault: getAssociatedTokenAddressSync(CUSDC, address, true),
};
};
return {
market,
oracle: pda([Buffer.from("oracle_config"), market.toBuffer()], ORACLE),
clp,
clpVault: getAssociatedTokenAddressSync(CUSDC, clp, true),
longPool: pool(0, longMint),
shortPool: pool(1, shortMint),
};
}
// marketAddresses("XAU", xauLongMint, xauShortMint).longPool.address
// => 5rEH7sYUS7kuqNiLZmCiQGwQLmQ1qqogRPavT9csKmHL
```
## External program IDs
| Program | ID |
| - | - |
| SPL Token | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` |
| Associated Token | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` |
| Metaplex Token Metadata | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` |
## RPC endpoints
| Cluster | Public endpoint |
| - | - |
| **Devnet** | `https://api.devnet.solana.com` |
| Mainnet (when launched) | `https://api.mainnet-beta.solana.com` |
For production, use a private RPC (Helius, Triton, QuickNode, Alchemy).
## See also
Per-market detail, fees and collateral.
What each program does.
# Error catalog
Source: https://docs.continuum.markets/reference/errors
Every custom error code of the six programs, generated from the published IDLs, with causes checked against the program source.
Anchor numbers each program's custom errors from 6000, so one code means different things in different programs. Always read a code together with the program ID that raised it. Wallets and RPC errors often show the code in hex: `custom program error: 0x177c` is 6012.
The tables are generated from the [published IDLs](/build/idls-and-pdas#idl-files), so the codes match the deployed programs. The Message column is the program's own text, as it appears in transaction logs. A cause is given where it was checked against the program source.
## mint-redeem
Program ID (devnet): `5MBjhNUUguLTPNR5WG6YBUUw7vUcxQ14ARw3NsS3rKu4`
Codes 6029 and 6030 are reserved placeholders: the γ rate caps they reported were removed, and the codes stay so that later codes keep their numbers. Codes 6048 to 6052 belonged to the unified vault, which the upgrade of 11 Oct 2026 removed. They are no longer returned and stay for the same reason.
| Code | Hex | Name | Message | Cause and fix |
| - | - | - | - | - |
| 6000 | `0x1770` | `MarketNotActive` | `Market is not active` | `Market.is_active` is false. Wait for the operator to reactivate the market. |
| 6001 | `0x1771` | `InvalidAmount` | `Invalid amount` | A zero amount, a redeem with `l_amount` or `s_amount` at 0, more than the outstanding supply, or a token account whose mint doesn't match the market. Keeper price writes also raise it for a move over 25% or a Stress exit that skips Recovery. It also covers a `rebalance_collateral` from a market to itself or between two markets that share a vault, and a wrong vault account on the collateral instructions. |
| 6002 | `0x1772` | `InsufficientCollateral` | `Insufficient collateral in vault` | A keeper single-side redeem would leave the market under 100%, or a withdrawal would take it below its target ratio. `rebalance_collateral` raises it when the source has nothing above its target, and `deploy_collateral_to_yield` when the vault holds less than the amount. User mint and redeem don't raise it. |
| 6003 | `0x1773` | `InvalidOracle` | `Invalid oracle account` | At setup or on a Normal-state read: the oracle account isn't the market's `OracleConfig` PDA (`Continuum`) or doesn't parse as a Pyth feed. A wrong `oracle` on mint or redeem fails Anchor's address check (2012) instead. |
| 6004 | `0x1774` | `UnsupportedOracleType` | `Unsupported oracle type` | `oracle_type` is `Switchboard`. Only `Pyth` and `Continuum` are accepted. |
| 6005 | `0x1775` | `OraclePriceUnavailable` | `Oracle price unavailable` | No usable price: a Normal-state oracle read failed (Pyth older than 60 s, or confidence over 2% of price), the confidence band is as wide as the price, or a keeper write carried a rejected source timestamp. |
| 6006 | `0x1776` | `MathOverflow` | `Math overflow` | Arithmetic overflow. Not expected in normal use. |
| 6007 | `0x1777` | `SymbolTooLong` | `Asset symbol too long` | `initialize_market` symbol over 10 bytes, or `create_token_metadata` name over 32, symbol over 10 or URI over 200 bytes. |
| 6008 | `0x1778` | `FeeTooHigh` | `Fee too high (max 10%)` | A mint or redeem fee above 1,000 bps, or `dev_tax_pct` above 25. |
| 6009 | `0x1779` | `MintNotAllowedInStress` | `Minting not allowed during Stress mode` | The market is in `Stress` and `allow_mint_in_stress` is false (the default, and the setting on every live market). Use the bin book. |
| 6010 | `0x177a` | `RedeemNotAllowedInStress` | `Redeeming not allowed during Stress mode` | The market is in `Stress` and `allow_redeem_in_stress` is false (the default, and the setting on every live market). Use the bin book. |
| 6011 | `0x177b` | `ExceedsStressMintCap` | `Amount exceeds stress mode mint cap` | Stress only, when minting is allowed and `stress_mint_cap` is non-zero: the deposit is above the cap. Live markets block mint in Stress, so this can't fire today. |
| 6012 | `0x177c` | `TwapNotAvailable` | `TWAP price not available - keeper has not updated` | The market price is 0 or more than 300 s old. The keeper stops pushing while the underlying market is closed, so mint and redeem revert until it reopens. Trade on the bin book meanwhile. |
| 6013 | `0x177d` | `AlreadyEnabled` | `Multi-collateral mode already enabled` | `enable_multi_collateral` on a market where it is already on. |
| 6014 | `0x177e` | `MultiCollateralNotEnabled` | `Multi-collateral mode not enabled` | `add_collateral_vault` before `enable_multi_collateral`. |
| 6015 | `0x177f` | `MaxCollateralTypesExceeded` | `Maximum number of collateral types exceeded` | A sixth `collateral_vaults` entry. The limit is 5. |
| 6016 | `0x1780` | `InvalidWeight` | `Invalid weight - must be between 1 and 10000` | A collateral weight of 0 or above 10,000 bps. |
| 6017 | `0x1781` | `CollateralAlreadyExists` | `Collateral type already exists` | `add_collateral_vault` with a mint that is already listed. |
| 6018 | `0x1782` | `CollateralNotFound` | `Collateral type not found` | `update_collateral_vault` with a mint that isn't listed. |
| 6019 | `0x1783` | `BelowMinimum` | `Amount below minimum ($10)` | A mint deposit under 10 cUSDC (`10_000_000` base units), or a redeem worth under 10 cUSDC at the quoted NAV. |
| 6020 | `0x1784` | `UnauthorizedKeeper` | `Unauthorized keeper` | The signer isn't `market.keeper_authority`, `redeem_single_for_vault` wasn't signed by the market's CLP PDA, or `realloc_market` wasn't signed by `market.authority`. |
| 6021 | `0x1785` | `Unauthorized` | `Unauthorized: not the market authority` | `devTokenAccount` isn't owned by `market.feeRecipient`, the fee waiver belongs to another wallet, or `permissionless_burn` isn't signed by the account owner. `fund_collateral` and `fund_collateral_from` raise it for a signer other than `market.authority`, and `fund_collateral_from` also when `funder` doesn't own `funder_collateral`. Other admin calls with the wrong signer usually fail with Anchor's `ConstraintHasOne` (2001) instead. |
| 6022 | `0x1786` | `InsufficientLiquidCollateral` | `Insufficient liquid collateral; recall yield deployment first` | The payout is larger than this market's liquid share (`total_collateral - deployed_to_yield`) or the vault balance. `rebalance_collateral` raises it when the source has no liquid collateral. For a redeem, queue it with `request_redemption` (see [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet)) or retry after the keeper recalls from lending. |
| 6023 | `0x1787` | `ExceedsYieldCeiling` | `Deployment would exceed yield ceiling` | A yield deployment above `yield_ceiling_bps` of `total_collateral`, or a ceiling above 10,000 bps. |
| 6024 | `0x1788` | `YieldManagementDisabled` | `Collateral yield management is disabled for this market` | Yield deployment or harvest while `yield_enabled` is false. It is true on every live market as of 11 Oct 2026, and `initialize_market` sets it to true on new markets. |
| 6025 | `0x1789` | `RecallExceedsDeployed` | `Recall amount exceeds deployed_to_yield balance` | A recall or impairment larger than `deployed_to_yield`. |
| 6026 | `0x178a` | `SingleSideMintStressFloor` | `Single-side mint would leave the vault under-collateralized under an adverse NAV shock` | `keeper_mint_single` would leave the market's collateral ratio below the lower of its pre-trade ratio and 100%. |
| 6027 | `0x178b` | `GammaMustRatchetDown` | `s_gamma_index may only ratchet down; raising the short decay scalar is rejected` | `set_s_gamma_index` with a value above the current γ. |
| 6028 | `0x178c` | `GammaOutOfRange` | `s_gamma_index is outside [GAMMA_FLOOR, GAMMA_ONE]; out-of-range writes are rejected, not clamped` | `set_s_gamma_index` outside 0.5 to 1.0 (`500_000_000` to `1_000_000_000`). Out-of-range values are rejected, not clamped. |
| 6029 | `0x178d` | `GammaStepTooLarge` | `unused (γ per-transaction rate cap removed)` | Reserved placeholder. The per-transaction γ cap was removed and this code is never raised; it stays so later codes don't shift. |
| 6030 | `0x178e` | `GammaDailyCapExceeded` | `unused (γ per-day rate cap removed)` | Reserved placeholder. The per-day γ cap was removed and this code is never raised; it stays so later codes don't shift. |
| 6031 | `0x178f` | `GammaEpochMismatch` | `γ epoch mismatch: this post was computed against a different epoch (a re-anchor landed in between)` | `expected_epoch` doesn't match `gamma_epoch`: a re-anchor landed after the keeper computed the write. |
| 6032 | `0x1790` | `InvalidMarketAccount` | `Account is not a Market owned by this program (bad owner, discriminator, or length)` | `realloc_market` on an account that isn't a mint-redeem `Market` (wrong owner, too short, or wrong discriminator). |
| 6033 | `0x1791` | `ReanchorNoDecay` | `Nothing to re-anchor: γ is already 1.0 (or unset)` | `reanchor_gamma` while γ is already 1.0 (or unset). |
| 6034 | `0x1792` | `ReanchorTooSoon` | `Re-anchor is inside the minimum interval since the last one` | `reanchor_gamma` less than 14 days after the previous one. |
| 6035 | `0x1793` | `ReanchorPrecisionFloor` | `Re-anchor would leave the short anchor without enough precision to price against` | The folded `initial_s_price` would drop below 1,000 (0.001 cUSDC) or below 1/10,000 of its genesis value. |
| 6036 | `0x1794` | `ReanchorNotValueConserving` | `Re-anchor failed its value-conservation invariant and was rejected` | The fold would change `initial_l_price × initial_s_price × γ` by more than rounding. |
| 6037 | `0x1795` | `ReanchorBadState` | `Market is in a degenerate state (zero anchor) and cannot be re-anchored` | `initial_l_price` or `initial_s_price` is 0. |
| 6038 | `0x1796` | `MarketNotEmpty` | `Cannot close a market that still has outstanding supply or collateral` | `close_market_admin` while L or S supply, `total_collateral` or `deployed_to_yield` is non-zero. |
| 6039 | `0x1797` | `CorporateActionBadRatio` | `Corporate-action ratio has a zero term` | `ratio_num` or `ratio_den` is 0. |
| 6040 | `0x1798` | `CorporateActionNoOp` | `Corporate-action ratio is 1:1; nothing to apply` | `ratio_num` equals `ratio_den`. |
| 6041 | `0x1799` | `CorporateActionRatioOutOfRange` | `Corporate-action ratio is implausibly large (fat-finger guard)` | A ratio beyond 1,000:1 in either direction. |
| 6042 | `0x179a` | `CorporateActionPrecisionExhausted` | `Cumulative corporate-action ratio would exceed its precision budget (cash dividends are deliberately out of scope)` | The reduced cumulative ratio would have a term above 1,000,000,000. |
| 6043 | `0x179b` | `PriceAdjustOverflow` | `Oracle price could not be mapped into genesis share units (corporate-action adjustment overflowed)` | Applying `price_adjust_num / price_adjust_den` to a price overflowed or truncated it to 0. |
| 6044 | `0x179c` | `CorporateActionEffectiveTooFarAhead` | `Corporate-action effective_ts is implausibly far ahead; check the unit (seconds, not milliseconds)` | `effective_ts` more than 400 days ahead, usually milliseconds passed as seconds. |
| 6045 | `0x179d` | `CorporateActionPendingExists` | `A future corporate action is already pending; wait until it takes effect` | Another action is armed and hasn't taken effect yet. |
| 6046 | `0x179e` | `UnauthorizedInitializer` | `Market initialization requires the program upgrade authority` | `initialize_market` signed by anyone other than the mint-redeem program's upgrade authority. Only the admin can list a market. |
| 6047 | `0x179f` | `InvalidProgramData` | `Market initialization requires this program's ProgramData account` | `initialize_market` passed a `program_data` account that is not this program's ProgramData. |
| 6048 | `0x17a0` | `UnifiedVaultRetired` | `The unified vault is retired; migrate_to_unified_vault is disabled` | No longer returned. `migrate_to_unified_vault` was removed on 11 Oct 2026. |
| 6049 | `0x17a1` | `NotOnUnifiedVault` | `Market collateral is not held in the unified vault` | No longer returned. `exit_unified_vault` and `sweep_unified_surplus` were removed on 11 Oct 2026. |
| 6050 | `0x17a2` | `MixedVaultRebalance` | `Cannot rebalance between the unified vault and a per-market vault; exit the unified vault first` | No longer returned. A `rebalance_collateral` between two markets that share a vault now fails with `InvalidAmount` (6001). |
| 6051 | `0x17a3` | `UnauthorizedSweep` | `Sweeping the unified vault requires the program upgrade authority` | No longer returned. `sweep_unified_surplus` was removed on 11 Oct 2026. |
| 6052 | `0x17a4` | `SweepExceedsSurplus` | `Sweep would take collateral still attributed to a market on the unified vault` | No longer returned. `sweep_unified_surplus` was removed on 11 Oct 2026. |
| 6053 | `0x17a5` | `NothingToSettle` | `No part of this redemption request fits in the vault's cash now` | `settle_redemption` on a request with nothing left, or when the vault's cash pays out nothing for any part of it. The request stays open; the keeper retries as cash arrives, and the owner can `cancel_redemption`. |
| 6054 | `0x17a6` | `AdvanceCapExceeded` | `Advance would exceed this market's advance cap` | `receive_advance` (or the CLP's `advance_to_market`) would push the outstanding advance above `max_advance_bps` of `total_collateral`. |
| 6055 | `0x17a7` | `AdvanceRepayExceedsOutstanding` | `Repayment exceeds the outstanding advance` | `repay_advance` or `write_off_advance` for more than is outstanding. |
| 6056 | `0x17a8` | `AdvanceWouldTakeOwnLiquidity` | `Repayment would take the market's own liquid collateral` | `repay_advance` would leave the vault with less than the market's own liquid collateral. Only the advance's own cash can be repaid. |
| 6057 | `0x17a9` | `AdvanceLenderLocked` | `The advance lender can change only while nothing is outstanding` | `configure_market_advance` with a new lender while an advance is outstanding. |
| 6058 | `0x17aa` | `AdvanceOutstanding` | `A treasury advance is outstanding: write it off or repay it first` | `mark_yield_impairment` or `close_market_admin` while an advance is outstanding. |
| 6059 | `0x17ab` | `InvalidAdvanceLender` | `The advance lender must be the CLP treasury vault (GlobalClp's cUSDC account)` | `configure_market_advance` with a lender that isn't the `GlobalClp` PDA's cUSDC account. |
## clp
Program ID (devnet): `8xauDRjw9XRyk4FE3hW1JKjD8nC87gfr59Xig1dJqLES`
Codes 6003 to 6016 and 6044 are swap errors from `pool_swap` and the committed-order instructions. `clp_swap`, retired on 11 Oct 2026, always fails with 6003. 6031 and 6032 come only from the legacy Meteora instructions, which the live bin books don't use.
| Code | Hex | Name | Message | Cause and fix |
| - | - | - | - | - |
| 6000 | `0x1770` | `Unauthorized` | `Unauthorized` | The signer isn't the required authority (`clp.authority`, `pool.authority`, `GlobalClp.authority` or the treasury guardian), or `deposit_profit`'s admin token account isn't owned by `GlobalClp.admin_wallet`. |
| 6001 | `0x1771` | `WithdrawalLocked` | `Withdrawal locked for 7 days after deposit` | Never raised. |
| 6002 | `0x1772` | `InvalidAmount` | `Invalid amount` | A zero amount, or out-of-range pool or book parameters. |
| 6003 | `0x1773` | `SwapDisabled` | `Swap path disabled (configure_swap enabled=true)` | Every `clp_swap`: the instant oracle swap was retired on 11 Oct 2026, and the instruction now only returns this error. Also `pool_swap` on a book that isn't `enabled` or is `frozen`, and `commit_swap` on a market whose swap guards are off in `configure_swap`. Since 11 Oct 2026 the guards are on for every live market. |
| 6004 | `0x1774` | `SwapFrozen` | `Swaps frozen (circuit breaker)` | `commit_swap` or a settlement while the market's swap guards are frozen. |
| 6005 | `0x1775` | `InvalidSide` | `Invalid swap side/direction` | `side` or `direction` isn't 0 or 1. |
| 6006 | `0x1776` | `SwapAccountMismatch` | `Swap account mismatch (mint/owner)` | A token account's mint or owner doesn't match the pool, market or order, or the pool or CLP account belongs to another market. |
| 6007 | `0x1777` | `MarketInactive` | `Market inactive` | `commit_swap` on a market with `is_active` false. |
| 6008 | `0x1778` | `SlippageExceeded` | `Slippage exceeded (output below min_out)` | The fill is below `min_out` (`pool_swap`). A committed order that misses `min_out` is refunded at settlement instead. |
| 6009 | `0x1779` | `OrderExpired` | `Order expired; use cancel_swap to refund` | `settle_swap` after the 180 s TTL. The keeper refunds the order about 5 s after expiry, and anyone can send `cancel_swap` to return the escrow to its owner. |
| 6010 | `0x177a` | `OrderNotExpired` | `Order not yet expired; wait for settlement or the TTL` | `cancel_swap` before the 180 s TTL has passed. Nobody can cancel an order before it expires. |
| 6011 | `0x177b` | `NoFreshOraclePush` | `No oracle push strictly newer than the commit yet` | `settle_swap` before a keeper price push newer than the commit. Retry after the next push. |
| 6012 | `0x177c` | `OracleStale` | `Oracle/TWAP stale` | `pool_swap` on a book with no posted price, or `settle_swap` with a price older than the market's `max_staleness_secs`. A committed order stays pending for a later push. |
| 6013 | `0x177d` | `OracleConfidenceTooWide` | `Oracle confidence too wide` | `settle_swap` with confidence wider than `max_confidence_bps` of the price. The limit is 5,000 bps on every market with guards on, so in practice it doesn't fire. |
| 6014 | `0x177e` | `OracleDeviationTooLarge` | `Oracle deviation too large` | `settle_swap` with a price more than `max_deviation_bps` from the last accepted price. The limit is 10,000 bps on every market with guards on, which turns the check off; the price write itself refuses a move of more than 25%. |
| 6015 | `0x177f` | `WindowCapExceeded` | `Swap window notional cap exceeded` | A `pool_swap` would push the market's rolling notional total past `window_cap_usd` for the current `window_secs`. Committed fills don't count. |
| 6016 | `0x1780` | `CollateralFloorBreach` | `Collateral floor breach` | `settle_swap` while the market's collateral is below `collateral_floor_bps` of its liability at NAV, 100% on every market with guards on. The order stays pending and fills at a later push once the keeper has topped the market up from the treasury. |
| 6017 | `0x1781` | `InsufficientShares` | `Insufficient LP shares` | Never raised. |
| 6018 | `0x1782` | `InsufficientFunds` | `Insufficient clp funds` | The book or CLP inventory can't fill the order (zero output or a short balance), or an operator transfer exceeds a vault balance. |
| 6019 | `0x1783` | `InsufficientLiquidFunds` | `Insufficient liquid funds in vault. Wait for keeper to rebalance or perform forced DEX liquidation.` | `admin_withdraw` or `backstop_market` above the global vault's balance. |
| 6020 | `0x1784` | `OICapExceeded` | `OI cap exceeded` | Raised only by `update_oi`, which nothing calls. The OI cap is stored but not enforced. |
| 6021 | `0x1785` | `MathOverflow` | `Math overflow` | Arithmetic overflow. Not expected in normal use. |
| 6022 | `0x1786` | `InsufficientBuffer` | `Insufficient protocol buffer` | `absorb_loss` larger than the protocol buffer. |
| 6023 | `0x1787` | `HardBoundBreach` | `Hard inventory bound breached - operations restricted` | Never raised. |
| 6024 | `0x1788` | `YieldStrategyDisabled` | `Yield strategy is disabled` | `record_deployment` while the market's yield strategy is disabled. |
| 6025 | `0x1789` | `ExceedsMaxDeployment` | `Deployment exceeds maximum allowed for current risk state` | `record_deployment` above the deployment share allowed in the current risk state. |
| 6026 | `0x178a` | `MaxDeploymentsExceeded` | `Maximum number of yield deployments reached` | `allocate_to_market` when `GlobalClp.market_allocations` already holds 10 markets, or `record_deployment` with a full deployment list. |
| 6027 | `0x178b` | `DeploymentNotFound` | `Deployment not found` | `record_harvest` or `record_withdrawal` for a protocol with no recorded deployment. |
| 6028 | `0x178c` | `InsufficientDeployment` | `Insufficient deployment balance` | A withdrawal or recall larger than the recorded deployment. |
| 6029 | `0x178d` | `DeploymentTooHigh` | `Deployment percentage too high` | `configure_yield_strategy` with a deployment share out of range. |
| 6030 | `0x178e` | `OracleReadError` | `Could not read oracle account data` | The `oracle_config` account passed to a yield instruction is too short to read. |
| 6031 | `0x178f` | `InvalidPool` | `Pool address does not match configured Meteora pool` | Legacy Meteora instructions only: the pool isn't the configured Meteora pool. |
| 6032 | `0x1790` | `PoolNotConfigured` | `Meteora pool not configured; run configure_meteora_pools first` | Legacy Meteora instructions only: `configure_meteora_pools` hasn't run. |
| 6033 | `0x1791` | `AdminWithdrawCapExceeded` | `Admin withdrawal would exceed the 24h cap of $100K; wait for window to roll or rotate authority if attack is suspected` | No longer returned. It reported the old 24 h cap on `admin_withdraw`, which the `TreasuryPolicy` token bucket replaced on 11 Oct 2026. The code stays so later codes keep their numbers. |
| 6034 | `0x1792` | `InvalidPosition` | `Position address does not match stored Meteora position` | A legacy Meteora position mismatch, or a wrong market, token account or mint passed to `clp_burn_excess`. |
| 6035 | `0x1793` | `InvalidAccountData` | `Account data too small for operation` | Never raised. |
| 6036 | `0x1794` | `InvalidVault` | `Token account is not the expected protocol-owned vault` | A token account has the wrong owner or mint: the global vault must be owned by the `GlobalClp` PDA, and a market's CLP vault and its L and S accounts by that market's `Clp` PDA. `vault_mint_pairs`, `vault_redeem_pairs`, `vault_redeem_single`, `allocate_to_market`, `return_to_global`, the treasury instructions and the legacy Meteora payouts check it. |
| 6037 | `0x1795` | `TreasuryFrozen` | `Treasury withdrawals are frozen` | `admin_withdraw` while the treasury is frozen. Only the guardian can unfreeze it. |
| 6038 | `0x1796` | `TreasuryAllowanceExceeded` | `Withdrawal exceeds the accrued treasury allowance` | `admin_withdraw` above the allowance. Wait for it to refill at `rate_per_sec`, up to `burst`. |
| 6039 | `0x1797` | `InvalidTreasuryParams` | `Invalid treasury limits: rate and burst must be > 0, delay within 0..=30 days, guardian set` | `init_treasury_policy` or `set_treasury_limits` with a rate or burst of 0, a delay outside 0 to 30 days or no guardian, or `set_treasury_guardian` with the default pubkey. |
| 6040 | `0x1798` | `InvalidTreasuryDestination` | `Treasury destination must be the named token account and hold the global vault's mint` | `admin_withdraw` to an account other than the policy's `destination`, or a new destination that isn't the account passed or holds another mint. |
| 6041 | `0x1799` | `NoPendingTreasuryChange` | `No pending treasury change` | `apply_treasury_limits` with nothing pending. |
| 6042 | `0x179a` | `TreasuryChangeNotReady` | `The pending treasury change is still inside its delay` | `apply_treasury_limits` before `pending_at`. |
| 6043 | `0x179b` | `MarketMismatch` | `Market is not this CLP's market` | `vault_mint_pairs`, `vault_redeem_pairs` or `vault_redeem_single` with a market other than the CLP's own. |
| 6044 | `0x179c` | `PoolClosedWhileLive` | `The book trades only while the market is closed; use a committed order` | `pool_swap` while the market's price is live: its risk state is `Normal`, or its last price push is at most 300 s old. Send a committed order instead. The book reopens once the price is more than 300 s old outside `Normal`. |
## oracle
Program ID (devnet): `5vxiCrDpFnQ2W5QtgZBC66K2XTC19bjVBjinGYYBsadC`
The oracle program isn't updated on devnet, so these errors come only from operator instructions and the oracle's own view instructions. Mint and redeem report price problems with mint-redeem errors.
| Code | Hex | Name | Message | Cause and fix |
| - | - | - | - | - |
| 6000 | `0x1770` | `OraclePaused` | `Oracle is paused` | `is_paused` is set on the `OracleConfig`. |
| 6001 | `0x1771` | `InvalidPythAccount` | `Invalid Pyth account` | The `pyth_oracle` account doesn't parse as a Pyth price feed. |
| 6002 | `0x1772` | `PriceUnavailable` | `Price unavailable` | The Pyth price is zero or negative. |
| 6003 | `0x1773` | `ConfidenceTooHigh` | `Confidence interval too high` | `get_price`: confidence above `max_confidence_interval`. |
| 6004 | `0x1774` | `PriceStale` | `Price is stale` | `get_price`: the Pyth publish time is older than `max_staleness`. |
| 6005 | `0x1775` | `PriceMovementTooLarge` | `Price movement too large (>20% in one update)` | `get_price`: more than 20% away from `last_price`. |
| 6006 | `0x1776` | `TwapNotReady` | `TWAP not ready - insufficient observations` | `get_keeper_twap` always (deprecated); `get_user_twap`, `get_user_price` or `get_keeper_price` before a value is stored. |
| 6007 | `0x1777` | `InvalidTwapWindow` | `Invalid TWAP window duration` | `configure_twap_windows` with a keeper window outside 30 to 120 s or a user window outside 300 to 600 s. |
| 6008 | `0x1778` | `MarketStateRestriction` | `Operation not allowed in current market state` | Never raised. |
| 6009 | `0x1779` | `UnauthorizedKeeper` | `Unauthorized keeper` | The signer isn't `OracleConfig.keeper_authority`. |
| 6010 | `0x177a` | `UnauthorizedAdmin` | `Unauthorized admin` | The signer isn't `OracleConfig.admin_authority`, or `initialize_oracle_config` isn't signed by `market.authority`. |
| 6011 | `0x177b` | `InvalidRegisteredFeed` | `Invalid registered oracle feed` | The `OracleFeed` doesn't belong to this config and Pyth account. |
| 6012 | `0x177c` | `InactiveOracleFeed` | `Registered oracle feed is inactive` | The `OracleFeed` has `is_active` false. |
| 6013 | `0x177d` | `MathOverflow` | `Math Overflow` | Arithmetic overflow. Not expected in normal use. |
## registry
Program ID (devnet): `Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau`
| Code | Hex | Name | Message | Cause and fix |
| - | - | - | - | - |
| 6000 | `0x1770` | `SymbolTooLong` | `Symbol must be 16 characters or fewer` | `register_market` with a symbol over 16 bytes after uppercasing. |
| 6001 | `0x1771` | `UnauthorizedInitializer` | `Registry initialization requires the program upgrade authority` | `initialize` isn't signed by the registry program's upgrade authority. |
| 6002 | `0x1772` | `InvalidProgramData` | `Registry initialization requires this program's ProgramData account` | `initialize` was given a `program_data` account that isn't this program's ProgramData. |
## faucet
Program ID (devnet): `9tUeQAPEtVSB68NSfvFAqfwaB74GuVxm6Zbp1hrMiNKY`
| Code | Hex | Name | Message | Cause and fix |
| - | - | - | - | - |
| 6000 | `0x1770` | `CooldownNotElapsed` | `Cooldown period has not elapsed yet` | Less than `cooldown_seconds` (86,400 s on devnet) since this wallet's last drip. |
## trading
Program ID (devnet): `Anyy78cCJqapwvvFsignmQ45an5PCg9HfeDhDtAPNfmA`
These come from the [order book](/flows/order-book) and the [trading account](/flows/trading-account). Errors raised inside Manifest itself are Manifest's, not these.
| Code | Hex | Name | Message | Cause and fix |
| - | - | - | - | - |
| 6000 | `0x1770` | `NotAuthorized` | `Signer is neither the owner nor the account's active session key` | The signer of a trading call is not the account owner and not its current session key. |
| 6001 | `0x1771` | `OwnerOnly` | `Only the owner can do this` | An owner-only call (deposit, withdraw, set or revoke a session, close) signed by anyone but the account's owner, a session key included. |
| 6002 | `0x1772` | `SessionExpired` | `The session key has expired or was revoked` | A session-key call after the session's expiry, or after the owner revoked it. |
| 6003 | `0x1773` | `InvalidExpiry` | `Session expiry must be in the future and at most 30 days away` | `set_session` with an expiry in the past or more than 30 days ahead. |
| 6004 | `0x1774` | `OrderTooLarge` | `Order is larger than the session's per-order limit` | A session-signed order above the per-order limit the owner set. |
| 6005 | `0x1775` | `WrongTokenAccount` | `Token account is not the trading account's own` | A token account that isn't the associated account of the owner or the trading account's trader for that mint, or a mint that doesn't match the market. |
| 6006 | `0x1776` | `NotAManifestMarket` | `Account is not a Manifest market` | The market account passed isn't owned by the Manifest program. |
| 6007 | `0x1777` | `ZeroAmount` | `Amount must be greater than zero` | A deposit or withdraw of 0. |
| 6008 | `0x1778` | `MathOverflow` | `Arithmetic overflow` | Arithmetic overflow. Not expected in normal use. |
| 6009 | `0x1779` | `NotOurMarket` | `That Manifest market does not trade this Continuum market's token for its collateral` | The Manifest market's mints are not this Continuum market's token and its collateral. |
| 6010 | `0x177a` | `NoReferencePrice` | `The market has no posted price yet` | A session-signed order on a market with no posted price, so there is no band to check. |
| 6011 | `0x177b` | `OutsideBand` | `Session-signed orders must be priced inside the band around the posted price` | A session-signed order priced outside the protection band around the posted price. The owner can sign wider. |
| 6012 | `0x177c` | `UnsupportedOrderType` | `Only limit, immediate-or-cancel and post-only orders are supported` | An order type other than limit, immediate-or-cancel or post-only. |
| 6013 | `0x177d` | `TooManyOrders` | `Too many orders or cancels in one call` | More orders or cancels in one call than the program allows. |
| 6014 | `0x177e` | `NotFilled` | `Fill-or-kill order did not fill in full` | A fill-or-kill order that could not fill completely. The call reverts. |
| 6015 | `0x177f` | `InvalidFillOrKill` | `Fill-or-kill takes one immediate-or-cancel order and no cancels` | A fill-or-kill call with more than one order, an order that isn't immediate-or-cancel, or any cancel. |
| 6016 | `0x1780` | `NoSeat` | `The trader has no seat on this market` | The trading account has no seat on the Manifest market yet. Claim one with `claim_seat` first. |
| 6017 | `0x1781` | `InvalidDirection` | `Direction must be 0 (buy) or 1 (sell)` | A direction other than 0 or 1. |
| 6018 | `0x1782` | `BadGlobalAccounts` | `Global accounts must come in groups of mint, global, global vault, market vault and token program, for this market's mints` | The remaining accounts for Manifest's global orders aren't in groups of five for this market's mints. |
## Anchor errors below 6000
Anchor raises its own errors before the program's code runs: when the instruction doesn't exist, or when an account fails validation. The common ones:
| Code | Name | Cause and fix |
| - | - | - |
| 101 | `InstructionFallbackNotFound` | The instruction isn't in the deployed program. mint-redeem's four unified-vault instructions (`init_collateral_authority`, `migrate_to_unified_vault`, `exit_unified_vault`, `sweep_unified_surplus`) fail this way since 11 Oct 2026. Rebuild your client from the current IDL. |
| 2001 | `ConstraintHasOne` | An admin instruction signed by a key other than `market.authority`. |
| 2012 | `ConstraintAddress` | The `oracle` account isn't `market.oracleAddress`. Wrong `longMint`, `shortMint` or `collateralVault` accounts raise `InvalidAmount` (6001) instead. |
| 3012 | `AccountNotInitialized` | A user token account (cUSDC, L or S) doesn't exist yet. Create the ATA first. |
## How errors surface in clients
Anchor turns a failed transaction into an `AnchorError` that carries the code, its name and the program that raised it:
```typescript theme={null}
import { AnchorError } from "@coral-xyz/anchor";
try {
await program.methods.mintPaired(amount).accountsStrict(accounts).rpc();
} catch (e) {
if (e instanceof AnchorError) {
console.log(e.error.errorCode.code); // "TwapNotAvailable"
console.log(e.error.errorCode.number); // 6012
console.log(e.program.toBase58()); // the program that raised it
}
throw e;
}
```
When you simulate with `@solana/web3.js`, `AnchorError.parse(logs)` reads the same fields from the logs. The raw log line looks like this:
```
Program log: AnchorError thrown in programs/mint-redeem/src/lib.rs:2826. Error Code: TwapNotAvailable. Error Number: 6012. Error Message: TWAP price not available - keeper has not updated.
```
## Common error sequences
### Mint or redeem fails with `TwapNotAvailable` (6012)
The market's price is 0 or more than 300 s old. The keeper doesn't push prices while the underlying market is closed (for equities, nights and weekends), so mint and redeem stay closed until it reopens. The bin books trade in the meantime. If it happens during market hours and persists, check #announcements on [Discord](https://discord.gg/U5hPCB8ufJ), where the team posts outages, or ask in #help.
### Mint or redeem fails with `MintNotAllowedInStress` or `RedeemNotAllowedInStress`
The market is in `Stress`, which blocks both mint and redeem on every live market. Read `market.riskState` and surface it to the user as "mint and redeem paused". Committed orders still fill while the keeper pushes prices.
### Redeem fails with `InvalidAmount` (6001)
Both `l_amount` and `s_amount` must be above 0: there is no single-leg redeem. Sell a lone leg with a committed order while the market is open, or on its bin book while its price is stale. The same code also covers an amount above the outstanding supply and a token account whose mint doesn't match the market.
### `pool_swap` fails with `PoolClosedWhileLive` (6044)
The market's price is live, and the bin books trade only while it is stale. Send a committed order (`commit_swap`) instead; it fills at the next price push. See [Trade](/flows/trade).
### Redeem fails with `InsufficientLiquidCollateral` (6022)
The payout is more than the market's liquid collateral. On mainnet most collateral is lent out, and the vault keeps only a liquid share. Escrow the tokens with `request_redemption` and the keeper pays the request in parts as money comes back from the lending venues. Cancel with `cancel_redemption` to take the unpaid part back. See [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet).
### Mint or redeem fails with `BelowMinimum` (6019)
A mint needs a deposit of at least 10 cUSDC (`10_000_000` base units). A redeem needs both legs together to be worth at least 10 cUSDC at the redeem NAVs.
### Mint or redeem fails with `Unauthorized` (6021)
`devTokenAccount` must be a cUSDC token account owned by `market.feeRecipient`. Derive it with `getAssociatedTokenAddressSync(cusdcMint, market.feeRecipient, true)`; the `true` matters because a fee recipient can be a PDA.
## See also
Common questions, including error symptoms.
What the terms in error names mean.
# FAQ
Source: https://docs.continuum.markets/reference/faq
Common questions about Continuum, paired tokens, the keeper, and integration.
## What is Continuum?
A Solana protocol for **24/7 synthetic exposure to real-world assets**. You deposit collateral (cUSDC on devnet) and get matched long and short tokens priced from the asset's NAV. You can trade either leg on Continuum's own venues, hold it with no expiry, or redeem both legs together for collateral. → [Introduction](/get-started/introduction).
## Is it like a perp DEX?
No. Perp DEXs use funding rates, margin and liquidations, and Continuum has none of them. Positions are SPL tokens, paid for in full, so there is no leverage and nothing to liquidate. You can transfer the tokens, deposit them elsewhere or redeem them.
The short leg does carry a volatility decay instead of funding. It is charged on realized volatility rather than on positioning, and longs never pay it.
## What's the difference between long and short tokens?
Both track the same underlying. The long token (`L`) is priced at the asset price P. The short token (`S`) is priced at `K / P × γ`, where `K = initial_l_price × initial_s_price` and γ is the short leg's volatility-decay index. So `L × S = K × γ`: when the asset rises, L rises and S falls.
A paired position has little exposure to small moves. To take a direction, you sell one of the legs.
## Can I lose more than my deposit?
No. Continuum has no leverage and no liquidations, and you pay the full value of both legs when you mint.
Redeeming needs a price pushed in the last 300 s. While the keeper is pushing, holders of both legs can redeem against the market's collateral vault. They get full NAV while the vault is fully collateralized, or a pro-rata share if a large move has left it below 100%. Nobody is wiped out. A redemption larger than the vault's cash can be queued with a redemption request and paid in parts as cash comes back (see [Redeem](/flows/redeem#when-the-vault-cannot-pay-yet)). If the keeper stopped pushing prices, redeem would revert until pushes resumed.
You can still lose part of it: to fees, to the short leg's volatility decay, to a pro-rata payout in that tail case, or to ordinary directional risk once you sell one leg.
## Can I just buy long-only?
Yes, in two ways:
1. **Mint, then sell the short leg.** On QQQ at its 9 Oct 2026 price, 100 cUSDC mints about 0.0664 QQQL and 0.1637 QQQS after the fee. The QQQS is worth about 49.9 cUSDC at NAV; selling it with a committed order, less the spread, leaves you long QQQ with a cash buffer.
2. **Buy directly.** While the market is open, send a committed order, which fills at NAV ± the posted commit spread at a price pushed after you commit. The app uses traders' orders on the [order book](/flows/order-book) instead when they fill all of it now for less. While the market is closed, the long leg's bin book and traders' orders form [one book](/flows/order-book#one-book-while-closed), and a buy takes the cheapest offers first from either.
Buying directly is the usual path. Minting first is useful when you also want the short leg. → [Trade flow](/flows/trade).
## Why "paired" if I can sell one leg?
The program mints and redeems user positions only in pairs, for solvency. Once minted, the legs are ordinary SPL tokens, and you can do anything with them, including selling one. The pairing rule applies where collateral turns into tokens and back, not to trading.
## Why no funding rate?
Perps use funding payments between longs and shorts to keep the perp price near the index. Continuum has no such transfer, so nobody pays for being on the crowded side.
Holding is not free on both legs, though. The short leg carries a [volatility decay](/concepts/nav#volatility-decay) (the γ index), which pays for the convexity a long-short pair is exposed to. Unlike a funding rate, it tracks **realized volatility rather than positioning**. It only moves one way, so it never credits a holder, and it **never touches the long leg**.
## How do book prices stay close to NAV?
While a market's price is live, the books don't fill: trades go through committed orders, which fill at NAV ± the committed spread. The keeper re-centres each leg's bin book near NAV whenever its centre drifts past the reposition gate, 20 bps by default. Each book is lean, and an outside trader who lifts a stale bin pays the real slippage of the bin ladder. Turning a cheap leg into cUSDC through the protocol means buying the other leg too and redeeming the pair, which pays the redeem fee. → [Peg maintenance](/keeper/arb-paths).
The keeper's old arbitrage loop belonged to the legacy Meteora layer, which is off by default. Anyone can trade a book that has drifted from NAV; no keeper key is needed. → [Arb bot](/build/composability#2-arb-bot).
## What if the keeper goes down?
Mint and redeem go straight to the on-chain program, but they need its price push. 300 s after the last push, both revert with `TwapNotAvailable` until pushes resume. The same happens every time the underlying market closes.
What keeps working:
* The bin books open at their last posted bins, because `pool_swap` trades once the price is more than 300 s old outside `Normal`.
* A committed order that nobody settles can be refunded with `cancel_swap` once its 180 s TTL has passed. Anyone can send it, and the escrow goes back to the order's owner.
* `settle_swap` is permissionless, so anyone can settle a committed order once a fresh price has been pushed.
## Is my money lent out? What if a lending venue freezes?
On devnet, no. There are no lending venues there, so collateral stays in each market's vault.
On mainnet, mostly yes. The keeper lends a market's collateral to USDC lending venues (Kamino, Jupiter Lend and Save) and keeps a liquid share in the vault, 3% by default and re-derived each night in the range 1% to 10%. A redeem larger than that reverts with `InsufficientLiquidCollateral`; the holder queues a redemption request, which the keeper pays as it recalls money from the venues, borrowing from the treasury for the part the venues can't release in time. Each venue limits withdrawals, and a venue can pause them: MarginFi paused all withdrawals for about 79 hours in April 2026. A large redemption could then wait until the venue reopens. If a venue loss leaves a market below 100% collateral, the keeper's backstop refills it from the protocol treasury. → [Solvency → Lending venues](/concepts/solvency#lending-venues-and-redemption-timing).
## Can I run my own keeper?
Not today. The keeper's source and binary are not published, and each market's `keeper_authority` is a single pubkey that the market admin sets with `update_keeper_authority`. You can still run bots that need no keeper key: settling committed orders and trading the books are open to anyone. → [Keeper configuration](/keeper/running) · [Arb bot](/build/composability#2-arb-bot).
## What chain / network is this on?
Solana devnet. Plans for mainnet are on the [Roadmap](/markets/roadmap).
## What wallets work?
Any Solana wallet that supports the Wallet Standard, such as Phantom, Solflare or Backpack. The app connects through Solana Wallet Adapter.
## How are L and S tokens priced?
```
L_NAV = Market.user_twap_price
S_NAV = (initial_l_price × initial_s_price) / L_NAV × s_gamma_index / 1e9
```
`user_twap_price` is the spot price the keeper pushes, scaled to 1e6 and adjusted for corporate actions, despite the "TWAP" in its name. If it is 0 or more than 300 s old, mint and redeem revert with `TwapNotAvailable`; there is no fallback price. Mint and redeem also quote each leg against the user by the oracle confidence band, on every market except QQQ as of 10 Oct 2026. → [NAV math](/concepts/nav).
## What's the minimum mint?
10 cUSDC (`10_000_000` base units). Below that, `BelowMinimum` rejects the mint. Redeems have the same minimum, measured as the value of the legs you burn.
## What fees are there?
| Fee | When | Value |
| - | - | - |
| `mint_fee_bps` | On `mint_paired` | Keeper-posted `clamp(fee_mult × half-spread, 10, 100)` bps. As of 13 Oct 2026 it runs from 10 (EUR, GBP, JPY) to 39 (TSLA); QQQ is 18. |
| `redeem_fee_bps` | On `redeem_paired` | Same value as the mint fee. |
| Risk-state multiplier | On mint and redeem fees | 1.5× in ProxyMode, where every devnet market runs, so QQQ is charged 27 bps. |
| Book spread | On `pool_swap` | The bin prices themselves; there is no swap fee. |
| Committed spread | On committed-order fills | NAV ± `max(commit_spread_bps, 10)` bps: 18 on QQQ, 15 on SPY and 18 on XAU as of 10 Oct 2026. Every live market accepts committed orders since 11 Oct 2026. |
| Solana network fee | On every transaction | About 0.000005 SOL. |
10% of each mint and redeem fee is a dev tax. A mint and redeem round trip at an unchanged price costs about twice the charged fee: 54 bps on QQQ.
## What's the OI cap?
Each market's `Clp` account stores an `oi_cap` of 250,000 cUSDC (as of 10 Oct 2026), but nothing enforces it. Only the authority-only `update_oi` instruction checks the cap, nothing calls it, and `current_oi` is 0 on every market. Mints are not limited by it.
## Why is the protocol the sole LP?
To keep the MVP simple. The protocol funds the CLP's capital itself, and there are no user LP shares.
## Is there a token?
Continuum's planned governance token is **CNTM**. It is not live, and nothing on chain is governed by it today.
## Where do I report bugs?
* Discord: [join the server](https://discord.gg/U5hPCB8ufJ) and post in #help. New members can post ten minutes after joining.
* Errors in these docs: [continuum-markets/continuum-docs](https://github.com/continuum-markets/continuum-docs).
## How can I get cUSDC for testing?
Use the faucet: **Get 1000 cUSDC** on [/faucet](https://continuum.markets/faucet), once per wallet every 24 hours. → [Faucet program](/programs/faucet) · [Quickstart](/get-started/quickstart).
## Why "Continuum"?
Synthetic exposure across the continuum of real-world assets (stocks, indices, commodities and FX), available at any hour from any Solana wallet.
## Can I integrate Continuum into my own product?
Yes, please. Long and short tokens are SPL tokens, paired mint and redeem are open to anyone, NAV is on chain, and CPI works. The protocol is permissionless, so integrating needs no approval.
Building an integration? Tell us in #general on [Discord](https://discord.gg/U5hPCB8ufJ). → [Composability](/build/composability).
## Where do I find the source code?
The program, keeper and frontend source is not public. The IDLs for all six programs are published in the docs repo, under [`idl/`](https://github.com/continuum-markets/continuum-docs/tree/main/idl), and are enough to build clients and CPI integrations.
This docs site's source is [github.com/continuum-markets/continuum-docs](https://github.com/continuum-markets/continuum-docs). PRs welcome.
## See also
First-time mint flow.
The full mental model.
Every term explained.
What to do when things fail.
# Glossary
Source: https://docs.continuum.markets/reference/glossary
Continuum-specific terms in one place.
## A to C
**Anchor** - The Solana smart-contract framework Continuum is built on. Version 0.32.1.
**Ask trim** - Keeper inventory rule: ask-side synth worth more than 1.5× the bid side sweeps back to CLP custody (`pool_sweep_synth`). Ask depth the bid side cannot absorb only invites arbitrage.
**`asset_symbol`** - The market's ticker, for example `"QQQ"`. Up to 10 characters. It seeds the Market PDA.
**ATA** (Associated Token Account) - Solana's standard derivation for a wallet's token account: `getAssociatedTokenAddressSync(mint, owner)`. Always derived, not always created, so create it idempotently before instructions that expect it.
**Authority** - A signer with rights over an account: `market.authority` (the market admin), `market.keeper_authority`, `oracle_config.admin_authority`, `oracle_config.keeper_authority`, the CLP authority on `GlobalClp` and each `Clp`, and the treasury guardian on `TreasuryPolicy`.
**Base price** - A book's posted centre, `Pool.base_price_1e6`: NAV moved by the inventory centre-shift, or the last price during free float. Ask bins ladder above it, bid bins below.
**Bid cap** - Keeper inventory rule: each side's bid cUSDC is held at the depth its next free-float book will post (\$500 a side until the keeper has measured flow; `CLMM_BID_TARGET_USD` overrides it). The excess goes to the CLP vault, and bids that sellers spent are refilled from it. Large sells use committed orders while the market is open.
**Bin step** - The spacing between bins, in bps. Books use a fine core step for the first `core_bins` bins and a wide wing step beyond. On the live books the core is 10 to 50 bps over 3 bins and the wings 200 to 700 bps.
**bps** (basis points) - 1 bps = 0.01%. Fees are quoted in bps: `mint_fee_bps = 18` means 0.18%.
**Buffer target** - A market's overcollateralization target over parity, `Market.buffer_target_bps`, which the keeper posts with `set_buffer_target`. The target collateral ratio is 100% plus the buffer. Since 12 Oct 2026 a job recomputes each market's buffer daily with a jump-diffusion Monte Carlo from its live leg imbalance, γ and volatility regime: at most a 1% chance a year of falling below 100%, never below 1%. On 12 Oct it ranged from 1% (FX) to 12.4% (TSLA). On chain it gates only the donor fee waiver, how much excess a market can shed, and the donation gap-to-target credit; mints and redeems don't depend on it. The program clamps any target to 101% to 150%. → [Solvency](/concepts/solvency#3-posted-overcollateralization-buffer).
**CLP** (Continuous Liquidity Provider) - Continuum's on-chain treasury and trading program. It holds protocol capital and hosts committed orders and the bin books. The instant oracle swap it also hosted was retired on 11 Oct 2026. → [CLP concept](/concepts/clp).
**CNTM** - Continuum's planned governance token. Not active.
**Collateral floor** - `swap_guards.collateral_floor_bps`: the lowest collateral ratio, at the settle price, at which a committed order can fill. It is 100% on every market with swap guards on. Below it the order waits for a later push, and the keeper's treasury backstop tops the market up.
**Committed order** - The open-hours venue, at any size. `commit_swap(side, direction, amount_in, min_out, nonce)` escrows the input, which fills at NAV ± `max(commit_spread_bps, 10)` bps at any keeper push after the commit. The keeper posts `clamp(half-spread, 10, 500)` bps; on 10 Oct 2026 that was 18 on QQQ, 15 on SPY and 18 on XAU. Since 11 Oct 2026 every live market accepts committed orders. A fill waits while the market is below its collateral floor or the price fails the oracle checks. After the 180 s TTL, anyone can send `cancel_swap` to refund an unfilled order to its owner, and the keeper does so about 5 s after expiry. → [Trade flow](/flows/trade).
**Confidence interval** - The uncertainty band around a pushed price, stored as `Market.oracle_confidence`. On devnet it comes from a GJR-GARCH volatility model. Worst-case quoting widens mint and redeem quotes by it.
**Constant-product invariant** - `L_NAV × S_NAV = K × γ`, where `K = initial_l_price × initial_s_price` and γ is the short leg's volatility-decay index. As one leg rises the other falls.
**CPI** (Cross-Program Invocation) - Solana's mechanism for one program to call another. The CLP calls mint-redeem by CPI to mint and redeem pairs from its own vault (`vault_mint_pairs`, `vault_redeem_pairs`).
**cUSDC** - Continuum's devnet collateral mint (`B1c5xBYkp7AAemYhcu4VuH4CU4sPJDDuG2iuv6ts38uE`). On mainnet the collateral is real USDC. These docs say "cUSDC" to avoid confusion.
## D to L
**Drift gate (reposition gate)** - The move in a book's target centre that triggers a re-centre: 20 bps by default (`CLMM_REPOSITION_BPS`), cut further so the book moves before NAV reaches the floor of its first funded bin.
**Dynamic buffer** - The formula the keeper used to compute each market's buffer target live from realized volatility, `B = δ²/2 + |q|·δ + margin`. Per-market Monte Carlo targets replaced it on 11 Oct 2026, recomputed daily since 12 Oct. See Buffer target.
**Fee waiver** - A grant created by `donate_to_vault` that lets the donor mint and redeem fee-free for a period, through `mint_paired_with_waiver` and `redeem_paired_with_waiver`. It applies only while the market's vault is above its target ratio. The app no longer offers it.
**Force price** - The oracle program's `force_price` instruction. It exists in the source only behind the `devnet` build feature, is not in the published IDL and is not deployed. An operator emergency tool.
**Free float** - The book the keeper posts while mint and redeem are frozen (last push over 300 s old): anchored at the last price, each side in one bin within 1% of it, sized to measured demand (\$500 a side until flow is measured) and limited by a risk cap on that market's share of the treasury. A guard can pull bins that other venues show have gone stale. → [Book management](/keeper/book-management).
**γ index** - `Market.s_gamma_index`, the short leg's volatility-decay index (1e9 = 1.0), which multiplies its NAV. The keeper lowers it as realized volatility accrues (`set_s_gamma_index`); the program rejects writes that raise it or fall out of range. `reanchor_gamma` (authority only, at least 14 days apart) folds γ into `initial_s_price` and resets it to 1. It is not a reset to the spot price. → [Volatility decay](/concepts/nav#volatility-decay).
**Guardian** - The freeze-only key on the CLP's `TreasuryPolicy`. It can freeze and unfreeze treasury withdrawals, cancel a pending loosening of the limits, and hand its role to a new key. It can't move funds. Only the guardian can unfreeze. → [Treasury](/programs/clp#treasury).
**`initial_l_price` / `initial_s_price`** - Anchors set when a market is initialized. Their product is K in the NAV formulas. NAV never falls back to them. `reanchor_gamma` folds γ into `initial_s_price`.
**Instant depth** - The book's standing bin liquidity: what a `pool_swap` can fill immediately while the market's price is stale. Deliberately lean.
**Inventory centre-shift** - The keeper's shift of each book's centre to work off the protocol's own inventory. When the public holds more L than S, the CLP holds the extra S, so the long book is quoted dearer and the short book cheaper. At most half the narrower leg's half-spread. On by default.
**`is_long`** - Boolean argument to the single-leg instructions (`keeper_mint_single`, `keeper_redeem_single`, `keeper_burn_single`, `permissionless_burn`). `true` means the long leg, `false` the short leg.
**Keeper** - The off-chain Rust service the protocol runs: price pushes, book management, committed-order settlement, inventory moves and parameter posting. Its source is not published. → [Keeper overview](/keeper/overview).
**`keeper_authority`** - The pubkey stored on `Market` that may push prices (`update_risk_state_v2`), post buffer targets and γ, rebalance collateral between markets, and call the keeper-only single-leg instructions.
**L** (Long) - One half of a Continuum paired position. Rises in value as the underlying rises.
**`L_NAV`** - Long-leg NAV: `Market.user_twap_price`, the spot price the keeper pushes, scaled to 1e6 and adjusted for corporate actions. If it is 0 or more than 300 s old, mint and redeem revert with `TwapNotAvailable`.
**LP** (Liquidity Provider) - The protocol is the only LP, through the CLP. There are no user LP shares.
## M to R
**`mint_fee_bps`** - Per-market mint fee, posted by the keeper as `clamp(fee_mult × half-spread, 10, 100)` bps. As of 13 Oct 2026: QQQ 18, SPY 16, NVDA 31, TSLA 39, EUR/GBP/JPY 10. The risk-state multiplier applies on top (1.5× in ProxyMode), and 10% of the fee is a dev tax.
**`mint_paired`** - User-facing instruction. Deposits at least 10 cUSDC and returns L and S at NAV, half the net value each. → [Mint flow](/flows/mint).
**Mint authority** - The signer that can mint new SPL tokens. For Continuum's L and S mints it is the Market PDA. There is no freeze authority.
**MVP** - The release deployed on devnet today. It has no governance and no user LP, and its collateral yield is emulated rather than earned.
**NAV** (Net Asset Value) - The on-chain protocol price for L and S, used by mint and redeem. → [NAV math](/concepts/nav).
**Native book / bin book** - Continuum's own per-leg quote book: one `Pool` account per market and side, up to 16 bins stored inline (no per-bin rent), filled by `pool_swap` only while the market's price is stale (`PoolClosedWhileLive` otherwise). Each bin holds a fixed amount at a fixed price, and a swap walks outward from the centre. The `CLMM_*` keeper variables and `clmm.rs` keep an older label. → [What the book actually is](/concepts/clp#what-the-book-actually-is).
**OI** (Open Interest) - Outstanding token supply, tracked per leg on the `Market` account (`total_l_supply`, `total_s_supply`).
**`oi_cap`** - A cap stored on each market's `Clp` account (250,000 cUSDC on every live market as of 10 Oct 2026). It is not enforced: only the authority-only `update_oi` checks it, and nothing calls that.
**Oracle** - The oracle program, which holds a per-market `OracleConfig` with feeds, TWAPs and health state. On devnet the keeper does not update it; mint and redeem use the price the keeper writes to `Market`. → [Oracle concept](/concepts/oracle).
**Paired** - As in `mint_paired` and `redeem_paired`: the user-facing entry points, where L and S are minted or burned together at NAV-weighted values rather than in equal quantities.
**PDA** (Program Derived Address) - Solana's deterministic pubkey derived from seeds. Continuum has many: `Market`, `Clp`, `OracleConfig` and others. → [PDA derivations](/build/idls-and-pdas#pda-derivation-cheatsheet).
**Protective skew** - The spread engine's trend response: the side a continuing trend would hit widens with the drift z-score, and the other side never tightens below the symmetric shape. An antisymmetric variant, which also tightened the other side, failed in backtests.
**ProxyMode** - The risk state every devnet market runs in. The programs price from `Market.user_twap_price`, and fees and the confidence band are multiplied by 1.5. → [Risk states](/concepts/risk-states).
**Pyth** - The oracle network mainnet will use (Pyth Pro). On devnet the keeper prices markets from a free public quote source, because Pyth's public price endpoint now needs an API key.
**Realized ratchet** - The spread engine's fast term: a trailing empirical q90 of window moves, warm after about 8 windows. One of four terms in the half-spread.
**Recovery** - The risk state after Stress. The fee and confidence multiplier falls from 3× to 1× over the hour after the market enters Recovery.
**`redeem_fee_bps`** - Per-market redeem fee. The keeper posts the same value as `mint_fee_bps`.
**`redeem_paired`** - User-facing instruction. Burns L and S, both amounts above zero and worth at least 10 cUSDC together, and returns cUSDC at NAV less the redeem fee. → [Redeem flow](/flows/redeem).
**Registry** - The on-chain symbol-to-market index (`Hss7QZ46RcrV4ta63q2EM2Wf1Pzbjnd5FNjaTTDZHnau`). It is deployed and initialized, but it holds no markets and nothing reads it yet. Its only venue type is `MeteoraDlmm`. → [Registry program](/programs/registry).
**`risk_state`** - `Normal`, `ProxyMode`, `Stress` or `Recovery`, stored on `Market` and written by the keeper with `update_risk_state_v2`. Devnet markets are always in `ProxyMode`. Fee and confidence multipliers: 1×, 1.5×, 3×, and 3× falling to 1× over 1 hour. → [Risk states](/concepts/risk-states).
## S to Z
**S** (Short) - One half of a Continuum paired position. Rises in value as the underlying falls.
**`S_NAV`** - Short-leg NAV: `K / L_NAV × s_gamma_index / 1e9`, with `K = initial_l_price × initial_s_price`.
**Slippage** - The difference between the expected and the actual swap output. On the bin books it is the real cost of walking the bin ladder. Committed orders have none: they fill at NAV ± `max(commit_spread_bps, 10)` bps whatever their size.
**Slow auto-floor** - The spread engine's slow term: `floor_mult × σ_60d`, never below 5 bps. It needs about 10 days of history in memory to engage, and the static floor applies before then.
**Solvency invariant** - A property the protocol enforces at all times. → [Solvency invariants](/concepts/solvency).
**Stress** - The risk state for extreme confidence or staleness. Mint and redeem are both blocked by default: `allow_mint_in_stress` and `allow_redeem_in_stress` are false on every live market.
**Swap guards** - `Clp.swap_guards`, set with `configure_swap`: whether a market takes committed orders (`enabled`, `frozen`), plus the price checks and the collateral floor that `settle_swap` applies. On for every live market since 11 Oct 2026. → [Trade flow](/flows/trade#committed-orders).
**Treasury** - The CLP's global vault, the cUSDC ATA of the `GlobalClp` PDA (`5GSTKN4u5LfropcnQQoqFht48JMaF1SzZzBcgDNL5VKy`). It held about 40.0M cUSDC late on 12 Oct 2026, after the keeper's liquidity reallocator returned idle CLP cash on 11 Oct and the CLP's own holdings were cut to working size on 12 Oct. The devnet keeper is now burning it down, and it held 36.1M on 13 Oct 2026. `backstop_market` moves it into a market's collateral with no limit. The keeper does that when a market falls below 100% or stays short of its posted target, filling to that target, at most 130%. `admin_withdraw` takes it out of the protocol at a capped rate, set in `TreasuryPolicy`, to one named account. The keeper's yield allocator counts it as first-loss capital. → [Treasury](/programs/clp#treasury).
**TWAP** (Time-Weighted Average Price) - A price averaged over a window. The oracle program keeps TWAPs in `OracleConfig`, but nothing updates them on devnet. `Market.user_twap_price` is a spot price despite its name.
**Two-zone geometry** - The book's bin layout: a fine core step near the centre for tight instant pricing, and wide wing steps beyond `core_bins`.
**`user_twap_price`** - The `Market` field that drives `L_NAV`. The keeper writes it with mint-redeem `update_risk_state_v2`, along with `twap_updated_at`.
**Vault** (collateral vault) - The token account holding a market's backing cUSDC (`market.collateral_vault`). Every market has its own: the cUSDC ATA of its Market PDA. QQQ, SPY and XAU shared the unified vault (PDA `[b"unified_vault"]`, `5a51yurKcqxy6rVAV1ykif5fDzhHdjgZs3EL7yncLrp8`) until 11 Oct 2026. It holds 0 cUSDC, and that day's upgrade removed it from the program.
**Venue split** - Routing by market state: committed orders while a market's price is live, and the bin books while it is stale. The instant oracle swap (`clp_swap`) was retired on 11 Oct 2026. Both venues price off the same keeper-pushed NAV.
**Worst-case quoting** - Mint and redeem price each leg against the user by `c = oracle_confidence × state multiplier`: mint values L at P + c and S at `K / (P − c) × γ`, redeem L at P − c and S at `K / (P + c) × γ`. A per-market switch (`worst_case_config.enabled`), on everywhere except QQQ as of 10 Oct 2026.
**Yield allocator** - The keeper task that decides how much of each market's collateral to lend, and where, from live mainnet venue state. Each venue's total position across all markets is capped at first-loss capital divided by the venue failures to cover (1 by default). First-loss capital is the treasury plus half of all markets' collateral above 100%. It lends only on mainnet with `COLLATERAL_YIELD_ENABLED=true`. On devnet an emulator credits the interest its plan would earn. Every live market has `yield_enabled = true` as of 11 Oct 2026, and new markets start with it true. Harvested yield is added to the market's own collateral.
## See also
Per-program error catalog.
All current devnet pubkeys.