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   selectors

Components 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.ts

Connection lifecycle, active address, network switching. Swap the three provider calls for a real adapter; nothing else changes.

Service layer

src/web3/services/streamService.ts

The 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.

1

Install the SDK

npm i @zebec-protocol/stream @solana/web3.js @coral-xyz/anchor
npm i @solana/wallet-adapter-react @solana/wallet-adapter-wallets
2

Fill 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.

3

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) × rate from 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. StreamClock runs a single requestAnimationFrame loop that every ticker subscribes to, throttled to NEXT_PUBLIC_STREAM_MAX_FPS and 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 baseAmount verbatim; 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 tour
  • architecture.mdLayer-by-layer walkthrough and data flow
  • sdk-integration.mdSwapping the mock for Zebec, line by line
  • wallet-configuration.mdWiring a real wallet adapter
  • mock-data-engine.mdHow the demo chain simulator works
  • customization.mdWhite-labelling: colour, copy, navigation
  • deployment.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.