Riprap is a platform for event-scoped mutual protection pools on Solana: one pool, one narrowly defined peril, one finite event. Members pay a fixed entry fee, peer jurors adjudicate claims, unused funds return pro-rata, and the pool dissolves. This monorepo holds the frontend (packages/ui — the illustration kit; apps/landing — the static site), the on-chain half (programs/pool — the generic three-track mutual-pool primitive; programs/hanse — the event-mutual orchestrator implementing meta/specs/EVENT-MUTUAL.md; generated Codama clients @riprap/pool / @riprap/hanse), the operator CLI (apps/cli — riprap), and the jest e2e suite (tests/ — @riprap/tests, Surfpool). Project rationale lives in meta/PROJECT.md; the first deployment is Riprap: Blade Pool @ Breakpoint 2026 (15–17 November 2026, Olympia Convention Centre, London).
packages/ui— a data-bound React SVG component kit implementing the illustration language specified inmeta/primitives/: 12 atoms, 4 composed scenes, 5 data stories, 5 motion components, Storybook 10 docs.apps/landing— static Vite + React site, one entry with hash routing (/platform landing,#/2026-breakpoint-blade-poolpool page,#/appmember wallet surface) built from the approved copy inmeta/marketing/03-website-copy/landing-page.md, consuming the UI kit.- One design system — semantic color tokens (
funds= money,deliberation= adjudication,peril= the peril), one stroke width (3), one radius (12), 8px grid, no gradients. - Data law — every number rendered by a component is either doc-sourced or an explicit
{{PARAM}}placeholder. Components never invent figures. programs/pool+programs/hanse— the on-chain half: a generic three-track mutual-pool primitive and the bounded-lifetime event mutual built on it, each with a Codama-generated TypeScript client (@riprap/pool,@riprap/hanse) and LiteSVM test suites.apps/cranker— the lifecycle daemon for the five permissionless cranks (settle_claim,settle_pool,claim_payout,dissolve,pool::crank): a reconciler poll over every Mutual, ported from the sibling accord cranker's architecture. Seeapps/cranker/README.md.
- Tech Stack
- Prerequisites
- Getting Started
- Architecture
- The Illustration Language
- Environment Variables
- Runtime: Node.js 26+
- Package manager: pnpm 11+ (workspace protocol,
onlyBuiltDependenciespinned inpnpm-workspace.yaml) - Language: TypeScript (strict,
verbatimModuleSyntax, noEmit viatsc -b) - UI: React 19
- Design law:
DESIGN.md— engineering section drawing: near-black ground, warm-white ink, one harbor-blue accent, sharp geometry, settle-not-slide motion - Component foundation: shadcn/ui (Radix base, CLI-scaffolded into
packages/ui/src/components/ui, restyled to DESIGN.md — never shipped in default state) +sonnertoasts +lucide-reacticons - Styling: Tailwind CSS v4 (
@tailwindcss/vite); every color/font/radius/motion value resolves throughpackages/ui/src/tokens.css→src/theme.css(@theme inline) — never inline hex - Type: Space Grotesk + JetBrains Mono, self-hosted via
@fontsource-variable/*; every numeral renders in JetBrains Mono (DESIGN.md) - Motion:
motion— settle-only (160/120ms ease-out, 40ms stagger); no bounce, no loops;prefers-reduced-motioncollapses to static - Docs: Storybook 10 (
@storybook/react-vite+ a11y, docs, vitest, chromatic, mcp addons) - Test: Vitest + @testing-library/react (jsdom)
- Lint/format: Biome 2.x (single root config, CSS parser with Tailwind directives enabled)
- Build: Vite (app build for the landing, lib build for the ui package)
- On-chain: Rust stable + Anchor 1.0.2 (
programs/), Codama-generated clients (packages/pool,packages/hanse), LiteSVM unit tests, jest e2e on Surfpool
- Node.js 26 or higher
- pnpm 11 or higher (
corepack enableornpm i -g pnpm) - Git
- A browser (for Storybook and the landing dev server)
- Rust stable (pinned in
rust-toolchain.toml) and Anchor CLI 1.0.2 (avm) — required by theanchor build/cargo testlegs ofpnpm verify - For a live e2e run only: a built sibling checkout of the accord repo (see Testing)
Note
No database, no API keys. The frontend runs with zero configuration; the CLI and the e2e suite take optional RIPRAP_* environment variables with localnet defaults (see Environment Variables).
git clone <repo-url> riprap
cd riprap
pnpm installpnpm 11 blocks dependency postinstall scripts by default; this repo already allowlists the only one needed (esbuild) in pnpm-workspace.yaml, so a plain pnpm install works. If you add a dependency that needs a build script, pnpm will tell you — extend onlyBuiltDependencies rather than running pnpm approve-builds interactively.
pnpm verifyThis runs, in order: pnpm -r run build → pnpm lint → pnpm -r run test → anchor build → cargo test — exactly the verify script in package.json. The jest e2e lane inside the test leg skips itself when no validator is reachable, so the gate stays green on a fresh clone. The first Rust build takes a while and is cached under target/ from then on. It must exit 0.
Expected tail (final leg, cargo test):
test result: ok. X passed; 0 failed; 0 ignored; 0 measured; X filtered out
# Landing page — http://localhost:5173
pnpm dev:landing
# UI kit Storybook — http://localhost:6006
pnpm dev:uiBoth hot-reload. The landing imports @riprap/ui through the workspace link, so atom changes appear in the page without a rebuild.
Tip
Run both in separate terminals while iterating on illustrations in page context — Storybook for isolated controls, the landing for composition and scroll behavior.
riprap/
├── apps/
│ ├── cli/ # @riprap/cli — `riprap` operator CLI (oclif v4) over @riprap/pool + @riprap/hanse
│ ├── cranker/ # @riprap/cranker — lifecycle daemon for the permissionless cranks
│ ├── docs/ # ADRs and fleet bean files
│ └── landing/ # @riprap/landing — static Vite + React site
│ ├── src/
│ │ ├── App.tsx # page composition (sections per the approved copy)
│ │ ├── main.tsx # hash-router entry (platform default; pool/app lazy routes)
│ │ └── index.css # imports @riprap/ui/tokens.css + page globals
│ └── index.html
├── packages/
│ ├── ui/ # @riprap/ui — the illustration primitive kit
│ │ ├── src/
│ │ │ ├── tokens.css # design tokens: colors (light/dark), type scale, motion
│ │ │ ├── lib/ # pure, tested logic: pool math + stone geometry
│ │ │ │ ├── poolMath.ts # TIERS, proRataShare, scaledPayout, fillHeight, usd
│ │ │ │ └── stone.ts # seeded irregular-hexagon generator (no mortar)
│ │ │ ├── atoms/ # 12 atomic SVG components (g-fragments, data-bound)
│ │ │ ├── scenes/ # 4 composed scenes (join, claim, end-of-event, lifecycle)
│ │ │ ├── datastories/ # 5 money-math narratives (annotated frame sequences)
│ │ │ ├── motion/ # 5 animated variants of atoms (motion package)
│ │ │ └── index.ts # public API — named exports only
│ │ ├── .storybook/ # main.ts (globs), preview.tsx (tokens + light/dark toolbar)
│ │ └── vite.config.ts # lib build + vitest (jsdom)
│ ├── pool/ # @riprap/pool — Codama client generated from programs/pool
│ └── hanse/ # @riprap/hanse — Codama client generated from programs/hanse
├── programs/
│ ├── pool/ # Anchor program `pool` — three-track mutual-pool primitive
│ └── hanse/ # Anchor program `hanse` — event mutual (meta/specs/EVENT-MUTUAL.md)
├── tests/ # @riprap/tests — jest e2e against a Surfpool surfnet (skips offline)
├── meta/ # source-of-truth docs (not code)
│ ├── PROJECT.md # the why and the what
│ ├── specs/ # protocol specs (EVENT-MUTUAL.md — the hanse contract)
│ ├── Pool Program.md # pool mechanics design notes
│ ├── naming.md # naming session + locked decisions
│ ├── Breakpoint/ # the Blade Pool policy document
│ ├── primitives/ # the illustration spec this kit implements
│ └── marketing/ # the approved marketing package (copy source)
├── biome.json # lint + format, whole repo
└── pnpm-workspace.yaml # packages/*, apps/*, tests; build-script approvals
meta/primitives (spec) ──implements──▶ packages/ui ──workspace import──▶ apps/landing
│ │ │
│ tokens.css ◀──── imports ───────────────┘
└── meta/marketing (copy) ─────────────────────────▶ landing sections
The dependency direction is one-way: the landing consumes the kit; the kit implements the spec; the spec and copy live in meta/ and are never generated from code.
Tokens (packages/ui/src/tokens.css) — CSS custom properties, two modes (:root light, [data-mode="dark"]), semantic hues only: funds (money, always), deliberation (adjudication), peril (incidents, sparingly). Type scale 44/28/18/16/13. Motion constants: signature easing --riprap-ease, duration palette 180/320/560ms — collapsed to 1ms under prefers-reduced-motion.
Pool math (lib/poolMath.ts) — the money rules as pure functions, fully tested: TIERS (policy §5, the only allowed prices), proRataShare (user_stake / total_stake × treasury), scaledPayout (min(tier cap, amount) × P/A — the fail-closed wall), fillHeight (the area = money mapping), usd (deadpan money formatting).
Stone geometry (lib/stone.ts) — seeded deterministic irregular hexagons: 6 vertices, no two angles equal, rotation ±15°, coordinates snapped to the 4px grid. A member's seed renders the same stone in every scene — that is what makes a pile readable as these members.
Atoms (src/atoms/) — each implements one spec file from meta/primitives/atoms/: MemberStone, PoolVessel (the most important — every dollar is inside it or visibly leaving it), SponsorRuleCard, TierCapStations, JurorStone, ClaimFiled, JurorDraw, CommitRevealVote, Ruling, TreasuryPayout, ProRataRefund, Dissolution. Atoms are <g> fragments positioned by x/y props; SvgFrame wraps standalone use and provides the accessibility contract (role="img" + aria-labelledby).
Scenes (src/scenes/) — composition per meta/primitives/composition.md § scene templates: JoinFlow, ClaimFlow (approved branch solid, rejected branch dashed — always drawn), EndOfEventFlow, LifecycleOverview (8 step chips + stepped balance trace).
Data stories (src/datastories/) — the five narratives from meta/primitives/data-stories.md: the standard story, the heavier-storm variant, the worst-case wall, the 1:100 tier ladder, the juror's side. Every figure doc-sourced, derived arithmetic labeled, shared money scale within a story.
Motion (src/motion/) — animated wrappers around the static atoms: StoneSettle, PoolFill, WaveBreak (the hero), DissolutionScatter, BalanceTrace. Personality: Premium/Corporate easing; stone is rigid material — no bounce, ever. All components render the final state under reduced-motion.
Four laws from meta/primitives/ that the kit enforces and reviewers should check:
- Area = money. Fill height is linear in USDC at constant interior width; compared vessels share one scale. Unreadable ratios (1:100 caps) print numbers instead of lying with bars.
- Dashed = limit. Caps, thresholds, window bounds are dashed; actual amounts are solid.
- Exactly two doors. Money leaves a vessel only through
spending — adjudicatedorliquidation. No third exit is ever drawn, because none exists. - Facts vs placeholders. Doc-sourced numbers print in monospace; unknowns print as
{{PARAM}}in monospace. A reader can always tell which is which.
The frontend needs no configuration to run. The landing's optional vars (see apps/landing/.env.example): VITE_N8N_WEBHOOK_URL (waitlist-form webhook, every route) and the Solana-route cluster config — VITE_DEVNET_RPC / VITE_MAINNET_RPC (custom RPC endpoints), VITE_LOCALNET_MUTUAL (Surfpool dev mutual address), and the per-cluster Accord evidence daemon serving the anchored cover terms: VITE_EVIDENCE_DAEMON_URL_DEVNET (default https://api.devnet.useaccord.xyz), VITE_EVIDENCE_DAEMON_URL_MAINNET (default https://api.useaccord.xyz), VITE_EVIDENCE_DAEMON_URL_LOCALNET (defaults to the resolved devnet value) — consumed only by the pool and app routes, never the platform route. The operator CLI and the e2e suite read optional overrides, all defaulting to localnet:
| Variable | Used by | Default | Meaning |
|---|---|---|---|
RIPRAP_RPC_URL / RIPRAP_WS_URL |
CLI (--rpc / --ws), e2e |
http://127.0.0.1:8899 / ws://127.0.0.1:8900 |
Solana JSON-RPC / WebSocket endpoint |
RIPRAP_KEYPAIR_PATH |
CLI (--keypair) |
ANCHOR_WALLET → ~/.config/solana/id.json |
Signer keypair JSON |
RIPRAP_PAYER_PATH |
e2e | ~/.config/solana/id.json |
e2e payer keypair |
RIPRAP_SMOKE_RPC |
CLI smoke test | http://127.0.0.1:8899 |
Live-smoke endpoint; test skips when unreachable |
ACCORD_SO / ACCORD_KEYPAIR |
e2e, Rust LiteSVM lane | …/accord/target/deploy/ in the sibling checkout |
Built accord program artifact + its keypair |
SAS_SO |
e2e, Rust LiteSVM lane | …/solana-attestation-service/target/deploy/solana_attestation_service.so |
Built SAS program artifact (the §2.8 closed-circle CPI target; the e2e fabricates its loader accounts at the canonical address) |
Run from the repo root unless noted:
| Command | Description |
|---|---|
pnpm install |
Install and link all workspace packages |
pnpm build |
Build every package (pnpm -r run build) |
pnpm lint |
Biome lint + format check for the whole repo |
pnpm lint:fix |
Biome with safe fixes + formatting applied |
pnpm test |
Run every package's vitest suite |
pnpm verify |
build → lint → test → anchor build → cargo test, the completion gate |
pnpm dev:landing |
Landing dev server (http://localhost:5173) |
pnpm dev:ui |
Storybook dev server (http://localhost:6006) |
pnpm --filter @riprap/ui run build-storybook |
Static Storybook build → packages/ui/storybook-static/ |
pnpm --filter @riprap/landing run preview |
Serve the built landing locally |
anchor test |
Full e2e: build, start Surfpool, deploy pool + hanse, run the jest suite (@riprap/tests) |
pnpm dev:cranker |
Cranker daemon (bun --watch; see apps/cranker/.env.example) |
pnpm --filter @riprap/cli dev <cmd> |
Run the riprap operator CLI from TypeScript sources |
# Everything (what CI / the completion gate runs)
pnpm test
# E2e lane alone (jest, Surfpool) — skips cleanly with no validator
pnpm --filter @riprap/tests test
# One package
pnpm --filter @riprap/ui test
pnpm --filter @riprap/landing test
# Watch mode while developing
cd packages/ui && pnpm exec vitest
# One file
pnpm --filter @riprap/ui exec vitest run src/lib/poolMath.test.ts
# By test name
pnpm --filter @riprap/ui exec vitest run -t "area = money"lib/— pure-function unit tests: the tier table (policy §5), pro-rata math on the worked example ($12,000 ÷ 1,000 = $12), the fail-closed payout wall (P/A scaling, cap-then-scale precedence), fill-height linearity and rim clamping, stone determinism/irregularity/grid snapping.atoms/+scenes/+datastories/— render tests via @testing-library: data bindings (labels printed, fill heights, dashed-vs-solid), composition invariants (both doors labeled, rejected branch present inClaimFlow, story frames carry their headline numbers).apps/landing— a smoke test that the page mounts with its core sections; visual verification is done in a browser (see Storybook/landing dev servers), not snapshot tests.tests/(@riprap/tests) — jest e2e, specs a–e against a live Surfpool surfnet: solvent to-the-cent payouts, denied claims, over-treasury scaling, the appeal ladder, and window gates. Serial by design (surfnet_timeTravelwarps the global surfnet clock). Live-run prerequisites:anchor test(deploys pool + hanse) plus a built sibling accord checkout (cd ../accord && make build, orACCORD_SO); the accord rev is pinned inprograms/hanse/Cargo.toml. With no validator reachable, every spec skips.apps/cli— vitest: help completeness and dry-run output always run;surfpool-smoke.test.tsgoes live only whenRIPRAP_SMOKE_RPCanswers.
- Colocated
*.test.ts(x)next to the code under test. - Test names cite their source spec (
policy §7,composition.md § scale discipline) so a failing test tells you which contract broke. - No snapshots — assertions target data-bound output (text, geometry attributes), which is the actual contract.
pnpm dev:ui # develop: http://localhost:6006
pnpm --filter @riprap/ui run build-storybook # static build → packages/ui/storybook-static/- Stories live colocated as
*.stories.tsxand are globbed by.storybook/main.ts. - The preview imports
tokens.cssand provides a light/dark mode toolbar (setsdata-mode), mirroring the two palette modes fromcomposition.md. Never mix modes within a scene. - Every data prop is a control — the stories double as the interactive reference for the kit's API.
- Addons installed (Storybook 10 recommended set): a11y, docs, vitest, chromatic, mcp.
Both artifacts are static — no server, no runtime.
Landing (primary artifact — riprap.xyz):
pnpm --filter @riprap/landing run build
# → apps/landing/dist/ (index.html + old-path redirect stubs + hashed assets)Deploy dist/ to any static host (Vercel, Netlify, Cloudflare Pages, or your own nginx):
- Build command:
pnpm --filter @riprap/landing run build - Output directory:
apps/landing/dist - No SPA rewrites needed (hash routing — every route renders from
index.html; old MPA paths redirect via static stubs); if your host offers it, setCache-Control: immutablefor hashed assets and revalidateindex.html.
Storybook (docs site):
pnpm --filter @riprap/ui run build-storybook
# → packages/ui/storybook-static/Ship as a second static site (e.g. design.riprap.xyz or /design on the same host).
hanse:initialize enforces three things operators cannot get wrong:
- One mint.
--deposit-mintand--fee-mintmust be the same mint — the program rejects mixed mints (InvalidConfiguration). Settlement adds claim and fee amounts as raw integers, so two different assets would corrupt payout math (ADR-0002). - Fixed payout pull window. 180 days after settlement, hardcoded on-chain (
PULL_WINDOW_SECS= 15 552 000 s) — there is no--pull-windowflag. Unpaid amounts revert to the residual when the window closes. - Classic SPL Token only. Token-2022 mints are not supported by the programs' token constraints; init rejects them by construction.
The Surfpool runbook (runbooks/deployment/, wired by txtx.yml) deploys both programs. The Surfnet-only instant-deploy cheatcode is gated to localnet; a mainnet run goes through the real deploy path. Once per program lifetime:
- Generate the program keypairs offline (
solana-keygen new --no-passphrase -o pool-mainnet.json, likewisehanse-mainnet.json) — keep them off this machine; the upgrade authority below is what matters day-to-day. Record the derived addresses inAnchor.toml's commented[programs.mainnet]block. - Pin the environment in
txtx.yml(mainnet): your RPC URL and theexpected_payer_address/expected_authority_addressvalues.runbooks/deployment/signers.mainnet.txtenforces both — a mismatched wallet fails the run. The authority should be the 2/3 Squads multisig (EVENT-MUTUAL §12), not a single key. - Gate:
pnpm verifygreen, plus the live e2e (anchor test) against a mainnet-forked surfnet with the exact accord artifact pinned inprograms/hanse/Cargo.toml. - Deploy:
surfpool run deployment --environment mainnet(or the Surfpool Studio equivalent). - Verify the deployment before announcing it: for each program,
solana program show <id> --url mainnet(owner = BPFLoaderUpgradeab1e,ProgramDataauthority = the multisig), thensolana program dump <id> dumped.so --url mainnetand comparesha256sum dumped.so target/deploy/<p>.so— the deployed bytes must match the reviewed build. A mismatch is a stop-ship.
Important
There is no CI pipeline in the repo yet. Until one exists, pnpm verify is the release gate — run it before every push that touches packages/, apps/, programs/, or tests/.
pnpm 11 blocks postinstall scripts. This repo allowlists esbuild in pnpm-workspace.yaml. If you hit this after adding a dependency with a build step, add its package name to onlyBuiltDependencies there and re-run pnpm install. Do not use pnpm approve-builds interactively — the allowlist must live in version control.
Run pnpm install from the repo root after cloning or adding packages — pnpm links workspace deps at install time. Confirm with pnpm -r ls --depth -1.
tokens.css is imported by .storybook/preview.tsx; if atoms render without it (e.g. embedded somewhere else), import it once in the consuming app (import "@riprap/ui/tokens.css" — see apps/landing/src/index.css).
The palette is driven by a data-mode="dark" attribute (Storybook toolbar sets it on a wrapper). If you wrap kit components yourself, set data-mode on an ancestor — the tokens cascade.
Tests require environment: "jsdom" — configured in packages/ui/vite.config.ts and apps/landing/vitest.config.ts. If you add a package with tests, add the same config; don't rely on globals.
pnpm --filter @riprap/landing exec vite --port 5180
pnpm --filter @riprap/ui exec storybook dev -p 6007@riprap/ui is consumed as TypeScript source through the exports map (no build step between packages). Renames propagate instantly — and so do breakages. After touching src/index.ts, run pnpm build from the root; tsc -b in every package catches stale imports.
| Path | What it is |
|---|---|
meta/PROJECT.md |
The why and the what — read this first |
meta/primitives/ |
The illustration spec this kit implements (atoms, composition grammar, data stories) |
meta/marketing/ |
The approved marketing package; landing copy lives in 03-website-copy/landing-page.md |
meta/Breakpoint/ |
The Blade Pool policy document (tiers, exclusions, claims rules) |
meta/specs/ |
Protocol specs — EVENT-MUTUAL.md is the contract programs/hanse implements |
meta/naming.md |
Naming decisions (Riprap locked, riprap.xyz, @riprapxyz) |
packages/ui/ |
The illustration primitive kit |
apps/landing/ |
The static landing page |
programs/pool/ |
Anchor program pool — the three-track mutual-pool primitive |
programs/hanse/ |
Anchor program hanse — event-mutual orchestrator (meta/specs/EVENT-MUTUAL.md) |
packages/pool/ |
@riprap/pool — Codama client generated from programs/pool |
packages/hanse/ |
@riprap/hanse — Codama client generated from programs/hanse |
apps/cranker/ |
@riprap/cranker — lifecycle daemon for the permissionless cranks (discovery via the SDK scans) |
apps/docs/ |
ADRs and fleet bean files |
tests/ |
@riprap/tests — jest e2e against a Surfpool surfnet (skips offline) |
AGENTS.md |
Instructions for coding agents working in this repo |