A sealed batch collects orders for one market and crosses them at one rate per side. Hidden: who placed each order and how much each order was. Visible: the market, the number of orders per side, each side's total, the net routed and one rate per side.
The parameters
A market's terms are fixed when it is added. The owner adds markets inside these bounds.
| Name | Allowed | Default | Effect |
|---|---|---|---|
epochLength | 30 - 3,600 s | 60 | A side can seal at each epoch end |
minOrders | 5 - 64 orders | 5 | Distinct orders a side needs per tranche |
maxWait | one epoch - 3,600 s | 600 | From a tranche's first order until a thin one voids |
priceWindow | at least 10 s | 30 | The reference is the price as of this long after the seal |
settleWindow | 30 - 600 s | 120 | From the seal until an unsettled batch voids |
bandBps | 10 - 300 bps | 50 | How far a quote or the routed net may sit from the reference |
Every market has 3 keepers, and all 3 must post: 3 of 3.
How a batch runs
- 1OrderThe sealed_order proof spends your notes and creates an order commitment and a Pedersen commitment to the amount on Baby JubJub. Additive shares of the amount and its blinding are encrypted to the 3 keepers, and the circuit proves all of it. No amount appears in calldata. The contract adds your commitment to the tranche collecting when the order lands, so a slow proof only joins a later epoch. A memo to your viewing key lets your keys find the order again.
- 2SealA side seals at the end of an epoch once it holds at least minOrders distinct orders. With fewer it rolls over into the next epoch. So no side's total ever shows with fewer than minOrders orders behind it, and a thin side holds up nothing.
- 3PriceThe reference is the price as of priceWindow seconds after the seal: a TWAP over exactly those seconds from a deep Uniswap v3 pool, or the Chainlink round in force at that time for stock tokens, in the equity session. Nothing can reveal before then, so no order saw its price and no keeper's timing changes it. A small quote on the market's own route must agree with it inside the band, both ways.
- 4Reveal and settleEach keeper posts the sum of its shares, both sides of a batch in one call. The last post checks that the total opens the aggregate, so keepers can stall but cannot lie. That same post makes the total public in TrancheRevealed and settles the batch, or it reverts and shows nothing. Two sides sealed by the same epoch end settle together. Opposing totals cross at the reference, and the net goes through the Router on the market's fixed route, inside the band. A net too big for the route trades in part, trying up to 8 sizes, each half the last, and the rest comes back to that side in kind. Each side gets one rate, after the 0.10 % fee, and every claim rounds down.
- 5ClaimThe sealed_claim proof shows your order sits in the tranche, publishes its nullifier and turns it into notes at the public rate, without revealing the amount. A relayer sends it and takes its fee from what you claim. Nobody's permission is needed.
An order has no limit price of its own. It takes its side's rate, and the band is what bounds that rate.
When a batch fails
A side that stays under minOrders for maxWait is void, and its total never shows. So is a sealed batch that has not settled within settleWindow of its seal, because a keeper stalled, the price check failed or no route took the net with nothing to cross. A last post that cannot settle reverts and shows nothing, and the keepers try again until the window ends: only time voids.
The sealed_refund proof returns your input as a new note. You do not need anyone's permission and no keeper can stop it: if the time is up and nobody has voided the batch, the refund does. A void batch pays no 0.10 %.
A full tree cannot lock an order. Every open order holds back two leaves of the tree for its claim or refund, and nothing else can use them. When only those are left, notes leave through exitFullTree.
What a batch hides, and from whom
- An amount stays hidden from everyone except all 3 keepers together. A single share is a random field element.
- A side's order count is public from its first order. Its total stays hidden until the transaction that trades it, and for a thin side it never shows.
- The net routed and the rate per side are public.
A batch hides an amount among the other orders of its tranche, and only among the honest ones. The total is public. A party holding all but one of a tranche's orders learns the last order's amount, by taking its own from the total. No minimum can stop that while totals are public.
What the minimum does is raise the price. To read one order out of a default market, someone must place the other four themselves, pay a relayer for each, and hope nobody else joins. A tranche that never reaches five orders shows no total and refunds at maxWait, ten minutes by default. In a quiet market, look at how many orders a tranche holds before you join it, and split a large order across epochs. The sealed ticket warns you when a tranche is thin.
Who placed an order stays hidden either way.
Sending a claim or refund yourself
A claim pays its relayer in what it claims, and a refund in what it refunds. A relayer takes only the fee tokens it lists, so a launch's first buyers, for one, have no relayer for their claim until one prices launch tokens. Whoever sends the transaction is then public, and if that wallet funded your deposit, you are.
So nothing here falls back to your wallet by itself. The app tells you, and you choose a fresh address: withdraw a WETH note to it as native ETH for the gas, then send the claim from it. It never uses the connected wallet. In the SDK, sealedRoute(order, 'claim' | 'refund') says which it will be, sealedClaim and sealedRefund throw SelfSubmitRequiredError unless you pass selfSubmit, which names the address, and sealedOrder takes it too.
Keepers
Keepers run as separate services. Each reads the public order logs, decrypts its own shares and keeps a running sum per tranche. They cannot move funds and they cannot change a total. They can stall, and then the batch voids and refunds. All 3 together could read per-order amounts, but not who placed them. At launch one operator runs all 3, and a new market can name other operators.
A market's keeper keys never change, and its encrypted shares stay in the logs for good, so whoever ever holds all of a market's secrets can read every past order's amount in it. Keys rotate by market: the owner adds a new market with new keeper addresses and keys, and stops the old one taking orders. Each keeper keeps the old secret until the old market's last tranche is done, then destroys it. A keeper's share secret stays on its own machine, and never in the wallet or server that holds other keys.
Markets
SealedMarkets is the owner's market list. The owner adds markets inside the bounds above and can stop one taking new orders. It holds no funds, and nothing the owner does later reaches an order already placed. SealedBatches has no owner.
Each market sets its smallest order per side and its fixed route. A market on a stock token takes orders in the equity session only.
In the app
Swap shows Immediate and Sealed batch when the pair has a market. The sealed ticket shows the epoch countdown, each side's order count and state, the terms above, and a warning when a tranche is thin. Your orders are on /private, with a randomised suggested claim time, so claims do not all land right after a batch. A claim or refund opens a drawer with the same private foot as any spend.
Launches
A launch can open through a sealed batch, so its first buyers share one price. SealedMarkets.createLaunchOpener makes the opener, LaunchFactory.createLaunch names it with an openDelay, and the opener's bind adds a buy-only market whose one fill is BondingCurve.allocate. The create page sends all three from one button. Only the batch's own settlement can make that fill, and an order is the only way into it. The launch takes sealed buys on its page while it waits.
The batch stays open for at least 10 minutes, and it seals the settle window plus 30 seconds before the launch would open, so the delay must leave all of that when bind runs: 12.5 minutes on the default windows. The fee comes off the quote token before the fill, the tokens come back as notes, and whatever the curve's opening cap leaves comes back in the quote token. If the batch fails, the launch opens by itself and the buyers take their refunds. See Launchpad.
contracts/src/shielded/SealedBatches.solorders, reveal, settlement, claim and refundcontracts/src/shielded/SealedMarkets.solthe market list, its bounds and the reference pricecontracts/script/shielded/ShieldedWiring.solthe default parametersapps/keeper/src/keeper.tsone keeper