Skip to main content

Architecture

Bachelier is a pnpm + turbo monorepo (Node 20+). The on-chain protocol is Clarity; everything off-chain is TypeScript. The web app can run chain-direct (reading state straight from a Stacks node) so a working demo needs no backend.

Data flow

┌─────────────────────────────────────────┐
Stacks (Clarity) │ oracle-adapter.clar ── reads ──► Pyth │
│ │ │
│ ▼ │
bs-math.clar ◄─ used by ─ vault.clar ◄── SIP-010 ── sBTC / USDC │
(fixed-point BS) │ + bcshare-token (vault-gated SIP-010) │
└───────────────┬───────────────┬──────────┘
│ print events │ read-onlys
▼ │
Chainhook ──► indexer (TS) ─────┤
│ writes │
▼ │
Postgres (events_raw,│
rounds, positions, │
snapshots, …) │
▲ │
│ reads │
api (Fastify) ◄──────┘

│ REST
web (React + Vite)
wallet writes ──► Stacks directly
keeper (TS) ── signs ──► start-round / settle-round

Repository layout

PathWhatTests
contracts/Clarinet project: bs-math, vault, oracle-adapter, bcshare-token, devnet mocks78 vitest + clarinet-sdk tests incl. BS vector table, 216-point TS↔Clarity parity grid, full lifecycle both settlement branches, execution-cost gate
packages/shared/BS TS mirror (bs.ts), fixed-point helpers, typed event decoders, network config, zod DTOsparity exercised by contract + API suites
db/Drizzle schema + migrations + seedcovered via indexer/API suites (PGlite)
indexer/Chainhook webhook consumer → Postgres projections + snapshots; reorg rollback; Stacks-API backfill5 acceptance tests (replay, duplicate no-op, rollback/re-apply, resume)
api/Fastify REST: /vault, /vault/history, /rounds, /positions/:addr, /quote, /price, /tx/:txid, /health13 tests, zod-validated
keeper/Cron + tick worker: weekly start-round, post-expiry settle-round, devnet price relay, retries/backoff, /health7 policy + loop tests
web/React + Vite app: landing + vault dApp, payoff canvas, wallet flows with post-conditions25 component/format tests

All tests pass across the workspace (pnpm test).

On-chain contracts

There are eight Clarity contracts. The vault is the hub; everything else is a dependency it calls. See the Contracts reference for live addresses and the Protocol section for mechanics.

ContractRole
vaultThe covered-call vault: deposits, shares, rounds, settlement, exercise
bs-mathFixed-point Black–Scholes pricing engine (pure, read-only)
oracle-adapterNormalizes a price feed; enforces positivity + staleness
bcshare-tokenVault-gated SIP-010 share token (the vault's share ledger)
sip-010-traitThe SIP-010 fungible-token trait
pyth-mockMock price feed (keeper relays a price on testnet/devnet)
sbtc-tokenMock SIP-010 sBTC (open mint = faucet)
usdc-tokenMock SIP-010 USDC (open mint = faucet)

Shared TypeScript core (packages/shared)

The shared package is the seam that keeps off-chain and on-chain in lockstep:

  • bs.ts — a TypeScript mirror of bs-math.clar, term-for-term, used as the parity oracle in tests.
  • networks.ts — network-keyed config (contract ids per devnet / testnet / mainnet). configWithDeployer() derives the whole address set from one deployer principal — this is how the web build bakes in live addresses without relying on process.env (which Vite strips from the browser bundle).
  • events.ts — typed decoders for the vault's print events.
  • dto.ts — zod schemas shared by the API and web.

Off-chain services

The indexer, API, and keeper are independent TypeScript services (each with a /health endpoint, Dockerized via docker/service.Dockerfile). They are optional for a demo — see Services and the Web app chain-direct mode.