Architecture
System design, capital flows, and technical decisions
Architecture
System Overview
┌───────────────────────────────────────────────────────────────────────┐
│ ON-CHAIN │
│ │
│ Curator ──▶ StrategyFactory ──▶ Strategy (clone) ──▶ Warehouse │
│ ▲ │ │
│ Donors ─── fund ────────┘ withdraw │
│ ▼ │
│ YieldRedirector ──▶ Source Vault Recipients │
│ │ │
│ harvest() ──▶ Strategy ──▶ Warehouse │
│ │
├───────────────────────────────────────────────────────────────────────┤
│ OFF-CHAIN │
│ │
│ Contracts ──events──▶ Ponder ──▶ GraphQL ──▶ SDK ──▶ Web App │
│ │ │ │ │
│ ┌─────┘ │ │ │
│ ▼ │ │ │
│ Hono REST API ◀──────────────────────────┘ │ │
│ /api/drafts,comments, │ │
│ reactions,notifications │ │
│ │ │ │
│ ▼ │ │
│ Drizzle (offchain) BetterAuth ◀┘ │
│ ┌──────────────┐ (SIWE → JWT) │
│ │ drafts │ │ │
│ │ comments │ ◀── JWT verify ──── JWKS │
│ │ reactions │ (middleware) │
│ │ notifications│ │
│ └──────────────┘ │
│ │ │
│ Same Postgres DB │
│ (offchain schema + Ponder schema) │
└───────────────────────────────────────────────────────────────────────┘On-Chain Layer
| Contract | Purpose | Pattern |
|---|---|---|
| StrategyFactory | Deploys Strategy clones — one per tenant | Factory + EIP-1167 clones |
| Strategy | Holds allocations, distributes funds | Minimal proxy clone |
| SplitsWarehouse | Holds recipient balances | External singleton (0xSplits) |
| YieldRedirectorFactory | Deploys YieldRedirector clones | Factory + EIP-1167 clones |
| YieldRedirector4626 | Wraps ERC-4626 vaults, redirects yield | Minimal proxy clone |
| ForeverSubnameRegistrar | Manages ENS subdomains for a tenant's namespace | External singleton per tenant |
Off-Chain Layer
| Component | Purpose |
|---|---|
| Indexer (Ponder) | Watches all tenant factory addresses on Base Sepolia; tags each strategy with its tenantId |
| REST API (Hono) | Social endpoints (/api/drafts, /api/comments, etc.) running in the same indexer process |
| Auth (BetterAuth + SIWE) | Web app issues JWTs via SIWE; indexer verifies via JWKS |
SDK (@curator-studio/sdk) | Tenant-scoped TypeScript client; resolves factory address + ENS domain from tenant registry |
Web App (@curator-studio/web) | Next.js frontend; tenant configured via NEXT_PUBLIC_TENANT env var |
Multi-Tenancy
Each tenant (e.g., support.eth, unicef.eth) deploys its own StrategyFactory and owns its ENS subdomain namespace. The tenant registry in the SDK maps tenant IDs to per-chain configuration:
tenants["support.eth"][sepolia.id] = {
ensDomain: "support.eth",
factory: "0x...",
}The indexer watches all registered factory addresses simultaneously and records tenantId on every indexed strategy. The SDK and web app are each scoped to a single tenant at runtime, set via the tenant prop on CuratorProvider (or NEXT_PUBLIC_TENANT).
Off-Chain Social Layer
The indexer process hosts two stacks in one server: Ponder (on-chain indexing → GraphQL) and Hono (off-chain social → REST API). Both share the same Postgres database but use separate schemas.
Database. Drizzle ORM manages the offchain PostgreSQL schema (pgSchema("offchain")), keeping social tables isolated from Ponder's on-chain tables while sharing one Postgres instance. Tables:
| Table | Purpose |
|---|---|
draft | Off-chain strategy proposals with allocations, visibility, fork lineage |
version | Immutable snapshots of title, description, and allocations — created on every draft edit or merge accept |
comment | Threaded comments on strategies and drafts, with optional allocation diffs |
reaction | Upvote/flag reactions on comments, drafts, or strategies |
notification | Activity feed entries (comments, reactions, merge proposals) |
REST API. Hono routes mounted on the indexer at /api/*. Read endpoints are public; write endpoints require a valid JWT.
Auth Middleware. The web app runs BetterAuth which handles SIWE login and issues JWTs. The indexer verifies tokens via the JWKS endpoint BetterAuth exposes — no shared secret, just public-key verification. Write routes pass through a requireAuth middleware that extracts the caller's address from the verified token.
API Surface
The indexer exposes two interfaces from a single process:
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/graphql | GET / POST | No | On-chain data (strategies, distributions) via Ponder |
/api/drafts | GET / POST / PUT / DELETE | Write: Yes | Draft CRUD, publish, fork |
/api/comments | GET / POST / DELETE | Write: Yes | Threaded comments on strategies and drafts |
/api/reactions | POST / DELETE | Yes | Emoji reactions on comments |
/api/notifications | GET / POST | Yes | Activity feed, mark-read |
Capital Flows
Direct Funding
Donor ──(transfer)──▶ Strategy ──(distribute)──▶ Warehouse ──(withdraw)──▶ Recipients- Donor sends ERC-20 or ETH to the strategy address (or ENS name)
- Anyone calls
distribute(token)— calculates shares by weight, batch-deposits to warehouse - Recipients call
withdraw(owner, token)when ready
Yield Funding
Capital Provider ──(deposit)──▶ YieldRedirector ──▶ Source Vault
│
generates yield
│
harvest() ◀──────────┘
│
▼
Strategy ──(distribute)──▶ Warehouse ──▶ Recipients- Capital provider deposits into YieldRedirector
- Assets route to source vault (Morpho, Euler, etc.) which generates yield
- Anyone calls
harvest()— withdrawsharvestable()assets from the source vault, sends them to the recipient Strategy, and callsdistribute(asset, amount)on it (subject tominHarvestAmount/minHarvestInterval) - Capital provider can withdraw principal at any time
Fund-of-Funds
When a strategy allocates to another strategy from the same factory, distribution transfers the child's share to the child strategy's contract balance (native ETH or ERC-20). A separate permissionless distribute() on the child routes those funds to its own recipients (warehouse and/or further children). This enables hierarchical curation with independent distribution at each level.
Security Model
The Strategy contract has no withdrawal function — funds can only leave via distribute() routing to the warehouse or to same-factory child strategies by direct transfer. Distribution is permissionless. Warehouse recipients self-custody via the warehouse. Off-chain social data is non-financial — on-chain state is the source of truth.
For the full trust model, security features, known limitations, and invariants, see Security & Roadmap.
Technical Decisions
Weights Over Percentages
Weights allow adding recipients without recalculating existing allocations. No need to sum to 100. Any precision supported.
Pull-Based Warehouse (0xSplits)
One batch deposit instead of N transfers. Reverting recipients don't block distribution. Recipients accumulate from multiple strategies and claim when convenient. Battle-tested code from the Splits ecosystem.
Fees as Allocations
Curator fees are regular allocations — no special fee handling code. Fully transparent, any fee structure, same code path for fees and recipients.
ERC-4626 for Yield Redirector
Standard vault interface for composability. totalAssets() overridden to return principal only, keeping share price stable at 1:1 so yield is cleanly separable.
EIP-1167 Minimal Proxies
~100k gas per strategy deployment instead of ~500k. All clones share the implementation contract. Trade-off: slightly higher per-call gas from DELEGATECALL, cannot upgrade individual clones.
ENS Subdomains
Free, permanent subdomains under the tenant's ENS namespace (e.g., strategy.support.eth) instead of requiring users to buy their own ENS names. Consistent namespace for discovery within each tenant. Both forward resolution (name → address) and reverse resolution (address → name) are set up during registration.
Because the namespace is per-tenant, each StrategyFactory is wired to its own ForeverSubnameRegistrar, and the SDK resolves the correct ENS domain from the active tenant's config.