Documentation
Architecture, integration path and configuration reference. Full guides ship as markdown in the repository's /docs folder.
Architecture
One rule governs the whole codebase: no component ever touches a chain.
Wallet state → SDK / data adapter → StreamPay UI → Real-time visualization
───────────── ─────────────────── ───────────── ─────────────────────
walletStore IStreamService React pages useStreamCounter
├── MockStreamService + Zustand + StreamClock (1 rAF loop)
└── ZebecStreamService selectorsComponents consume normalised, typed data — plain decimal amounts and five canonical stream statuses. They never see base units, BN instances, program-derived addresses, ABIs or RPC calls. That is what makes the protocol swappable: porting StreamPay from Zebec to Sablier, Superfluid or a bespoke contract means rewriting one adapter class, not the interface.
Wallet layer
src/web3/store/walletStore.tsConnection lifecycle, active address, network switching. Swap the three provider calls for a real adapter; nothing else changes.
Service layer
src/web3/services/streamService.tsThe IStreamService contract. Five reads, five writes, all normalised. Writes never throw — they return a typed TxResult.
Engine layer
src/web3/engine/Pure accrual maths plus one shared requestAnimationFrame loop. Values are derived from wall-clock time, never accumulated per frame.
Presentation
src/features/ · src/components/Feature modules and a dark-mode design system built on Radix primitives and Tailwind v4 tokens.
Going live with Zebec
Three steps. The UI is not touched in any of them.
Install the SDK
npm i @zebec-protocol/stream @solana/web3.js @coral-xyz/anchor
npm i @solana/wallet-adapter-react @solana/wallet-adapter-walletsFill in the three integration points
Open src/web3/services/zebecStreamService.ts. Each point is marked INTEGRATION POINT and carries the real SDK call in the comment above it: build the client in connect(), fetch and normalise in getStreams(), and implement the write path — the shared execute() wrapper already handles phase reporting and error normalisation for all five writes.
Flip the flag
# .env.local
NEXT_PUBLIC_ENABLE_MOCK_MODE=false
NEXT_PUBLIC_DEFAULT_NETWORK=solana-mainnet
NEXT_PUBLIC_RPC_URL=https://mainnet.helius-rpc.com/?api-key=...
NEXT_PUBLIC_ZEBEC_PROGRAM_ID=...The factory in src/web3/services/index.ts reads that one flag and returns ZebecStreamService instead of MockStreamService. Every component keeps calling the same interface.
Different protocol? Write a class that implements IStreamService and return it from createStreamService(). The interface is documented method by method, and MockStreamService is a complete reference implementation including the state machine.
The streaming engine
Why the balances tick smoothly without melting the main thread.
const live = useStreamCounter({
baseAmount: stream.streamedAmount, // authoritative, from chain
syncedAt: stream.lastSyncedAt, // when it was read
flowRatePerSecond: stream.flowRatePerSecond,
isActive: stream.status === 'streaming',
maxAmount: stream.totalAmount,
});- Derived, never accumulated. Each frame computes
base + (now − syncedAt) × ratefrom scratch. Dropped frames, a sleeping laptop and a backgrounded tab all resolve to the correct value instantly — no drift, no catch-up animation. - One loop for the whole app.
StreamClockruns a single requestAnimationFrame loop that every ticker subscribes to, throttled toNEXT_PUBLIC_STREAM_MAX_FPSand suspended while the tab is hidden. - Re-renders only when digits change. State is committed only when the value changes at the rendered precision, and the subscription lives in a leaf component — so a frame updates one span, not the page.
- Hydration-safe by construction. The first render returns
baseAmountverbatim;Date.now()is never called during render. Server and client markup are byte-identical. - Corrected on a slow poll. Every
NEXT_PUBLIC_STREAM_SYNC_INTERVAL_MS, authoritative balances are re-read from the service and the interpolation re-anchors.
Repository guides
Markdown, in the repo’s /docs folder.
getting-started.mdInstall, run, and take the demo tourarchitecture.mdLayer-by-layer walkthrough and data flowsdk-integration.mdSwapping the mock for Zebec, line by linewallet-configuration.mdWiring a real wallet adaptermock-data-engine.mdHow the demo chain simulator workscustomization.mdWhite-labelling: colour, copy, navigationdeployment.mdVercel, Netlify, Docker, static export
Security posture
What this application can and cannot do.
Non-custodial StreamPay never holds funds. Escrow lives in the streaming contract; withdrawals settle from contract to wallet.
No keys The application never requests a private key or seed phrase. Every transaction is signed inside the user’s wallet.
No backend There is no server tier and no database. Every environment variable is NEXT_PUBLIC_* because everything the browser needs is public by definition.
Note Client-side state-machine guards are a courtesy that saves the user a doomed wallet popup. The contract remains the only source of truth.