Integration
The rules that hold between repositories. Each one exists because breaking it fails in a way that is quiet rather than loud, which is exactly the kind of thing no single repository's README will catch.
Deploy order
The order is forced by the fact that later contracts are checked against earlier addresses at deploy time, and by the gate: a collar router must be a known intermediary before it can hold a lending long on a gated market.
It inverts the source dependency, and that is not a mistake. bivium-option's code depends on
bivium-lending's (it has it as a submodule), so it compiles second — but LendingController's
constructor needs the CollarRouter address, so it deploys second. Compile-time and deploy-time
run opposite ways here, and reading the submodule order as a deploy order is
how a gated market ends up without a working collar.
- bivium-protocol —
script/Deploy.s.sol. Produces Core, Exchange, ratifiers and periphery. - bivium-option —
script/DeployOption.s.sol.CollarRouterandHedgeRouter, each checked to be wired to that protocol deployment's core and Exchange. - bivium-lending —
script/DeployLending.s.solwithEXTRA_INTERMEDIARIES=<CollarRouter>, so collars work on gated markets. - bivium-lending —
script/RegisterMarket.s.sol, once per market. - bivium-lending —
script/DeployInvest.s.solfor an Earn pool: the module, itsHedgeDesk, and aCollateralDutchBookunless an existing one is given. The broadcaster deploys the module as its own curator, binds the desk, lists the initial quoters, attestors and rungs, and hands the curator role toCURATORin the same run, so a half-configured module is never curated by the wrong key. Rungs on gated markets additionally needgate.setLender(module, true)from the gate owner; the script logs any that are missing. - bivium-protocol-tbv —
script/DeployTbv.s.sol, for TBV markets: vaultBTC, the escrow and the app, which deploys itsTbvControllerandTbvCallBook. The script reads both back against the app and logsTBV_CALL_BOOKfor the frontend's relayer. - bivium-protocol-tbv —
script/DeployTbvInvest.s.solfor an Earn pool on TBV markets: step 5's module, desk and book, given onlyTBV_APP. The core, Exchange, controller, vaultBTC and TBVBTC are read from the app, so a pool cannot be wired to another deployment's tokens, and the app is the module's redeemer. Rungs are checked againstTbvController— registered, in the loan token, not matured, named by the even series id. There is no gate step: TBV markets have no gate.
An Earn pool is unreachable until three other repositories name it: the frontend's deployment file
(earnPools, and selfRatifiers, without which the relayer refuses its bids), a bivium-curator
wrangler.*.toml, and a bivium-protocol-mm quote config — with controllerKind: "tbv" for a pool
on TBV markets.
Deployments are passed forward as JSON (PROTOCOL_DEPLOYMENT, or explicit
BIVIUM_PROTOCOL_CORE / BIVIUM_PROTOCOL_EXCHANGE). A set but malformed value reverts; it never
falls back to a default. That is deliberate — a silent fallback here deploys a contract wired to
the wrong core, and nothing downstream would notice until a fill failed.
Redeploying the core redeploys everything downstream
Core and Exchange addresses are immutable in the contracts bound to them. An Exchange redeploy orphans every module, router and open offer bound to the old one. This has already happened once: an Earn pool deployed 2026-09-29 was bound to a pre-Permit2 Exchange that neither the SDK nor the mm could drive, and was abandoned untraded rather than migrated. Plan a core change as a stack migration, not as a contract upgrade.
Submodule bumps
Solidity dependencies are git submodules under lib/:
bivium-lending → lib/bivium-protocol
bivium-option → lib/bivium-protocol, lib/bivium-lending
bivium-protocol-tbv → lib/bivium-protocol, lib/bivium-lending
A pin is a commit, so a protocol change reaches products only when each product bumps its submodule and its tests are re-run against the new commit. Bump deliberately and in dependency order: protocol first, then lending, then option and tbv. Do not bump a product to a protocol commit that the deployed Exchange does not match — the ABI check will pass locally and the fill will fail on chain.
The SDK release chain
TypeScript consumers do not depend on a sibling checkout or a registry. @bivium/protocol-sdk is
built in bivium-protocol-cli, frozen as a release, and vendored into each consumer as a tarball
whose integrity the lockfile pins:
bivium-protocol-cli releases/sdk/<version>/ ← the frozen artifact + manifest.json
│
├─ tarball → bivium-protocol-mm/vendor/
├─ tarball → bivium-curator/vendor/ (plus a tarball of mm itself)
└─ tarball → bivium-protocol-frontend/vendor/
Three consumers, not two: the web app signs and checks the same offers, so it carries the same frozen release rather than its own encoding.
Each release carries a manifest.json recording the source commit, the upstream lock hash, the
tarball SHA-256 and the npm integrity string, and
node scripts/freeze-sdk-release.mjs --check in that repo verifies a release has not drifted.
Consumers' vendor/README.md records which release they carry and where it came from.
Why this shape: it keeps npm ci --ignore-scripts working with no private token and no sibling
checkout, and it makes "which encoding is this runner actually using" a question with a hash for an
answer. The cost is that an SDK change does not propagate — each consumer re-vendors deliberately.
Four rules:
- Never overwrite a released version with different contents. The version is the identity of an artifact, not a branch name.
- Bump consumers in chain order: SDK, then mm, then curator — curator vendors both, and a curator carrying mm built against a different SDK than the one beside it is the failure this ordering avoids. The frontend bumps independently, since nothing depends on it.
- Copy a release byte for byte. A consumer takes the tarball out of
releases/sdk/<version>/unchanged and pointspackage.jsonat it; the archive SHA-256 in that release'smanifest.jsonis what makes "same encoding" checkable rather than asserted. - The Offer shape is the SDK's. The canonical Offer includes
permit2; the pre-Permit2 Offer is intentionally unsupported. A deployment whose Exchange does not match the SDK's ABI, selectors, signature domain or embedded Offer signature fails the runner's preflight — which is the loud failure you want, so do not route around it.
The Direct SDK
bivium-tbv-direct's @bivium/direct-sdk follows the same shape on a smaller chain: built in that
monorepo, packed (npm pack in packages/sdk) and vendored into bivium-v3-frontend as
vendor/bivium-direct-sdk-<version>.tgz, whose integrity the lockfile pins; the frontend README
records the source commit and the archive SHA-256. There is no releases/ directory or manifest
yet, so that README line is the whole provenance record. The same rule holds: a changed SDK is a new
version, never the old version re-packed.
Checkouts go stale, fast
This stack moves quickly across ten repositories, and a local checkout is routinely dozens of commits
behind its default branch. Before reading code to answer a question — and certainly before
documenting it — git fetch and read the default branch, not the working tree. The same applies to
a repository's own README: a Status section can contradict the Scope section above it after a
feature lands.
Where this document and a repository disagree about that repository, the repository wins.
Verification pins are not current
bivium-protocol-lean4 pins the exact source revisions it modelled. That is the right discipline, and it means a pin dates the review rather than tracking the code. Treat a pinned theorem as evidence about that revision only.
Two things follow for anyone changing the contracts. A change to Core.sol accounting does not
inherit the existing proofs — the model must be re-extracted and re-pinned, and the conformance
corpus re-run. And a change that moves a function invalidates its proofs more quietly than a
change that edits one, because nothing fails: the theorem still compiles against the old pin while
the function it describes no longer exists where it was. That has already happened once — the hedge
files the Lean repo pins are on a bivium-option revision that is not on main, and the hedge role
now sits in bivium-lending's HedgeDesk.
One pin defends itself and one does not, which is why only one of them drifted. The conformance
harness vendors the Core source byte for byte and checks its hashes before replaying anything,
so that pin can only fall behind visibly. The hedge pin has no harness behind it, and it is the one
now pointing at a revision that is on no main.
Verification has the current pin table and what each artifact does and does not claim.
The price boundary
The contracts read no price. Everything that does is off-chain — and there are more such inputs than "the price feed" suggests. They are not interchangeable, and every one of them fails closed:
| Input | Gives | To | When it fails |
|---|---|---|---|
| bivium-protocol-spot | S, the collateral's price now, from a venue index | the curator's mark, the mm's guards | 502; the consumer suppresses that market |
| the SDK's venue surface (Deribit or Derive) | σ, an implied-volatility surface | the curator's mark; advisory only for the mm | that market is suppressed, with no venue fallback |
| the pair's own Uniswap v4 pool | S, for a curator configured MARK_SOURCE = "pool"; σ is then a measurement pasted into the config |
the curator's mark | the mark throws and nothing is valued |
the app's /api/reference |
S and realised σ, from daily candles | the lend door, display only | 502 |
The split that matters is not the count but the consequence. The first three feed machines deciding what to quote and what to mark, where being wrong costs a bad fill. The last feeds a person deciding whether a bid is fair, where being wrong costs a misleading number on a screen and nothing else — nothing from it enters an offer.
See Architecture for why the boundary sits where it does, Curator for how the mark combines the first three, and Frontend for the last.
bivium-protocol-spot
A venue index per allowlisted pair, published in the units each consumer reads:
| Consumer | Configuration | Reads |
|---|---|---|
| bivium-curator | MARK_SPOT_URL |
loanPerCollateral (whole loan tokens per whole collateral), observedAt |
| bivium-protocol-mm | markets[].spotUrl |
ratio (loan base per collateral base × 1e36), observedAt |
One observation serves both; they differ only in scaling, so the two never disagree about what the
price is. GET /pair?collateral=…&loanToken=… answers one pair and GET / lists the configured
ones.
Four properties are load-bearing for everything above it:
- Nothing is inferred. A pair in
PAIRSnames both token addresses, both decimals, the venue index, and the assertion that makes that index this pair's price — for example that a given open-mint test token tracks BTC and another is USDC. The assertion comes back with every observation, so a consumer always sees what it is trusting rather than inheriting it silently. Pairs are directional, and anything unlisted is a 404, never a guess. - Never stale. The index must be at most 30 s old and at most 30 s ahead of our clock — the same
convention as the rest of the tooling — or the answer is 502
spot-unavailable, with no upstream message passed through. Responses areno-store. - Rounded down, the conservative side for both consumers at once: a lower price raises the curator's LTV and lowers its ceiling, and trips the mm's strike-at-or-above-spot guard sooner. Rounding goes through the venue's own decimal, so binary noise cannot cost a unit.
- Allowlisted by address. Anyone can mint a token called WBTC on a test chain, so a pair is keyed by address and admitted deliberately.
Why not the pool
A pool's own price is no price when its tokens are open-mint test tokens: anyone can move such a pool for free, and the curator's ceiling is computed from it. This is not hypothetical — on Robinhood testnet the bUSD/mNVDA pool was uninitialised, nothing else on that chain priced mNVDA, and the one pool that did exist implied $1,346 against a $170 manifest.
The result is the design working rather than an outage: the mark read throws and nothing is valued — fills wait unmarked and nothing acts on bad data. A price that fails toward "pay anything" is worse than no price. Do not add a fallback to get past it; see Curator for which direction each failure has to fall.
Operational: Deribit refuses Cloudflare
Deribit answers Cloudflare's shared egress with 429 — 6 of 6 probes on 2026-10-01, while a local
machine got 200 — so the Worker cannot be relied on to reach it. The same handler runs as a plain
node server (npm run serve, reading the same PAIRS from wrangler.toml) on a host with its own
egress, which is what the live curator reads. The Worker stays deployed and answers 502 whenever the
venue refuses it. CI parses the deployed PAIRS, because a malformed list would answer 503 to
everyone.
Roles are roles, not owners
Market maker and curator describe jobs, and one actor commonly does both — the curator is defined as the market maker plus the Earn pool. The job says nothing about the authority, which comes entirely from how the quoting is funded, and the three funding kinds are not parallel in what they permit. Tooling has the table; the one to carry around is that a funding account's authorized maker can spend the entire account, which is neither a passive-depositor vault nor a limited quoting permission and must not be described as one.
Vendored from bivium-protocol-doc 57ed1bdc6cfe, synced 2026-10-27. Where this and a repository disagree about that repository, the repository wins.