DOC# ZERASW SLUG zeraswap_compressed_amm PRINTED 2026-05-06 03:47 UTC

ZeraSwap: An AMM for Compressed Tokens

Initial commit of the first compressed-token AMM on Solana — Anchor program, x*y=k math, SOL/cToken pairs, and the cyberpunk launchpad UI that grew up around it.

FROM
Dax the Dev <[email protected]>
SOURCE
https://blog.skill-issue.dev/blog/zeraswap_compressed_amm/
FILED
2026-02-10 21:03 UTC
REVISED
2026-02-15 00:36 UTC
TIME
6 min read
SERIES
Zera Origin Stories
TAGS
#zera #solana #anchor #amm #light-protocol #compressed-tokens #rust

“Initial ZeraSwap: compressed token AMM for Solana”

That’s the first commit on z_trade, dropped at 2026-02-10T21:03:36Z. It’s also, as far as I’m aware, the first AMM where the token side of every pool is a Light Protocol compressed token instead of an SPL token. That’s not an accident; that’s the entire pitch.

Solana compressed tokens (@lightprotocol/compressed-token) cost roughly 1/5000th of SPL tokens to mint and transfer at scale, because the account state lives in a Merkle tree off-chain instead of a 175-byte SPL account on-chain. That’s incredible for token launches, terrible for AMMs — because every existing AMM expects to hold token accounts. So if you want compressed tokens to actually be useful as economic objects, you need an AMM that natively takes them.

The Anchor program

Seven instructions. From programs/zeraswap/src/lib.rs:

#[program]
pub mod zeraswap {
    use super::*;
    pub fn initialize_protocol(ctx, fee_recipient, lp_fee_bps, protocol_fee_bps) -> Result<()> { ... }
    pub fn create_pool(ctx, initial_sol, initial_tokens) -> Result<()> { ... }
    pub fn add_liquidity(ctx, sol_amount, token_amount, min_lp_out) -> Result<()> { ... }
    pub fn remove_liquidity(ctx, lp_amount, min_sol_out, min_tokens_out) -> Result<()> { ... }
    pub fn swap_sol_for_tokens(ctx, sol_in, min_tokens_out) -> Result<()> { ... }
    pub fn swap_tokens_for_sol(ctx, tokens_in, min_sol_out) -> Result<()> { ... }
    pub fn collect_fees(ctx) -> Result<()> { ... }
}

Constants (constants.rs):

pub const DEFAULT_LP_FEE_BPS: u16 = 20;       // 0.20%
pub const DEFAULT_PROTOCOL_FEE_BPS: u16 = 5;  // 0.05%
pub const MAX_FEE_BPS: u16 = 1000;            // 10% max total
pub const MINIMUM_LIQUIDITY: u64 = 1_000;     // locked forever on first deposit
pub const MINIMUM_SOL_RESERVES: u64 = 10_000; // 0.00001 SOL

The math is x*y=k, the same constant-product curve Uniswap v1 shipped in 2018. There’s a reason every L1 AMM eventually defaults to this: it has no edge cases that you find in production. From instructions/swap.rs:

// Constant product:
// tokens_out = token_reserves * sol_in_after_fee
//            / (sol_reserves + sol_in_after_fee)
let tokens_out = (pool.token_reserves as u128)
    .checked_mul(sol_in_after_fee as u128)?
    .checked_div(
        (pool.sol_reserves as u128).checked_add(sol_in_after_fee as u128)?,
    )? as u64;

require!(tokens_out >= min_tokens_out, ZeraSwapError::SlippageExceeded);
require!(tokens_out < pool.token_reserves, ZeraSwapError::ReservesDrained);

I wrote it u128-promoted for the multiply, then cast back to u64 after the divide, because u64 * u64 overflows roughly the moment any pool gets serious volume. Nothing exciting; just the kind of detail that bites you exactly once.

What’s actually novel

The thing I had to figure out wasn’t the curve. It was state trees. Each pool gets its own state_tree: Pubkey field in the Pool struct:

#[account]
pub struct Pool {
    pub token_mint: Pubkey,
    pub lp_mint: Pubkey,
    pub sol_vault: Pubkey,
    /// Dedicated state tree for this pool's compressed token operations
    pub state_tree: Pubkey,
    pub sol_reserves: u64,
    pub token_reserves: u64,
    pub lp_supply: u64,
    // ...
}

Light Protocol’s compressed token operations need an explicit state_tree reference. If you forget that, the compress/decompress CPI just silently lands the tokens in someone else’s tree, and your pool can never reconstruct them. Five days of staring at logs taught me to put state_tree directly on the Pool account at creation time and never touch it again.

Five days later: the cyberpunk launchpad

The next major commit is b6b6fa5 — Add shared AMM vault, launchpad, pools, transfers, cyberpunk UI on 2026-02-15. This is where the AMM stopped being a barebones swap and started being a launchpad — bonding curves, internal UserPosition.token_balance accounting, a graduation flow at 50 SOL of bonding-curve liquidity, and the cyan/purple cyberpunk frontend that ended up being the project’s identity.

The launchpad is conceptually a separate Anchor program that buys/sells against a virtual reserve (think pump.fun) until a token “graduates” to a real ZeraSwap AMM pool. The curve uses a base reserve to bootstrap price discovery. From the same day, I shipped both f3f71f3 and d01b4683 lowering graduation from 85 → 50 SOL after the first paper trade made it obvious 85 was too high — nobody graduates a token if they need to spend $15K to do it.

The quality-of-life shift

The most under-appreciated commit of that February sprint is cb14990 — Fix RPC spam: pause polling on hidden tabs. The whole repo had been making 46–94 RPC calls/min to Helius. New worst case after the fix: 12 calls/min on the active tab, 0 on hidden tabs. The hook is six lines of meaningful code:

// app/src/hooks/useVisibleInterval.ts
function onVisibilityChange() {
  if (document.hidden) {
    stop();
  } else {
    savedCallback.current(); // fire immediately on re-show
    start();
  }
}
document.addEventListener("visibilitychange", onVisibilityChange);

A free tier of Helius is 100k calls/day. A tab open for 24 hours at 94 calls/min burns through that in 18 hours. This bug was costing me real money. The fix shipped 12 days into the project.

Trade-offs

Why not use an existing AMM SDK? Because none of them know what to do with @lightprotocol/compressed-token. Orca, Raydium, Meteora — every one of them assumes SPL token accounts. By the time you’ve patched their account derivation, you’ve written your own program anyway.

Why x*y=k instead of concentrated liquidity? Because the AMM is a graduation target for the launchpad, not a yield-farming venue. The launch flow guarantees pools start with deep, balanced reserves. Concentrated liquidity in that environment is just a way to price-impact yourself. If somebody serious comes along and wants to bring real liquidity, they can fork the program; the math is 30 lines.

Why two fees (LP + protocol)? Because I don’t trust myself to skim the protocol fee out of LP revenue post-hoc. Putting the protocol fee on a separate counter from the start was cheap then and saved me a migrate_config (6d04415) later — well, almost saved me. We’ll get to that.

What this taught me

Compressed tokens are an unfair advantage for whoever ships first, because the entire DEX ecosystem on Solana is built on the assumption that “token” = “SPL Token Account.” Light Protocol changed that assumption. The block of code most people miss is keeping a state_tree field on every pool — once you’ve done that, everything else is x*y=k and being kind to your RPC provider.

Further reading

← Back to article