@ecurvesplarticle

e/curve, documentation

the program, the bytes, the checks
Byte map

The state account laid out byte by byte.

offsetlengthfieldtype
08discriminator[u8;8]
832mintPubkey
4032vaultPubkey
728anchor_cap_microu64
808burned_rawu64
888steps_firedu64
968spent_lamportsu64
1041bumpu8
1051vault_bumpu8
burned_raw is in base units with six decimals; divide by one million for whole tokens.
Program source

The full program follows. It is the same code that is deployed.

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Burn, Mint, Token, TokenAccount};

declare_id!("REPLACE_WITH_DEPLOYED_PROGRAM_ID");

pub const PUMP_PROGRAM: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
pub const PUMPSWAP_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
pub const RESERVE_FLOOR_LAMPORTS: u64 = 50_000_000;

#[program]
pub mod ecurve {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, anchor_cap_micro: u64) -> Result<()> {
        let s = &mut ctx.accounts.state;
        s.mint = ctx.accounts.mint.key();
        s.vault = ctx.accounts.vault.key();
        s.anchor_cap_micro = anchor_cap_micro;
        s.burned_raw = 0;
        s.steps_fired = 0;
        s.spent_lamports = 0;
        s.bump = ctx.bumps.state;
        Ok(())
    }

    pub fn step(ctx: Context<Step>, max_lamports: u64, min_tokens_raw: u64, evaluation_id: u64) -> Result<()> {
        let vault_balance = ctx.accounts.vault.lamports();
        require!(vault_balance > RESERVE_FLOOR_LAMPORTS, EcurveError::BelowFloor);
        let spend = max_lamports.min(vault_balance - RESERVE_FLOOR_LAMPORTS);
        require!(spend > 0, EcurveError::NothingToSpend);

        let venue = ctx.accounts.venue_program.key();
        require!(venue == PUMP_PROGRAM || venue == PUMPSWAP_PROGRAM, EcurveError::WrongVenue);

        let before = ctx.accounts.program_token.amount;
        let mint_key = ctx.accounts.mint.key();
        let seeds: &[&[u8]] = &[b"vault", mint_key.as_ref(), &[ctx.accounts.state.vault_bump]];
        crate::venue::buy_with_vault(&ctx, spend, min_tokens_raw, &[seeds])?;
        ctx.accounts.program_token.reload()?;
        let received = ctx.accounts.program_token.amount.checked_sub(before).unwrap();
        require!(received >= min_tokens_raw, EcurveError::TooFewTokens);

        token::burn(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Burn {
                    mint: ctx.accounts.mint.to_account_info(),
                    from: ctx.accounts.program_token.to_account_info(),
                    authority: ctx.accounts.state.to_account_info(),
                },
                &[&[b"state", mint_key.as_ref(), &[ctx.accounts.state.bump]]],
            ),
            received,
        )?;

        let s = &mut ctx.accounts.state;
        s.burned_raw = s.burned_raw.checked_add(received).unwrap();
        s.steps_fired = s.steps_fired.checked_add(1).unwrap();
        s.spent_lamports = s.spent_lamports.checked_add(spend).unwrap();

        let r = &mut ctx.accounts.step_record;
        r.evaluation_id = evaluation_id;
        r.step_index = s.steps_fired;
        r.spend_lamports = spend;
        r.burned_raw = received;
        Ok(())
    }

    pub fn record_fee(ctx: Context<RecordFee>) -> Result<()> {
        msg!("vault {}", ctx.accounts.vault.lamports());
        Ok(())
    }
}

#[account]
pub struct State {
    pub mint: Pubkey,
    pub vault: Pubkey,
    pub anchor_cap_micro: u64,
    pub burned_raw: u64,
    pub steps_fired: u64,
    pub spent_lamports: u64,
    pub bump: u8,
    pub vault_bump: u8,
}

#[account]
pub struct StepRecord {
    pub evaluation_id: u64,
    pub step_index: u64,
    pub spend_lamports: u64,
    pub burned_raw: u64,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = authority, space = 8 + 32 + 32 + 8 + 8 + 8 + 8 + 1 + 1, seeds = [b"state", mint.key().as_ref()], bump)]
    pub state: Account<'info, State>,
    /// CHECK: system owned PDA that only receives lamports
    #[account(seeds = [b"vault", mint.key().as_ref()], bump)]
    pub vault: UncheckedAccount<'info>,
    pub mint: Account<'info, Mint>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(max_lamports: u64, min_tokens_raw: u64, evaluation_id: u64)]
pub struct Step<'info> {
    #[account(mut, seeds = [b"state", mint.key().as_ref()], bump = state.bump)]
    pub state: Account<'info, State>,
    /// CHECK: the reserve
    #[account(mut, seeds = [b"vault", mint.key().as_ref()], bump = state.vault_bump)]
    pub vault: UncheckedAccount<'info>,
    #[account(mut)]
    pub mint: Account<'info, Mint>,
    #[account(mut, associated_token::mint = mint, associated_token::authority = state)]
    pub program_token: Account<'info, TokenAccount>,
    #[account(init, payer = keeper, space = 8 + 8 + 8 + 8 + 8, seeds = [b"step", mint.key().as_ref(), &evaluation_id.to_le_bytes()], bump)]
    pub step_record: Account<'info, StepRecord>,
    #[account(mut)]
    pub keeper: Signer<'info>,
    /// CHECK: checked against the two allowed venue ids in the handler
    pub venue_program: UncheckedAccount<'info>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct RecordFee<'info> {
    /// CHECK: read only
    pub vault: UncheckedAccount<'info>,
}

#[error_code]
pub enum EcurveError {
    #[msg("reserve is at or below the floor")]
    BelowFloor,
    #[msg("nothing to spend")]
    NothingToSpend,
    #[msg("venue program not allowed")]
    WrongVenue,
    #[msg("received fewer tokens than required")]
    TooFewTokens,
}
The venue module builds the pump.fun or PumpSwap buy instruction with the vault as payer and the program's token account as receiver; it is a thin wrapper and carries no state of its own. The state account size in the byte map above is 105; the deployed struct adds one byte for the vault bump, making 106, and the byte map row for offset 105 is vault_bump, u8.
Invariants
Deployment and naming

The program must show its name on Solana explorers. These steps are part of the deployment and are not optional.

Data sources
valuesourcerefresh
market cap, priceBirdeye price endpoint through the edge function1 s
24h volume, holders, liquidityBirdeye token overview through the edge function5 s
tradesBirdeye token transactions through the edge function1 s
price history for the figure seedBirdeye OHLCV, one minuteon load, then 60 s
holder listBirdeye holder endpoint10 s
SOL priceBirdeye price endpoint for the wrapped SOL mint5 s
reserve, burned, steps fired, spentthe state account and vault, read by the evaluate function and written to the databaseon every evaluation
steps, evaluationsdatabase tablesrealtime subscription
Checks

Anything shown here can be checked without trusting this site. The state account can be read with any RPC. The vault balance is a system account balance. Each step's transaction is on Solscan with its burn instruction visible. The market cap is Birdeye's own figure.

Glossary
anchor capThe market cap recorded at initialization. The base the target grows from.
bandTwo percent either side of the target. Nothing fires inside it.
burned shareBurned tokens divided by the initial supply of one billion.
evaluationOne reading of the spread, triggered by one observed trade.
kernelThe rule that turns a breach of the band into a fraction of the reserve to spend.
reserveThe vault balance in SOL. Funded by creator fees only.
spreadMarket cap over target, minus one.
stateOne of below, inside, above.
stepOne buy and burn transaction sent by the program.
targetAnchor cap times e to the eight times burned share.
vaultThe program's PDA that receives creator fees and pays for steps.
venuepump.fun before graduation, PumpSwap after.