Skip to content
Sesame

Connect a wallet

You can use every feature without a wallet: the demo account lives in this browser. Connecting only labels the account with your address and prepares for Robinhood Chain Testnet.

DemoRobinhood Chain testnet · 46630

How Sesame works

What each part does, which numbers are real and which are simulated, and what stays public. Sesame runs on a demo ledger: no contracts are deployed yet.

Overview#

Sesame has two lanes, kept apart on purpose: a public lane for trading and liquidity, and a shielded lane where a deposit becomes a private note.

Two lanes#

The public lane holds the DLMM and DAMM pools, perpetuals, capped-call options, the bonding-curve launchpad and analytics. Everything in it is visible: who traded, what, how much, at what price and when. Price discovery needs that. LPs judge their risk from depth and flow, and traders can check the price they got.

The shielded lane is a vault. You deposit one of a few fixed amounts and get a note: a secret only you hold. Later you withdraw with that note, to any address. The vault's job is to make it hard to tell which deposit paid for which withdrawal.

Public lane

DLMM and DAMM pools, perps, options, the launchpad and analytics.

Visible: who, what, how much, at what price, when.

Deposit a fixed amountWithdraw with a note

Shielded lane

The vault: deposits become notes in a commitment tree.

Visible: commitments, nullifiers, fixed amounts, timing.

The public lane and the shielded lane meet only at deposit, where a fixed amount enters the vault, and at withdrawal, where a note is spent.

The lanes meet only at deposit and withdrawal. Shielded funds can't trade in the pools directly: a trade made from inside the vault would show its size and timing to everyone, which undoes the point of the vault. Shielded swaps are on the roadmap, not in the app.

Every action lands in one activity feed, tagged with its lane: public, shielded or system.

Demo mode#

Sesame runs on a demo ledger: one SQLite database on the server, with CHAIN_MODE=off. No contracts are deployed and no real funds move. Your browser gets a demo account behind an httpOnly cookie, opened on your first action with 10,000 USDC, 1 ETH and 25,000 SEED; the faucet tops up the same amounts, once every 10 minutes. Connecting a wallet (Robinhood Chain testnet, chain id 46630) stores your address as a label only, and demo mode never asks you to sign anything. Block numbers come from a 250 ms clock that started on 1 January 2026 (UTC), and transaction hashes are keccak256 hashes of ledger records, not on-chain transactions. Reference prices are real, from Yahoo Finance and DefiLlama. The full split is under simulated and real.

The shielded vault goes further: its zero-knowledge proofs are real Groth16 proofs, even on the demo ledger, and its contracts are ready for Robinhood Chain testnet. See the shielded lane.

Public lane#

Two pool types share one ledger. DLMM pools put liquidity into price bins; DAMM pools spread it along a constant-product curve. In both, 80% of every swap fee goes to liquidity providers.

DLMM pools#

A DLMM pool splits prices into bins. Bin i trades at exactly one price, set by the pool's bin step. Inside a bin a swap has no price impact; a larger swap empties the bin and moves on to the next one, and that is what moves the price.

price(bin i) = (1 + binStep / 10,000) ^ i      quote per base

Bins below the active bin hold only the quote token, bins above it hold only the base token, and the active bin holds both. A buy takes base from the active bin upwards; a sell takes quote from it downwards.

To add liquidity you pick a range, as bin offsets from the active bin, of up to 69 bins per position, and a shape:

  • Spot puts the same amount in every bin.
  • Curve puts most of it near the active bin and thins out towards the edges.
  • Bid-ask puts more towards the edges and less near the active bin.

Spot

Even across the range

Curve

Concentrated near the price

Bid-ask

Heavier towards the edges

Quote token (USDC), below the price Base token, above the priceActive bin (holds both)

Quote goes into bins at or below the active bin, base into bins at or above it. A range entirely above the active bin takes only base; a range entirely below takes only quote.

The fee is taken from the swap input and credited to the bins the swap passed through, in proportion to what each bin filled. 80% goes to the LPs in those bins and 20% is protocol revenue. A position earns only while the active bin is inside its range; the app marks each position in range or out of range. You can claim fees at any time. Removing a position closes all of it and pays out its tokens plus any unclaimed fees.

DAMM pools#

A DAMM pool is a full-range constant-product pool. The product of the two reserves stays the same through a swap (before fees), and the price is the quote reserve divided by the base reserve. Liquidity is always in range, so a position never stops earning, but it is spread thinly over every possible price.

x · y = k                         x = base reserve, y = quote reserve
price      = y / x
buy:  out  = x · in′ / (y + in′)    in′ = input after the fee
sell: out  = y · in′ / (x + in′)

The fee is dynamic. It starts at the pool's base fee and rises with the price move since the last trade that is at least an hour old: for every 1% the price has moved, the fee rises by 0.2 percentage points, up to +0.70%. Calm markets pay the base fee; fast markets pay LPs more for the risk they carry.

fee = baseFee + min(70 bps, ⌊0.2 × |ln(P_now / P_1h)| × 10,000⌋ bps)

To add liquidity you enter the quote amount and the app matches the base amount at the pool's current ratio. Your share of the pool is your share of the reserves and of the fees. As with DLMM, 80% of fees go to LPs and 20% to the protocol.

Swaps and slippage#

A swap goes through one pool. Buy means quote in, base out; sell means base in, quote out. There is no routing across several pools yet.

While you type, the swap form asks the server for a quote: the same swap run with dryRun, which changes nothing and returns the output, the fee, the average price, the price before and after, and the price impact. Price impact is the gap between your average price and the price before the swap.

Slippage is what changes between that quote and the fill, because other trades, the keeper or simulated flow can land in between. The form shows Minimum received: the quoted output reduced by your slippage limit. It is sent with the swap as minOut. The server runs the swap again at submit time and, if the output would be below minOut, rejects it with a “price moved” error and changes nothing.

Each swap runs in one database transaction, so it fills completely or not at all. A DLMM swap that runs out of liquidity in range is rejected, and the error says how much the pool can fill. A DAMM swap is rejected if it would empty a reserve.

Pool registry and simulated flow#

The ledger starts with 13 pools, created on first run at that day's reference prices and funded with protocol-owned liquidity. DLMM seed positions span 69 bins around the starting price in the curve shape.

Pool registry
PairModelBase feeBin stepSeeded liquidity
SEED/USDCDLMM0.3%25 bps$180K
ETH/USDCDLMM0.2%10 bps$1.5M
TSLA/USDCDLMM0.3%10 bps$650K
NVDA/USDCDAMM0.3%–$900K
AAPL/USDCDAMM0.3%–$700K
MSFT/USDCDAMM0.3%–$520K
AMZN/USDCDAMM0.3%–$450K
META/USDCDAMM0.3%–$420K
GOOGL/USDCDAMM0.3%–$380K
PLTR/USDCDAMM0.3%–$260K
NFLX/USDCDAMM0.3%–$210K
AMD/USDCDAMM0.3%–$300K
SEED/ETHDAMM1%–$60K

Pool ids look like addresses but are not contracts: each is the last 20 bytes of the keccak256 hash of the pool's name, for example sesame.pool.ETH.USDC.DLMM. Graduated launches add more DAMM pools to this registry.

So that charts have a past, seeding writes 7 days of simulated history: three trades per hourly reference candle (open, then high or low, then close). SEED has no outside price, so its history is a seeded random walk that ends at 0.042 USDC.

After that, a keeper runs whenever pools are read, at most once every 20 seconds per pool. If a pool is more than 0.15% away from its reference price, the keeper trades it back, as an arbitrageur would. A DLMM pool can only follow as far as its bins hold liquidity. If a pool has had no trade for 150 seconds, there is a 60% chance of a random trade worth between $60 and $1,860. Pools without an outside price (SEED/USDC and graduated launch pools) only get the random trades; SEED/ETH follows SEED/USDC.

Simulated trades count

Keeper and random trades pay fees like any other trade and are included in volume, fees, candles and LP earnings. They are stored with a simulated flag, and the activity feed labels them “keeper arbitrage (simulated)” or “simulated flow”.

Fees and the assistance fund#

Protocol revenue comes from four places, each recorded in the asset it was paid in:

  • Pool swaps: 20% of the swap fee, in the input token.
  • Perps: 6 bps of notional on open and again on close, in USDC.
  • Options: 1% of the premium, in USDC.
  • Launchpad curves: 1% of every curve trade, in USDC.

The assistance fund turns that revenue into SEED buybacks when someone presses the button on Analytics (POST /api/analytics). It values the pending revenue in USDC at current prices, then splits it:

  • 99% buys SEED in the SEED/USDC pool. The SEED bought is burned: it is credited to no account, so it leaves circulation.
  • 1% goes to the treasury account, in USDC.

The buyback is an ordinary swap. It pays the pool fee and moves the SEED price like any trade, and its own protocol fee becomes new pending revenue. If pending revenue is worth less than 1 USDC, nothing happens.

Demo mode

Revenue is a list of ledger records, not tokens held by a contract. Valuing it in USDC is bookkeeping (reference prices for ETH and stock tokens, pool prices for SEED and launch tokens), not a sale in a pool.

Perpetuals#

Perps let you take a leveraged long or short with demo USDC as collateral. They are priced by the reference feed, not by the pools, so a thin demo pool can't move a perp.

Perp markets
MarketMax leverageOracle
ETH-PERP20×Yahoo Finance, DefiLlama as fallback
TSLA-PERP10×Yahoo Finance, delayed
NVDA-PERP10×Yahoo Finance, delayed
AAPL-PERP10×Yahoo Finance, delayed
MSFT-PERP10×Yahoo Finance, delayed

You choose a market, a side, your collateral (at least 10 USDC) and leverage. Notional is collateral times leverage. The entry price is the oracle price with a 0.02% spread against you: higher for a long, lower for a short. The open fee, 6 bps of notional, is charged on top of your collateral.

notional   = collateral × leverage
entry      = oracle × (1 ± 0.0002)        + for a long, − for a short
size       = notional / entry             in units of the asset
open fee   = notional × 0.0006
close fee  = size × exit × 0.0006

Open positions are marked to the oracle price. A position is liquidated when its equity, collateral plus unrealised PnL, falls to 5% of the position's value at the mark price. That gives these liquidation prices, shown on every position:

long:  liq = (size × entry − collateral) / (size × (1 − 0.05))
short: liq = (size × entry + collateral) / (size × (1 + 0.05))

Closing pays out your collateral plus PnL minus the close fee, at the oracle price with the same 0.02% spread against you. A loss can't exceed your collateral.

What the demo perps leave out

  • No funding rate. Nothing pulls long and short open interest towards balance, and holding a position costs nothing over time.
  • No liquidation bot. Positions are checked when your account is loaded. One that is past its liquidation price is closed then and you lose the collateral. A price that crosses and comes back between two checks does not liquidate you.
  • No counterparty. PnL is paid by the demo ledger. Open interest is shown for information only.
  • Market hours. Stock prices are delayed and only move while their exchange is open. If the feed can't be reached, the oracle falls back to the last good price or a snapshot, marked as not live, and you can still trade on it.

Options#

Options are European, cash-settled capped calls on six stock tokens and ETH. You pay a premium now; at expiry you receive the gain above the strike, up to a cap. You can't lose more than the premium and fee.

A capped call is a call spread: long a call at the strike K, short a call at the cap C. Sesame sets the cap at 110% of the strike, so the most one contract can pay is (C − K) × contract size. European means it pays only at expiry; cash-settled means it pays in USDC, not in the asset.

payout per contract = min(max(S − K, 0), C − K) × contract size
S = reference price read at settlement
Strike KCap C = 1.1 KPrice at expiryMax payout (C − K) × size0
Payout per contract at expiry, before the premium you paid.

Series. Each underlying has two expiries: a weekly one, the next Friday at 20:00 UTC that is at least 48 hours away, and a monthly one, the last Friday of next month at 20:00 UTC. Each expiry has two strikes, at the current price and 5% above it, rounded to a strike grid. New series are listed automatically as the calendar moves on, so there is always a weekly and a monthly expiry to trade. Trading in a series stops 10 minutes before expiry.

Premium. The premium is the Black-Scholes value of the call spread at the current reference price, times the contract size. The model uses a stated volatility for each underlying, which is an assumption rather than a market-implied figure, and a 4% rate. A 1% fee on the premium goes to protocol revenue.

premium = [ BS(S, K, T, σ, r) − BS(S, C, T, σ, r) ] × contract size
fee     = 1% × premium
T = years to expiry,  σ = model volatility,  r = 4%
Option underlyings
UnderlyingContract sizeModel volatility
TSLA1 share55%
NVDA1 share50%
AAPL1 share25%
MSFT1 share25%
AMZN1 share32%
META1 share35%
ETH0.1 ETH60%

Collateral. The writer is a protocol vault of demo USDC that started with 2,000,000. For every contract it sells it locks the full maximum payout, so every contract can always pay. If the vault doesn't have enough free collateral, the buy is rejected. You can buy up to 500 contracts at a time. The trade panel sends maxCost 2% above the quote it shows; if the fresh premium plus fee is higher, the buy fails and nothing is charged.

Settlement. On the first read after expiry, the series settles at the reference price read at that moment, and payouts are credited in USDC. If the feed can only return a snapshot, settlement waits up to 6 hours for a live price before it uses the snapshot, and the settlement is marked as one.

Limits

  • You can only buy. There is no selling back before expiry and no writing your own options, so positions are held to expiry.
  • Stock reference prices are delayed, and settlement uses whatever the feed returns at that moment.
  • The premium comes from a model with an assumed volatility, not from a market. See Options for the live series.

Launchpad#

Anyone with a demo account can launch a token on a bonding curve. When the curve sells out, the USDC it raised and a reserve of tokens move into a DAMM pool.

Bonding curves#

Every launch has 1,000,000,000 tokens. 800,000,000 are sold on the curve and 200,000,000 are held back for the pool. The curve is constant-product with virtual reserves: it starts with T₀ = 1,073,000,000 virtual tokens and Q₀ virtual USDC, and the price is Q ÷ T. Q₀ is set so that selling exactly the 800,000,000 curve tokens raises exactly the template's threshold.

T · Q = k = T₀ · Q₀
price  = Q / T                                  USDC per token
Q₀     = threshold × (T₀ − 800,000,000) / 800,000,000
Launch templates
TemplateGraduates atVirtual USDC (Q₀)First priceLast curve pricePool opens at
Standard25,000 USDC8,531.250.000007950.0001230.000125
Sprint5,000 USDC1,706.250.000001590.00002460.0000250

Prices in USDC per token. The pool opens at the threshold divided by the reserved tokens.

Every buy and sell pays a 1% fee on the USDC side, which goes to protocol revenue. A buy that would pass the threshold is cut to exactly what the curve still needs, and the rest isn't charged. Sells put tokens back into the curve, up to the number sold so far. Quotes and the slippage limit (minOut) work as they do for swaps.

To create a launch you pick a template, a name (2 to 32 characters), a ticker (2 to 8 letters, not already in use), and optionally a description and a first buy. Each demo account can create up to 10 launches. The list starts with five demo launches whose trades are simulated and labelled as such.

Graduation and LP allocation#

When the USDC raised reaches the threshold, the curve closes and the launch shows graduation pending. Anyone can then migrate it. Migration creates a TICKER/USDC DAMM pool from the raised USDC and the 200,000,000 reserved tokens, in the 1% fee tier; the dynamic fee applies on top, as in every DAMM pool. From then on the token trades in that pool under Pools.

The LP shares minted at migration go into an escrow position and are split three ways:

LP allocation at graduation
ShareGoes toLock
10%The launch creatorTime-locked; claimable 10 minutes after migration in demo mode
10%The protocolMoved to a protocol-owned position at migration
80%NobodyLocked in escrow for good; it can never be claimed

Because 80% of the starting liquidity can't be withdrawn by anyone, neither the creator nor the protocol can empty the new pool.

Demo mode

  • Launch tokens are entries in the demo ledger, not token contracts.
  • The creator lock is 10 minutes so the whole flow can be tried in one sitting. It is not a suggested value for a real launch.

Shielded lane#

The vault turns a deposit into a note. To withdraw, your browser builds a real zero-knowledge proof that you hold an unspent note in the vault, without saying which one.

The vault runs in two modes, with the same circuit, the same notes and real Groth16 proofs in both:

  • Demo ledger. Balances live in SQLite. The server checks each proof with the same verification key as the on-chain verifier and never receives your secret, nonce or leaf.
  • On-chain. The SesameVault contract holds sUSDC, a test token with no value, on Robinhood Chain testnet, and its Groth16Verifier checks each proof. The contracts are ready for testnet but not deployed yet; see contracts.

Notes and commitments#

When you deposit, your browser makes a note: a random secret and a random nonce, 31 bytes each from crypto.getRandomValues, so both fit in the proof's field. From them it computes a commitment with the Poseidon hash over the BN254 field:

context    = keccak256(abi.encode(chainId, vault)) mod p
commitment = Poseidon(context, asset, amount, secret, nonce)
  asset    = token address as a field element
  amount   = fixed amount in 6-decimal units

The context binds a note to one chain and one vault, so a proof made for one vault fails in any other. The note is saved before the deposit is sent, in a journal kept in your browser and encrypted with your passphrase (AES-GCM, with the key derived by PBKDF2-SHA-256 over 250,000 iterations). You can export the encrypted journal as a backup file and import it in another browser.

Only the commitment, the asset and the amount leave your browser. The vault appends the commitment as the next leaf of a Poseidon Merkle tree. Every insert makes a new root, and the vault accepts the last 64 of them.

Hash
Poseidon over the BN254 scalar field, the same in the circuit, the browser, the server and the contract
Tree depth
20 levels, room for 1,048,576 notes
Node hash
Poseidon(left, right)
Empty leaf
keccak256("sesame.vault.zero") mod p
Root history
Last 64 roots
Demo vault address
0x8fcc02aec01725f6ab3784fadd4b4da9ed459396, derived from keccak256("sesame.vault.v1"); it feeds the context but is not a contract

Deposits come in fixed amounts so that they look alike:

  • Demo ledger, USDC: 100, 1,000, 10,000
  • Demo ledger, ETH: 0.1, 1
  • On-chain, sUSDC: 100, 1,000, 10,000

Your anonymity set is every deposit of the same asset and amount. The deposit form shows how many unspent notes are in the set you picked. A small set hides little.

The note is the money

Whoever has the secret and nonce can withdraw. The server and the contract store only the commitment, so nobody can recover a lost note. Export a backup after each deposit and keep the passphrase somewhere safe.

Notes made before real proofs (version 1, with keccak commitments) are shown as retired and can't be withdrawn. The demo tree was reset when proofs went live, and the demo balances in those notes were not returned.

Withdrawing#

A withdrawal proves the statement in zk/withdraw.circom: “I know a secret and nonce whose commitment is a leaf of the tree under this root, and whose nullifier hash is this one.” The nullifier hash is published when the note is spent. A second withdrawal of the same note gives the same hash and is rejected.

nullifierHash = Poseidon(1, secret, nonce)
public  = [root, nullifierHash, recipient, amount, asset, context]
private = secret, nonce, 20 sibling hashes, 20 left/right bits

The circuit has 11,841 constraints. The recipient is bound to the proof (the circuit squares it), so a proof made for one address fails for any other and whoever submits it can't redirect the funds. The context input ties the proof to one chain and one vault.

Your browser builds the proof with snarkjs. The first time you withdraw it downloads the circuit (/zk/withdraw.wasm, 2.9 MB) and the proving key (/zk/withdraw.zkey, 5.3 MB). Proving takes about a second on a laptop, and the panel shows how long it took.

Demo ledger

Live
  1. Your browser downloads every leaf (GET /api/vault/leaves) and rebuilds its own path, so the server doesn't learn which leaf is yours.
  2. It builds the proof and sends the asset, amount, root, nullifier, recipient and proof. The secret, nonce and leaf index stay in the browser.
  3. The server checks that the root is one of the last 64 and the nullifier is unspent, then verifies the proof with the same verification key as the on-chain verifier.
  4. It records the nullifier and credits the amount to the demo account behind your session cookie. The recipient is your connected wallet, or “this demo account”. No tokens move on any chain.

On-chain

Ready for testnet
  1. Your browser reads every Deposit event from SesameVault and rebuilds the tree.
  2. It builds the proof, and your connected wallet calls withdraw(a, b, c, root, nullifierHash, recipient, amount) with itself as the recipient.
  3. The contract checks the amount, a non-zero recipient, an unspent nullifier and a known root, then asks Groth16Verifier to check the proof.
  4. It marks the nullifier spent, sends the sUSDC to the recipient and emits a Withdrawal event.

Trusted setup: testnet only

Groth16 needs a one-time setup. Sesame's proving key comes from a single-contributor ceremony run locally (zk/setup.sh). Whoever kept that ceremony's randomness could forge proofs, so this setup is fit for testnet only. Mainnet needs a multi-party ceremony first.

Errors you may see when withdrawing (contract error names in brackets):

  • Root no longer in the root history (UnknownRoot): more than 64 deposits landed since you read the tree. Build the proof again.
  • Nullifier is spent (NullifierSpent): this note was already withdrawn.
  • Proof does not verify (InvalidProof): the proof doesn't match the root, nullifier, recipient, asset or amount, for example because the recipient changed after proving.
  • On chain only: NotADenomination, ZeroRecipient, and at deposit NotInField, CommitmentExists or TreeFull.

What stays public#

The vault hides one link: which deposit paid for which withdrawal. Everything else about a deposit or a withdrawal is as public as a pool trade. This table covers both modes and says where they differ.

Asset and amount
Public pools: Public. Every swap, LP position, perp, option and curve trade records the asset, size and price.
Shielded vault: Public at deposit and at withdrawal, but always one of the fixed amounts (demo ledger: USDC 100, 1,000, 10,000; ETH 0.1, 1; on-chain: sUSDC 100, 1,000, 10,000).
Depositor address
Public pools: Public. Each trade is tied to the trader: an address on chain, a demo account in the ledger.
Shielded vault: Public. A deposit is an ordinary transfer into the vault, from your wallet on chain or from your demo account on the ledger.
Withdrawal recipient
Public pools: Not applicable: proceeds go back to the trader.
Shielded vault: Public. In the Withdrawal event on chain; stored with the nullifier on the demo ledger.
Who paid the gas
Public pools: Public: the trader's wallet.
Shielded vault: On chain, public: whoever sends the withdraw transaction. The app sends it from your connected wallet, which is also the recipient, and there is no relayer yet. Withdrawing with the wallet you deposited from links the two.
Timing
Public pools: Public: block and time of every action.
Shielded vault: Public: block and time of each deposit and each withdrawal. A withdrawal soon after a lone deposit is easy to link.
Which deposit funded a withdrawal
Public pools: Not applicable: the same account trades and receives.
Shielded vault: Hidden among the notes of the same asset and amount, in both modes: the proof doesn't say which leaf, and nobody is told which path you built. Caveats: on a fresh vault the set is small; timing, amount and who paid the gas can still link you; and on the demo ledger the server sees your session cookie, because it credits your demo account, so demo-mode privacy is limited to the proof itself.
Note secret, nonce and leaf
Public pools: Not applicable.
Shielded vault: Never leave your browser. The path is rebuilt from the public leaf list (demo ledger) or from Deposit events (on chain).
Commitment and nullifier
Public pools: Not applicable.
Shielded vault: Public. Commitments are tree leaves; a nullifier hash is published when its note is spent. Without the secret, one can't be matched to the other.
Swap size and route
Public pools: Public: pool, side, size, price and fee.
Shielded vault: Shielded swaps are not offered yet. To trade, you withdraw first, and that trade is fully public.

On the demo ledger, the server still knows your account

The proof hides your leaf from the server, but deposits and withdrawals both use your session cookie: the ledger records which demo account deposited and which one was credited. Whoever runs the server can link them by account. The public activity feed and /api/events do not include account ids.

Security#

What could go wrong, what Sesame does about it, and where the demo falls short. The demo holds no real funds, so much of this describes what a deployment has to meet.

Threat model#

  • Stolen or lost notes

    A note is a bearer secret. Anyone who has the secret and nonce can withdraw it, and a lost note can't be recovered.

    Defence. Notes are made and kept in your browser, encrypted with your passphrase, and the backup file is the same encrypted journal. The secret never leaves the browser, not even when you withdraw. Treat the backup and passphrase like cash.

    Today. Same in both modes.

  • Malicious frontend

    The page you load makes your secret and serves the prover. A modified page could use weak randomness, copy notes out, ship a different prover, or deposit a commitment that isn't yours.

    Defence. Secrets come from crypto.getRandomValues, and the commitment formula is published, so a deposit can be recomputed independently. A deployment also needs a way to check the page and the prover files themselves, for example published source with reproducible builds. That doesn't exist yet.

    Today. The page comes from the same server that runs the ledger, so you are trusting both.

  • Malicious relayer

    A relayer submits your withdrawal so the recipient doesn't need gas from a linked address. A dishonest one could try to change the recipient or sit on the transaction.

    Defence. The recipient is bound to the proof, so a proof made for one address fails for any other; a Foundry test checks exactly that. A relayer could still delay or refuse.

    Today. There are no relayers yet. On chain your own wallet sends the withdrawal, and the gas payer is public.

  • Replay across chains or vaults

    A note or proof made for one vault could be replayed against another vault, or against the same vault on another chain.

    Defence. Every commitment hashes in a context, keccak256(abi.encode(chainId, vault)) mod p, and every proof takes it as a public input. The contract computes its own context from the chain it runs on and its own address, so a proof for any other chain or vault fails. Nullifiers are tracked per vault.

    Today. Implemented: the demo ledger uses chain id 46630 and its placeholder vault address in the context.

  • Stale or forged roots

    Every proof refers to a root. If the vault accepted any root, a made-up tree could prove a note that was never deposited. If it accepted only the newest, every deposit would break the proofs in flight.

    Defence. The vault accepts only roots it produced itself, and only the last 64. Older or unknown roots are rejected (UnknownRoot on chain) and the proof has to be built again.

    Today. Implemented the same way on the demo ledger and in the contract.

  • Double spend

    Withdrawing the same note twice.

    Defence. Spending a note publishes its nullifier hash, which is fixed by the note's secret and nonce. The vault rejects any nullifier hash it has already seen (NullifierSpent on chain).

    Today. On the demo ledger the nullifier is checked again inside one database transaction after the proof is verified, so two requests at once can't both pass.

  • Forged proofs from the setup

    Groth16 needs a one-time trusted setup. Whoever keeps that setup's secret randomness can make valid-looking proofs for notes that were never deposited, and empty the vault.

    Defence. Sesame's setup is a single-contributor ceremony run locally (zk/setup.sh), marked testnet-only in the script and in the generated verifier. Mainnet needs a multi-party ceremony first.

    Today. Acceptable for test tokens with no value. Not acceptable for real funds.

  • Linking by metadata

    The proof hides which leaf you spend, but timing, amounts and who paid the gas can still tie a withdrawal to a deposit.

    Defence. Fixed amounts make deposits look alike. Waiting helps, and so does withdrawing with a wallet that has no link to the one you deposited from, though that wallet still needs gas from somewhere. A relayer would hide the gas payer; none exists yet.

    Today. The demo ledger also sees your session cookie on both deposit and withdrawal, so it can link them by account.

  • Oracle manipulation

    Perps and options pay out on a price. Whoever can move that price can force liquidations or inflate a settlement.

    Defence. Perps and options read the reference feed, not the pools, so pushing a thin demo pool moves neither. The weak point is the feed itself: one unofficial source, delayed for stocks, which falls back to the last good value or a snapshot (marked not live) when it fails. Options wait up to 6 hours for a live price before settling on a snapshot. A deployment needs several independent sources and limits on how stale a price may be.

    Today. As described. Perps still open and close on a snapshot price when the feed is down.

  • Admin keys

    Whoever holds admin or upgrade keys can change the rules, pause withdrawals or move funds.

    Defence. SesameVault, Groth16Verifier and DemoUSD have no owner, no admin functions and no upgrade path: the vault's verifier, token and context are fixed when it is deployed. A new circuit means a new vault. Contracts for the rest of the app don't exist yet; when they do, this section needs to list every admin power they carry.

    Today. The server operator controls the ledger completely and can change any balance, pool or record.

  • Demo account theft

    Your demo account is a random 128-bit id in a cookie. Anyone who learns it controls the account.

    Defence. The cookie is httpOnly and SameSite=Lax (and Secure in production), so page scripts can't read it. The activity feed never shows account ids.

    Today. This is a demo-ledger concern: the on-chain vault uses your wallet, not a cookie.

Simulated and real#

Demo mode is real about prices, math and proofs, and simulated about money and the chain.

Real

  • Reference prices and candles from Yahoo Finance and DefiLlama.
  • AMM math: DLMM bins, the DAMM constant product, the dynamic fee, and fee accounting per bin and per share.
  • Perp margin, liquidation-price and fee arithmetic; Black-Scholes premiums for options; bonding-curve math.
  • Zero-knowledge proofs: Groth16 proofs of the withdraw circuit, built in your browser with snarkjs and checked by the server or by the Groth16Verifier contract.
  • Note cryptography: Poseidon commitments and nullifier hashes, the depth-20 Poseidon tree, the 64-root history and the nullifier registry.
  • The vault contracts: SesameVault, Groth16Verifier and DemoUSD, with 8 Foundry tests passing that use real proofs. Ready for testnet, not deployed yet.
  • The encrypted note journal in your browser (AES-GCM, PBKDF2-SHA-256).
  • Wallet connection to Robinhood Chain testnet (chain id 46630); the status page makes real RPC calls to its endpoints.
  • Transaction hashes are real keccak256 hashes, of ledger records.

Simulated

  • The chain: CHAIN_MODE=off, no deployed contracts, one SQLite ledger, and block numbers from a 250 ms clock.
  • Money: demo balances, starting amounts and the faucet, including balances in the demo-ledger vault. Stock tokens are ledger entries that follow a price; they are not shares.
  • Trusted setup: a single-party ceremony run locally, fit for testnet only.
  • Privacy plumbing: there is no relayer, so withdrawing from your own wallet reveals the gas payer; and the demo ledger credits the account behind your session cookie.
  • Trading activity: 7 days of seeded history, keeper arbitrage, random flow, and the seeded launches.
  • SEED's price: there is no outside market, so it starts from a seeded random walk and then follows its pool.
  • Timing: liquidations happen when an account is loaded, options settle on the first read after expiry, and buybacks run on request.
  • Counterparties: perp PnL comes from the ledger, and option collateral from a protocol vault seeded with demo USDC.

Data sources and licences#

Yahoo Finance chart endpoint

query1.finance.yahoo.com/v8/finance/chart/{symbol}

Prices and candles for the stock tokens and ETH. Public and keyless, but unofficial: it isn't a documented API and can change or stop without notice. Stock quotes are delayed. It has no published licence for reuse, so treat its numbers as indicative. Sesame caches prices for 60 seconds and candles for 5 minutes, and labels every price with its source.

DefiLlama coins API

coins.llama.fi/prices/current/coingecko:ethereum

The ETH price when Yahoo Finance doesn't answer. Public, free and keyless.

Robinhood Chain public RPCs

rpc.testnet.chain.robinhood.com/rpc, fallback robinhood-testnet.drpc.org

Used by your wallet connection and by the status page, which asks them for the latest block and tries the fallback when the first one fails. The demo ledger sends no transactions. Chain details follow chainid.network (opens in a new tab) and Robinhood's chain documentation.

Proving and contract libraries

/zk/snarkjs.min.js, /zk/withdraw.wasm, /zk/withdraw.zkey, served by this app

snarkjs 0.7.6 (GPL-3.0) builds proofs in your browser and checks them on the server. circomlib 2.0.5 (GPL-3.0) supplies the circuit's Poseidon. poseidon-lite 0.3.0 (MIT) hashes in the browser and on the server; poseidon-solidity and OpenZeppelin Contracts 5.6.1 (both MIT) run on chain. The generated Groth16Verifier.sol carries the GPL-3.0 header from snarkjs.

Snapshot prices

src/lib/tokens.ts

If every feed fails, the app uses a price snapshot built into the code and marks it not live everywhere it appears.

Logos and names

No third-party logos

Tokens are shown as monograms: up to three letters of the ticker on a coloured disc, and a seed glyph for SEED. Company names only say which stock a demo token follows.

Contracts#

Three contracts for the shielded vault are written, tested and ready for testnet. The rest are planned. None is deployed on any chain yet.

Solidity contracts
ContractJobStatus
SesameVaultTakes sUSDC deposits of 100 / 1,000 / 10,000, keeps the depth-20 Poseidon tree with its last 64 roots and the spent nullifiers, and pays out withdrawals whose proof checks out.Ready for testnet
Groth16VerifierGenerated by snarkjs from the withdraw circuit and the testnet setup. Checks a proof against its six public inputs.Ready for testnet
DemoUSDsUSDC, a 6-decimal test token with no value. Anyone can take 10,000 from its faucet once an hour.Ready for testnet
ShieldedLPManagerHolds public-pool LP positions on behalf of shielded notes, for the shielded-swaps phase.Planned
SesameRouterEntry point for swaps and liquidity: checks the minimum output and calls the pools.Planned
DLMMPoolOne binned pool: bins, the active bin and per-bin fee accounting.Planned
DAMMPoolOne full-range constant-product pool with the dynamic fee.Planned
CurveLaunchpadBonding-curve sales, graduation and the LP split into a DAMM pool.Planned
OptionsVaultSells capped calls against locked USDC collateral and settles them in cash at expiry.Planned
PerpsEngineMargin, positions and liquidations against the oracle price.Planned
AssistanceFundReceives protocol revenue, buys SEED in the SEED/USDC pool and burns it, and sends 1% to the treasury.Planned

The vault's interface, from contracts/src/SesameVault.sol:

  • deposit(commitment, amount) and withdraw(a, b, c, root, nullifierHash, recipient, amount).
  • Events Deposit and Withdrawal. The app rebuilds the tree from Deposit events, so it never asks anyone for a path.
  • Errors NotADenomination, NotInField, CommitmentExists, TreeFull, UnknownRoot, NullifierSpent, InvalidProof and ZeroRecipient.
  • No owner and no admin functions. The verifier, token and context are fixed at deployment.

Eight Foundry tests pass. The withdrawal tests use real Groth16 proofs, made through ffi by zk/cli.mjs with the same circuit and proving key as the app. On a local anvil chain a deposit used about 882k gas and a withdrawal about 315k. The tree and the nullifier registry, once planned as separate contracts, live inside SesameVault.

Deployment to Robinhood Chain testnet is waiting for the deployer account to be funded. Once it happens the addresses appear in src/contracts/deployments.json and the vault panel offers the on-chain mode. Until then, the addresses shown in the app for the demo vault and for each pool are derived from hashes of names. They are placeholders, not deployed code, and sending tokens to them on any chain would lose them.

Roadmap#

Phases are numbered in the order they were planned. Real proving went live before the testnet deployment finished. None of the remaining work has a date.

  1. Phase 1: Demo ledger

    Live

    Pools, perps, options, launches, analytics and the vault on one SQLite ledger, with real reference prices and real proofs. Balances and notes on the ledger exist only there; nothing in it is a claim on tokens on any chain.

  2. Phase 2: Testnet contracts on Robinhood Chain testnet

    In progress

    The vault contracts (SesameVault, Groth16Verifier and the DemoUSD test token) are written and pass their tests with real proofs. Deploying them to chain id 46630 is waiting for the deployer account to be funded; addresses will appear in src/contracts/deployments.json.

    Still to do: the deployment itself, then the other planned contracts and switching CHAIN_MODE from off to testnet.

  3. Phase 3: Real proving

    Live

    A Groth16 circuit for the withdraw statement, proved in your browser with snarkjs and verified by the server or the contract. The proving key comes from a single-party testnet setup.

    Still to do: a multi-party trusted setup before mainnet.

  4. Phase 4: Shielded swaps

    Planned

    Trade from inside the vault without withdrawing first, so a swap is not tied to a public address.

  5. Phase 5: Independent security review, then mainnet

    Planned

    An outside review of the contracts and the circuit. Mainnet comes only after it.

API#

The app talks to its server through JSON endpoints under /api. They are the demo ledger's interface, not a stable public API, and can change without notice.

  • Errors come back as { "error": "…" } with a 4xx status and a message written for people. Anything unexpected returns 500 with a generic message.
  • Writes that act for you open a demo account on first use and set its cookie. Quotes with dryRun don't.
  • Responses are sent with cache-control: no-store.
  • GET/api/health

    Health check: whether SQLite answers, the chain mode, network and commit. Returns 503 if the database is down.
  • GET/api/status

    Live checks: database, demo clock, vault, contracts (not deployed), each reference feed and the Robinhood Chain RPC endpoints.
  • GET/api/markets

    Every pool with price, TVL, 24h volume and fees, plus the reference prices they follow. Runs the keeper first.
  • GET/api/pools/:id

    One pool: state, liquidity by bin (DLMM, 40 bins either side of the active bin) or sampled depth (DAMM), the last 40 trades and its reference price.
  • GET/api/pools/:id/candles

    ?interval=5m|15m|1h|4h|1d&source=pool|reference. Pool candles come from the pool's trades, simulated ones included; reference candles come from Yahoo Finance (USDC pairs only).
  • POST/api/swap

    { pool, side, amountIn, minOut?, dryRun? }. With dryRun: true it returns a quote and changes nothing.
  • POST/api/liquidity

    { action: "add", pool, amountQuote, amountBase?, lower?, upper?, shape? } with bin offsets for DLMM, or { action: "remove" | "claim", position }.
  • GET/api/reference

    ?symbols=ETH,TSLA. Reference prices with their source and a live flag.
  • GET/api/reference/candles

    ?symbol=ETH&interval=5m|15m|1h|1d. Candles of a reference market from Yahoo Finance.
  • GET/api/account

    Your demo account: balances, LP positions, perps, the last 50 swaps and your activity. Returns { account: null } before your first action.
  • POST/api/account

    { action: "open" | "faucet" | "wallet", wallet? }: open an account, use the faucet, or store a wallet address as a label.
  • GET/api/events

    ?limit=30&lane=public|shielded|system&ref=…. The activity feed, newest first, up to 200 rows, without account ids.
  • GET/api/perps

    Perp markets with oracle price, 24h change and open interest, plus the fee rate and maintenance margin.
  • POST/api/perps

    { action: "open", market, side, collateral, leverage } or { action: "close", id }.
  • GET/api/options

    Settles expired series, lists missing ones, and returns series, your positions, settlements, activity without account ids, analytics and the vault state.
  • POST/api/options

    { action: "buy", seriesId, qty, maxCost? }. Fails with 409 if the fresh premium plus fee is above maxCost.
  • GET/api/launch

    Every launch with progress and stats, plus the templates.
  • POST/api/launch

    { template, name, ticker, description?, initialBuy? }. Creates a launch, with an optional first buy.
  • GET/api/launch/:id

    One launch: curve state, recent trades, LP claims, the DAMM pool once migrated, and your balances.
  • GET/api/launch/:id/candles

    ?interval=5m|15m|1h|4h|1d. Candles from the launch's curve trades.
  • POST/api/launch/:id/trade

    { side, amount, minOut?, dryRun? }. Buy with USDC (fee included) or sell tokens on the curve.
  • POST/api/launch/:id/migrate

    Once the threshold is reached, moves the raised USDC and reserved tokens into a DAMM pool. Anyone can call it.
  • POST/api/launch/:id/claim

    { claim }. Moves an unlocked LP allocation out of escrow into your own DAMM position.
  • GET/api/analytics

    Volume, TVL, protocol revenue and the assistance fund, all read from the ledger. Runs the keeper first.
  • POST/api/analytics

    Processes pending revenue: values it in USDC, buys and burns SEED with 99%, sends 1% to the treasury. Skips below 1 USDC.
  • GET/api/vault

    Demo-ledger vault: root, leaf and nullifier counts, anonymity sets per denomination, liabilities, recent roots, hash and proof system.
  • POST/api/vault

    { action: "deposit", asset, amount, commitment } or { action: "withdraw", asset, amount, root, nullifier, recipient, proof }, where proof is a snarkjs Groth16 proof. The server verifies it and never receives the secret, nonce or leaf index.
  • GET/api/vault/leaves

    Every commitment in insertion order, plus the current root, so a browser can rebuild its own Merkle path without saying which leaf it wants.

28 handlers in total. All except /api/health go through one wrapper, which makes sure the demo ledger is seeded before answering.

Why “Sesame”#

The name comes from the story most often used to explain zero-knowledge proofs, the idea the shielded vault is built on.

Zero-knowledge proofs were introduced by Shafi Goldwasser, Silvio Micali and Charles Rackoff in “The knowledge complexity of interactive proof-systems”, presented at STOC in 1985 [1] and published in the SIAM Journal on Computing in 1989 [2]. A zero-knowledge proof convinces a verifier that a statement is true without revealing anything beyond that.

Jean-Jacques Quisquater and Louis Guillou, writing with their families and with Tom Berson for the English version, explained the idea as a children's story: “How to Explain Zero-Knowledge Protocols to Your Children”, in the CRYPTO ’89 proceedings, published in 1990 [3].

In the story, Ali Baba chases thieves into a cave whose entrance forks into two passages, both ending in a dead end, and every thief gets away. Hiding at the end of one passage, he hears a thief whisper the magic words, “Open sesame.” A wall slides open, joining the two passages.

Centuries later a researcher called Mick Ali wants to show a television reporter that he knows the words, without saying them. Mick goes into one passage alone. The reporter walks to the fork, flips a coin and calls out which side Mick must come out of. Someone without the words would fail half the time; the scene is played forty times and Mick never fails, so the reporter is convinced.

A rival network then films the same scene with a look-alike actor who doesn't know the words, and keeps only the takes that worked. In court, nobody can tell the two tapes apart. That is the zero-knowledge part: the real demonstration tells a viewer no more about the secret than a fake made without it.

Later retellings usually call the two roles Peggy, the prover, and Victor, the verifier, and draw the cave as a ring with a door at the back.

The cave and the seed

The logo is a cave doorway holding a sesame seed. In Sesame, your note is the magic phrase: a withdrawal convinces the vault that you know the phrase for one of its notes, without saying which note or what the phrase is. The seed's gold marks the shielded lane across the app.

You never whisper the phrase to anyone. Like Mick Ali in the cave, you prove that you know it: your browser builds a Groth16 proof, and the vault checks it without learning the secret or which note is yours. How that works is under withdrawing.

References

  1. [1]Shafi Goldwasser, Silvio Micali and Charles Rackoff. “The knowledge complexity of interactive proof-systems.” In Proceedings of the 17th Annual ACM Symposium on Theory of Computing (STOC ’85), pp. 291–304. ACM, 1985. doi.org/10.1145/22145.22178 (opens in a new tab)
  2. [2]Shafi Goldwasser, Silvio Micali and Charles Rackoff. “The knowledge complexity of interactive proof systems.” SIAM Journal on Computing 18(1), pp. 186–208, 1989. doi.org/10.1137/0218012 (opens in a new tab)
  3. [3]Jean-Jacques, Myriam, Muriel and Michaël Quisquater; Louis, Marie Annick, Gaïd, Anna, Gwenolé and Soazig Guillou; in collaboration with Tom Berson for the English version. “How to explain zero-knowledge protocols to your children.” In G. Brassard (ed.), Advances in Cryptology — CRYPTO ’89 Proceedings, Lecture Notes in Computer Science 435, pp. 628–631. Springer, 1990. doi.org/10.1007/0-387-34805-0_60 (opens in a new tab)