Tooling
bivium-protocol-cli and bivium-protocol-mm are the stack's least visible and most load-bearing repositories. The first defines the protocol's vocabulary for everything off-chain; the second is the first consumer of it, and is itself a library for the curator.
They are a pair, and the chain they form is the stack's main structural rule:
bivium-protocol-cli → bivium-protocol-mm → bivium-curator
(the SDK) (quoting) (pool operations)
↘ bivium-protocol-frontend
bivium-protocol-cli, the shared vocabulary
Three workspace packages: @bivium/protocol-sdk, @bivium/protocol-cli, @bivium/protocol-mcp.
What the SDK owns
Protocol types, ABIs, Offer encoding, signature domains, funding payloads and quote-grid search.
Owning them in one place is the point: a runner, a curator and a web app that each kept their own
encoding would drift, and the drift would show up as a signature that does not recover rather than
as a test failure. So bivium-protocol-mm has no local protocol.ts or quote.ts at all, and the
frontend's protocol constants are pinned against vectors taken from the contracts.
The Offer's shape is the clearest case. It carries a trailing permit2 field, and the field
changes the Offer hash even at zero, so pre-Permit2 signatures cannot be reused. Every Offer must
state permit2: "0" explicitly; a nonzero mode is rejected by quote, signature, fill, lending,
collar and hedge preparation with UNSUPPORTED_FUNDING_MODE. Parsing and hashing accept the full
range, so the restriction is in what will be prepared, not in what can be read.
Pricing is published, not private
The core has no oracle and no liquidation, so a lender's credit is a bond short the borrower's walk-away — an option. Pricing credit means pricing that option, and the SDK publishes the arithmetic rather than leaving every lender and market maker to keep a private copy:
optionFractionOfFace, fairCreditPrice |
the option inside the credit, and credit's fair price |
aprFromPrice, priceFromApr |
the book's APR convention |
impliedSigma |
the volatility a quoted APR implies net of funding — or below-floor when the quote gives the option away |
assignmentProbability |
the chance the lender is assigned, identical on both legs |
realizedVolatility, breakEvenVolatility |
measuring σ where there is no implied volatility to read |
This is why "fair" means one thing across the stack: the lend door
shows a lender the same arithmetic the curator marks a pool's fills against, with the same
FAIR_BASE_APR default.
Two disciplines travel with it:
- Advisory only. These are floats. Nothing here enters an amount, a tick, a signed offer, an
approval or settlement. A target APR becomes a tick through
Exchange.quoteon-chain, never through this module. The exact/advisory line is the same one the whole stack draws: identities and amounts are exact integers, judgement is float and never binding. - No silent convention.
realizedVolatilityrequiresperiodsPerYearexplicitly, because the same series annualises about 20% apart under a trading-day convention and a round-the-clock one. A default here would be a wrong answer that looks right.
market-data likewise shares normalized Deribit and Derive index/IV surfaces, so mm and curator
interpolate the same way while keeping their own strategy and pair-unit policy.
The CLI and MCP prepare; they do not act
They never sign, never broadcast and never hold a key. Operations are read-or-prepare, and the
repository is blunt about what that does and does not mean: unsigned preparation is not execution
and not a permanent authorization, approval_required means approvals happen externally and the
operation is then prepared again, and a sign request is unsigned but signing it externally can
authorize economically significant fills, including mint-enabled debt.
Configuration is explicit by design — an absolute --config JSON file, no deployment defaults, no
RPC default, no environment keys, no per-call overrides, and uints as decimal strings rather than
JavaScript numbers. On every operation it verifies chain identity, role code hashes and the
links between roles, pins one block and rechecks its hash before returning, and rejects a head more
than 30 seconds ahead of wall clock. Approvals are finite, exact and per-token or per-id, excess
allowances are reduced by zero-reset first, and a global Core operator or a MAX cap is rejected
where it exceeds scope.
The MCP server is the same catalog over stdio, filtered by the capabilities the config declares — a Protocol-only configuration exposes the subset that needs no Lending deployment, and adding the Lending, Collar and Hedge roles exposes the rest. Transport limits are explicit (bounded concurrent calls, input and result caps, read deadlines) and there is no HTTP transport.
The README states the operation count in four places and they do not agree, which is the same failure this documentation avoids by not restating per-repo numbers. Take the catalog from
packages/sdk/OPERATIONS.mdand the capability filter, not from a count.
Verifying a candidate
contracts:prepare builds against fixed commit pins of all three private upstream repositories,
with clean-tree checks and the upstream tests. A missing artifact is an integration failure, never a
skip. That is what makes "this SDK matches those contracts" checkable rather than assumed, and it is
the reason the repo describes its upstreams as fixed candidates rather than latest main.
bivium-protocol-mm, quoting
What it owns, and what it refuses to own
Configuration, scheduling windows, funding budgets, spot policy, execution orchestration. Not protocol types, not the quote grid, not a compatibility mode for the old core.
A one-shot quote engine, not a daemon
Each run emits a bundle of signed offers for an aligned window that by default does not overlap the
next (expiry = nextStart − 1). Non-overlapping has a cost: a run quoting the next window posts
some seconds after it starts, so back-to-back windows leave the book empty for that long every
cycle. The optional overlap keeps each window's offers valid that far into the next one, so the
old quotes stand until the new ones are up — at the price that while two windows overlap their
offers together may promise more than the maker can fund, and a fill beyond it reverts.
Either way, the window model has consequences worth knowing before operating it:
- A signed output is a bearer order until it expires or is cancelled on-chain. The spot service cannot revoke what it already signed; a stale price suppresses the next quote, not the last one.
- Levels share a cap.
maxBidAssetsis the aggregate payment cap per window across all markets and ladder levels, not a budget per level. Levels are alternative prices, not independently replenished tranches. - Cancellation is on-chain:
Exchange.setConsumed(group, max), orInvestModule.cancelGroupfor an Earn pool. Emitting a new bundle does not replace existing signatures. Wait out existing orders before changing TTL or funding.
Nothing is dropped silently. Every configured level that is not quoted appears in exactly one of two
buckets, and they mean different things: skipped is a configuration problem — a level whose
reference cost exceeds maxBidAssets can never be funded by any window — and exits 2.
unfunded is normal operation — no budget left this window, nothing idle in the account, no
credit to sell — and does not change the exit code.
Three funding kinds, three very different authorities
This is the part to read carefully, because the kinds look parallel in the config and are not:
| Kind | Who holds the capital | What the maker can spend |
|---|---|---|
wallet |
the maker | its own balance and approval |
aave |
an AaveFundingAccount |
the entire account |
earn |
an InvestModule |
only what the module's mandate allows |
Treat the middle row as unavailable. The mode is real in the runner, but there is no deployed funding account to point it at, so a configuration naming it has nothing to fund from.
The middle row is also the one to say out loud when it does land: an authorized maker on a funding
account has full spending authority over it. That is not a passive-depositor vault and not a limited quoting
permission, and it must not be described as one. The Earn row is the opposite — the quoter key
cannot exceed the module's mandate, which checkBid(offer, units) enforces at quote time and the
module's ratifier enforces again at fill. No owner or curator key is required to quote.
Preflight reads are pinned to one block, and Aave capacity is checked by simulating the required withdrawal, which catches a pause or a liquidity shortfall at the snapshot — not a change between snapshot and settlement.
TBV markets
controllerKind: "tbv" points the runner at a TbvController, whose market has no lot size and no
gate, and whose series lock vaultBTC, which no spot source quotes. The runner reads the app from the
controller and checks that the app names it back, requires each series to lock the app's vaultBTC,
and prices it as the app's TBVBTC, which the app backs 1:1 — so the priced pair comes from the chain,
not from the config. Minting asks are refused, because only the app mints TBV credit. An Earn pool on
TBV markets is quoted this way, and its bids are filled through the app's borrowAgainst rather than
Exchange.fill (Lending).
Guards
Pair mismatch, a missing, stale or future price, and floor ≥ spot each suppress both sides of a
market. Optional venue market data (Deribit or Derive) is advisory: APR-derived ticks and signed
offers do not use it, provider failure or a wrong collateral mapping suppresses that market, and
there is no venue fallback. Choosing a venue currency is an explicit assertion that the token
tracks that underlying — a matching address does not establish economic equivalence, and wrapped
basis risk remains.
The RPC transport (src/rpc.ts, exported for the curator) handles two realities of a shared public
endpoint: a rate-limit reply is waited out and retried rather than failing the run, and a read
pinned to a just-read block may meet a load-balanced node that has not reached that height, which is
retried briefly. Other errors are not retried.
The second product: a hedged option writer
Separately from lending credit, mm writes fully collateralised European puts and calls on bivium-protocol and hedges each sale back-to-back on Deribit. A listing whose exact strike and expiry is not listed on Deribit is skipped, because it has no exact hedge.
Quoting sells the option's long with allowMint, so a fill pulls the maker's collateral into the
core and mints — the buyer's protection is collateralised on-chain from that moment, whatever
later happens to the maker or its Deribit account. The premium is the venue ask plus taker fee
plus an edge, rounded up onto the tick grid.
The hedge loop is built to survive a crash rather than to be fast: fills are written to the state
file before any order is sent, and the plan is then reconciled against the positions Deribit
reports, so a crash re-plans from the venue's own position and never counts a fill twice. It refuses
a hedge costing more than a tolerance above the premium received, reports notional it cannot hedge,
and --state stops quoting once that reaches the configured cap.
With --post, signed asks go to a bivium-protocol-frontend relayer, and an ask counts as posted
only when the relayer returns the same offer hash. That closes the loop: the quoting runner and the
web app's book are the same book.
The writer is not BTC-specific. TBVBTC is an ordinary ERC-20 that the spot service prices off BTC
1:1 (/pair?collateral=TBVBTC&loanToken=USDC), so puts on it needed configuration and no code: the
grid maker quotes a TBVBTC/USDC put grid on the options cron, plus named puts at the strike and
maturity of each TBV market whose maturity falls on a Deribit expiry — a TBV lender's default hedge,
back to back with its loan. The settler keeper values TBVBTC strikes from the same spot pair. On the
testnet neither grid is hedged on Deribit.
Bidding for calls on vaults
TbvCallBook (bivium-protocol-tbv) holds only writers' asks, so the tbv-call-bids lane makes
that market as a buyer, with the grid maker's key. Its bids are firm on TbvCallBids, which a writer
sells into as the taker (Lifecycle):
- Sheet. For Deribit expiries 2–90 days out, it prices European calls (window: the day before
the 08:00 expiry) at listed strikes from 5% above the index, the same way as the collar bids: what
a same-strike Deribit call expiring no later fetches at its bid, less the fee and an edge. The
sheet is signed as its exact text (an EIP-191 message over its hash) and posted to the frontend
relayer's
/api/call-bids, valid for minutes. - Firm bids. With
callBidsset, each row is also signed as aCallBid(sheet schema/2).- One group, one cap. Every row shares one group, capped at what the group has bought plus the room left under the open-sats cap. A fill raises both by its sats.
- The take lane leaves promised room free. It reads the last posted
/2sheet's cap back from the relayer and keeps that room (the cap less what the group has bought) out of what it buys; if it cannot read it, it buys nothing that run. So each run's cap is at least the last, and the newest cap bounds every bid still live from earlier runs. With firm bids on, only/2sheets are posted, so that record is never replaced. - The allowance is the spend bound. The allowance to
TbvCallBids(never to the book) is set each run, raised or lowered, to the lesser of that room at the dearest row's premium and the run's premium budget left after the take lane. It is a hard bound on what all live bids pull before the next run. - Lifetime and size. A bid closes with the sheet and at least an hour before its window, and buys at most the per-call cap.
- Take. It reads
/api/calls, prices each European offer afresh, and buys withbuyCallany that ask no more than its bid plus a tolerance that the config holds below the edge, so it never pays above the venue's value. Caps bound the sats per call, the sats held open and the premium per run, and each buy is simulated first. - Exercise. It finds the calls it holds from the book's
CallSoldandCallTransferredevents, and exercises one inside its window when the vaults' value at the TBVBTC spot beats the strike by a margin. It receives the vaults as TBVBTC.
Like the grid, it is not yet hedged on Deribit.
Keeper lanes
mm also runs the stack's settlement keepers. Each lane is a contract in the product's repo plus a one-shot runner here, and none of them takes custody: each acts through a hook the product built for it.
| Lane | Contract (repo) | What the runner does |
|---|---|---|
settle |
MaturitySettler (bivium-protocol periphery) |
In an armed long's last window, pays the strike and asks only its value at spot plus a margin; out of the money it does nothing |
exercise-collars |
CollarRouter (bivium-option) |
Exercises the calls on loans that the option writer bought, inside their window, when the collateral beats the strike |
tbv-auto-repay |
TbvAutoRepay (bivium-protocol-tbv) |
Repays an armed TBV loan in its last day and takes its cost in the group's TBVBTC; skips a loan worth less than its debt |
tbv-redemptions |
BiviumVaultApp's redemption book and settleDelivered (bivium-protocol-tbv) |
The peg-out keeper. It fills a redemption order once the Dutch-decayed sats due leave its minimum spread, then settles keeper-pool vaults to its AVK key with the TBVBTC above a reserve. It has to be a registered keeper (addKeeper), and it serves only orders and vaults created after that |
The lanes share the grid maker's key, because a keeper pays strikes and debts out of a balance it
already has, and they read the same spot service as the quoting runners. Each lane finds its work from
the contract's own events: FloorSet for the settler and auto-repay, and the Exchange's Filled
events for the calls mm bought, and the app's order list and Delivered / Converted events for the
peg-out keeper. So a runner needs no index of its own. Which positions are armed is decided in the
frontend, which arms by default.
The peg-out keeper is the one lane that has to move value outside the chain: a fill front-pays BTC.
It has one payout mode so far, simulated. It pays nothing, writes SIM-NO-BTC-PAID: and the order
id as the claim's txid, so the RedemptionFilled event says so, and it refuses mainnet and any app
with a payout verifier. A real mode needs a BTC wallet.
The option writer also bids for calls on loans (collars) on the lending markets it is configured
for. It prices them off a Deribit call at the same strike that expires by the call's window close,
falling back to earlier expiries when that book has no bid. Those bids are what the Basic borrow guide's
upside step, Portfolio's "Sell a call" and Pro's desk sell into (Lifecycle,
Frontend). Each bid is for up to collars.notional
(10 BTC on Sepolia), capped like every other offer at the depth share of the Deribit bid it is priced
from, so most land at 7–10 BTC. The size is a product limit, not just a risk setting: a borrower sells
the call on its whole loan into one bid, so the largest bid at a date is the largest loan that can
sell its upside there. At 1 BTC, a 2 WBTC loan had no call to sell. With collars.buyback it also offers the
calls it bought for resale, to the borrower closing early and to anyone buying upside (they are the
options pages' supply): it finds them by strike on the standard window in one
multicall, and asks the Deribit ask plus fee and edge, for all it holds when the depth share of that
ask covers it. Covered amounts are offered exactly, not rounded to Deribit lots, because a borrower can
burn only the amount it sold.
The writer searches the tick grid with tickForCostBatched, not the SDK's tickForCost. Both ask
Exchange.quote on-chain and pick the same tick (a test holds them together), but the SDK asks one
tick at a time, about a dozen sequential calls per offer. From the cron host that was slow enough
that a grid run outlasted its ten-minute schedule and its quotes reached the relayer near expiry. The
mm version prices 16 ticks per multicall and checks hash, digest and ratification in one more, so an
offer costs about four round trips. Each puts run prints a timing line on stderr (RPC and Deribit
requests, time, rate-limit waits) so a slow run says where its time went. The batched search is a
candidate to move into the SDK.
mm as a library
npm run build compiles the put writer to dist/, and npm pack ships only that. The curator
consumes it exactly as mm consumes the SDK, with @bivium/protocol-sdk and viem as peer
dependencies at a pinned range — and check:pack fails on any node: import in what ships,
because the consumer is a Cloudflare Worker. A Node-only convenience in the wrong file is caught at
pack time rather than at the curator's first deploy.
The curator then quotes this writer's ask for each rung's put and takes it with
InvestModule.buyHedge, reading the same writer config, so every put a pool buys is a listing the
hedger already knows about. The Deribit hedge loop stays in mm, with its state file and credentials.
A naming rule worth obeying
The funding kind for an InvestModule is earn, not vault, because in this product a vault is
a TBV BTC vault. A config that still says vault is refused with that hint, as is one still
naming the retired signed-anchor relayer.
bivium-tbv-direct's packages, for v3
The v3 design does not use bivium-protocol-cli's SDK: none of its types — series,
Offers, ratifiers, Permit2 funding — exist on a Direct app. Its tooling lives in its own monorepo,
packages/, built together with the contracts:
bivium-tbv-direct/packages/sdk (@bivium/direct-sdk)
├── mm, keeper, cli, frontend, web (same workspace, built by `npm run build`)
└── tarball → bivium-v3-frontend/vendor/
- The SDK owns the vocabulary here too: ABIs generated from the Foundry build, the EIP-712 type
hashes and digests of the five orders (
LendBid,BorrowIntent,CallOffer,CallBid,ActivationBorrow), loan and collar quotes, call pricing,SeriesMathand app clients. The v3 app imports its digests, maths and ABIs rather than keeping copies. - Held to the contracts by tests.
abis:checkfails when the ABIs drift from the build;series:vectors:checkreplays SDK-computedSeriesMathvectors through the contract library, reverts included. Both run in CI. - mm quotes Direct orders —
LendBids,CallBids and novationCallOffers — and keeper plans keeper actions and runs a regtest peg-in relayer for the devnet. Neither is the bivium-protocol-mm runner, and they share no code with it.
Vendored from bivium-protocol-doc 57ed1bdc6cfe, synced 2026-10-27. Where this and a repository disagree about that repository, the repository wins.