Frontend
bivium-protocol-frontend is the stack's human surface: one app over both products, lending markets
and TBV whole-vault borrowing. It replaces bivium-frontend, which stays with the old bivium-core.
Two things about it matter at stack level and are easy to miss from outside. It carries the offer relayer — there is no separate relayer repository on this stack. And it is the third consumer of the frozen SDK release, so it signs and checks offers with the same encoding as the market maker and the curator rather than its own.
What it reads, and from where
Everything comes from the chain. There is no indexer and no backend holding protocol state; pages read contract calls and logs directly, and the relayer below stores only signed offers that the Exchange re-checks anyway.
| Page | Reads | Writes |
|---|---|---|
Borrow /markets (Basic) |
Per date and market: the lender bids, the market maker's bids for calls on the loan (locks=), for vaults its TBV call sheet, and spot |
The six-step guide below: the borrow (Exchange.fill per bid, or borrowAgainst for vaults) and the call (CollarRouter.openCollar, or for vaults a signed CallOffer sold into the maker's CallBid with TbvCallBids.sell) |
All markets /pro/markets (Pro) |
Registered markets from LendingController's MarketRegistered logs, series state, best bid (borrow APR) with depth, best ask (lend APR) |
— |
Market /market?id= |
Both sides of the book. A lender bid is sized by the loan tokens its maker can still pay, a borrower ask by the collateral its maker can still lock, each capped by its remaining limit and hidden when its ratifier is off | Borrow (one Exchange.fill per bid, best price first), lend (cheapest ask first), or post a resting bid or ask |
Options /options (Basic), /pro/options (Pro) |
The collar call market: each listed loan market's calls (locks=), priced by Exchange.quote; the cheapest series per strike and window; your own calls from CollarOpened and Filled logs |
Buy from the asks (Exchange.fill); sell what you hold into the bids; in the window take the profit through CollarFlashExerciser. In Basic, bid your own price for a date and strike: one offer on each loan due then, sharing one group (below) |
Earn /earn?pool= |
An InvestModule: NAV, run rate net of hedging, and per rung its lent face, exposure, the cover from HedgeDesk's hedges, the premium paid, and the lowest rate the mandate allows; the mandate's limits; your own requests |
requestDeposit, requestRedeem, claim a settled epoch, harvest a matured rung |
Portfolio /portfolio |
LendingLens positions, Earn holdings, TBV loans, vaults and calls on vaults, your live bids, asks and borrow intents |
Repay, collect, redeem, record a TBV default, cancel an offer or intent, the BTC-vault actions, exercising, releasing, handing on or cancelling a call on vaults, and buying back a call sold on a loan (fill an ask, burnPair, optionally repay) |
Basic borrowing is a six-step guide
Basic has no order book. Borrowing is a guide that asks for one decision per step, in the order a borrower thinks about it:
- What you have: WBTC and how much, or which whole BTC vaults.
- Until when: the dates some lender pays at, each with the most it pays for that collateral.
- Your upside: keep all of it, or sell it above a price. Every price is a call strike the market maker bids for at that date, shown with its premium and the lowest rate it brings.
- Cash and rate: one option per lending market at that date, that is per min price (the loan's strike). Each shows the cash now, the repayment and the rate with and without the call: a higher min price pays more cash at a higher rate.
- Review: the three outcomes at maturity. Confirming reads the books and the call again and asks again if anything moved.
- Sign: the borrow and the call together. For vaults the collar is signed first, on the group
the borrow will create, then
borrowAgainstandTbvCallBids.sellgo out together. Where the maker's bid is only indicative, or the sale fails after the borrow lands, the call is offered instead and paid when the market maker buys it.
Every number on it comes from the repositories around it. The cash and the loan rate are the lender
bids on the relayer, which bivium-protocol-mm posts on the WBTC/USDC markets registered for this guide
(configs/sepolia-target-rate-bids.json, $50k/$60k/$70k at each date). The prices in step 3
are mm's collar call bids. Those markets mature two days after a Deribit expiry, so a collar's window
lines up with the Deribit option mm prices it from (Tooling). A price is offered only
where one bid takes the call on the whole loan, so mm's bid size bounds the loan that can sell its
upside. When the loan is bigger than every bid, step 3 says how much one bid takes rather than that
nobody is buying. Pro keeps choosing the market and the call by hand.
Options are the collar call market
The options pages list no standard series. They show the calls borrowers sold on their loans (Basic: "bet on BTC going up"; Pro: calls on loans by window and strike), each as a European call at K. That is exact for a holder, so the pages' payoffs and breakevens need no special case. The differences are in the plumbing:
- The series is nested, so the take-profit exit goes through bivium-option's
CollarFlashExerciserand owes K: K − D on the call plus D on the loan's long. - An offer's price is per unit of the call's face, K − D, not K, and D differs by loan. The Pro options ticket therefore trades a collar call at market only, and sells only what is held: a collar call is written by a borrower on its loan, never from the options ticket. Pro limit orders stay on the loan's own desk, which prices on that face; Basic places bids itself (next).
- Auto-exercise is not offered: the keeper's
MaturitySettlerexercises standard options only.
Supply exists only after borrowers sell calls (into the market maker's bids, which it then resells), so a network with no collars has nothing to show here.
A bid at your own price spans the loans. The same call (strike, window) exists once per loan due
at that date, and a borrower sells into the best bid on its own loan. So Basic's "Bid a lower price"
posts one buy offer per loan, each at its own tick, the given premium converted onto that loan's
K − D. All of them share one Exchange group (Architecture): the
Exchange counts what is consumed per maker and group against each offer's cap, so together they fill
no more than the size bid for, and one setConsumed(group, max) cancels them all. The Permit2
allowance is granted once, for the dearest leg; the wallet signs one offer per loan. The panel shows
the market maker's best bid beside yours, since a bid below it does not fill first, and "Your bids"
lists one row per group with what can still fill.
Earn leads with the curator, not the deposit box
A depositor in an Earn pool is buying management, so /earn is built to answer what did the
curator do before how do I deposit. The answers are read from the contract rather than reported
by the curator: the rungs it listed, the cover it bought, the premium it paid, and the limits the
mandate does not let it exceed. That ordering is a deliberate trust stance — the page shows what is
enforced, not what is claimed. The same holds for its warnings: an offer is dropped from the book
when its price sits above the pool's own yield floor, because the module would refuse it anyway.
The lend door
The clearest illustration of why this app is more than a wrapper. The core has no oracle and no liquidation, so a lender's credit is a bond short the borrower's walk-away — a put. A bid is only as good as what it charges for that option, and nothing on-chain tells a lender whether theirs does.
Under the APR, Post bid shows the collateral's reference spot and the strike as a fraction of it, the volatility the APR implies above the funding rate, what the collateral has actually done over 30 days, and the chance of receiving the collateral at that realised volatility. Then one reading: in line, charging for more, charging for less, or not even covering funding. The band is deliberately wide (×0.8–×1.25) because volatility is noisy.
Two details make this a stack-level fact rather than a feature:
- The arithmetic is
@bivium/protocol-sdk'spricing/— the same module the curator marks the invest module's fills against, with the sameFAIR_BASE_APRdefault. "Fair" therefore means the same thing to a lender at this screen and to the curator policing a pool's fills. One definition, one implementation. - It is display only. Nothing here enters an offer or a fill.
Configuration is by address, not symbol, for the obvious reason: NEXT_PUBLIC_REFERENCE_PRODUCTS
maps a collateral address to a public product, because anyone can mint a token called WBTC on a
test chain. A collateral with no mapping simply shows none of these rows. The funding-rate override
must not be zero — on low-LTV credit nearly all of an ordinary APR is time value, and a zero rate
reads every normal bid as overpriced, which would advise a lender to cut an already-thin bid.
The relayer
functions/api/ is a Cloudflare Pages Function over KV. It serves the offer book
(GET /api/offers?series= or ?maker=, POST /api/offers) and the lend door's reference data
(GET /api/reference?product=, Coinbase candles, allowlisted products, cached five minutes, failing
closed on a stale or malformed upstream).
It is not an authority. Exchange.fill re-checks everything and cancellation happens on-chain.
What it does is admission: an offer is stored only if it is on a served market's credit, quoted in
that market's loan token, on the tick grid, carries exactly one cap, expires within 30 days, and its
signature recovers to the maker under the configured ratifier. At most 20 live offers per maker per
market, dropped at expiry.
It understands all three ways an offer can be attested, which is where it touches the rest of the stack:
SignatureRatifier— an ECDSA signature, the ordinary case.SetterRatifier— no signature at all; the maker approves the offer's hash on-chain and the relayer checksapproved(maker, hash)on its RPC. This is the path for a contract wallet, which cannot produce the signatureSignatureRatifierrecovers, and the book hides an offer whose approval is withdrawn.- Listed bidders (
CALL_BIDDERS, from the deployment file'stbv.callBidders) — the only accounts whose bid sheets for calls on vaults/api/call-bidsstores (bivium-protocol-mm'stbv-call-bids). A sheet is a price shown to writers, so it is admitted only signed by a listed bidder over its exact text, for this chain and call book, and valid for at most an hour; one per bidder, replaced by the next. In a/2sheet each firm row'sCallBidsignature must also recover to the bidder, so the page never offers a sale thatsellwould refuse for its signature. - Self-ratifying makers (
SELF_RATIFIERS) — anInvestModuleis its own ratifier. Such an offer names the maker as ratifier and carries the quoter's signature, and the relayer accepts it only when the maker's ownisRatifiedaccepts it on the relayer's RPC. The pool's mandate is therefore enforced at admission as well as at fill. Every Earn pool the app lists must be in this list — a test holds the two together — or its bids never reach the book.
Deliberate duplication: the tick port
lib/tick.ts is an exact port of the contract's TickLib, checked against vectors the contract
produced. The SDK deliberately has no port — it asks Exchange.quote on-chain — and a keystroke
cannot wait on an RPC round trip, so the app keeps its own.
This is the one place the stack knowingly holds two implementations of the same arithmetic, and it
is worth knowing about because the usual rule here is the opposite: series ids, offer hashes, the
Ratify message and the lending ratio all come from the SDK, and lib/protocol.test.ts pins them
against vectors taken from Core.seriesIdOf, Exchange.hashOffer and SignatureRatifier.digest
directly. What the SDK scopes out stays in the app — it supports ERC-20 funding only, so the Permit2
witness and the fill-once ratifier data are the app's.
Allowances
Makers fund bids and asks through Permit2, so the Exchange never receives an ERC-20 allowance; the
only standing approval is the one-time approve(Permit2, max). Partial-fill offers extend the
Permit2 allowance to the offer's expiry, while fill-once offers use a SignatureTransfer permit
witnessed by the offer hash, so the first taker takes what they need and the rest lapses with the
nonce. Takers approve exactly what the transaction needs and nothing stays approved afterwards.
TBV in the app
TBV markets appear in the list tagged TBV and behave differently in ways that follow from the
collateral being soulbound vaultBTC: the borrower picks whole vaults, the credit is sold into the
single best bid that can take the group, and there are no asks, because only the app can borrow.
Resting a borrow is therefore a signed BorrowIntent rather than an Exchange offer — a signature
with no transaction and no allowance, stored at /api/intents and filled whole by a lender. Those
fills do not pass through the Exchange, so they are not in the trade chart.
On a TBV market Pro switches between Credit and Calls on vaults. Selling a call signs a
CallOffer — on free vaults, or on one of the account's loans as a collar — and posts it to
/api/calls; expiries default to 08:00 UTC on Deribit expiry dates, so a call can be hedged to the
minute. The form sells European calls by default, exercisable on the last day before the expiry,
with American as the alternative. For a European call the form shows the dates and strikes a market
maker bids on (/api/call-bids, Lifecycle), its bid for the call
being written, and a button that sets the premium to it. Where that bid is firm (TbvCallBids) and
takes a call this size, the form offers Sell now: one signature for the offer and one sell
transaction, paid at once. Otherwise the maker buys offers at or under its bid within a run. It caps a European offer's deadline at the opening of its
window, and the relayer refuses an offer that stays open past it, so no one can buy and exercise at
once. The board and positions show each call's window, and Portfolio offers exercise only inside
it. The board lists calls for sale (on a market, that market's collars plus every covered call,
which belongs to no market) and the account's own: held, written and resting. The relayer refuses
Exchange collar calls on TBV markets (TBV_CONTROLLER), because there the app alone holds the long
(Architecture). Options list a TBVBTC/USDC pair beside
WBTC/USDC, with the same flash exit.
An Earn pool can be the lender on a TBV market. The borrow panel sells into its bid as into any
other, wrapping a self-ratified bid as abi.encode(offer, signature) before borrowAgainst. On
/earn, a pool whose module has a redeemer is read as a TBV pool and says so: borrowers lock whole
vaults through the app, and a default is delivered as TBVBTC
(Lending).
Trade history is reconstructed
Filled carries shares and cost, not price, and the price depends on the series' collateral per
share at that moment — which falls as borrowers repay. Public RPCs keep no historical state, so
lib/trades.ts replays the series' Minted, Exercised and PairBurned events in log order to
price each fill. Worth knowing before trusting a chart or trying to reproduce one from Filled
alone.
One site, two networks
The testnet deployment is a single build serving both networks, switched in the header, rather than one site per chain. Addresses therefore come from per-network configuration rather than from a build, which is the same reason the relayer takes its ratifier and self-ratifier lists from the deployment file instead of hard-coding them.
Configuration it needs from a deployment
Addresses come from the deployment, not from defaults: the core, Exchange, controller, ratifiers
(NEXT_PUBLIC_SETTER_RATIFIER for contract wallets), Earn pools as
NEXT_PUBLIC_EARN_POOLS ({module, curator, fromBlock} — the curator is a display name and
fromBlock is the module's deploy block), NEXT_PUBLIC_REFERENCE_PRODUCTS by collateral address,
and SELF_RATIFIERS on the relayer from the deployment file's selfRatifiers. TBV comes from the
deployment file's tbv block (app, callBook, controller, vaultBtc, tbvBtc, escrow,
registry, callBidders), with TBV_APP, TBV_CALL_BOOK, TBV_CONTROLLER and CALL_BIDDERS on the
relayer. Shares are displayed
by value, because the module's ERC-20 keeps 18 decimals while its shares carry the loan token's
decimals plus a 6-digit virtual offset, so a raw count reads as thousandths.
Vendored from bivium-protocol-doc 57ed1bdc6cfe, synced 2026-10-27. Where this and a repository disagree about that repository, the repository wins.