Hatch Architecture
How the monorepo fits together — at the level a new engineer needs before their first PR.
Also read: ADRs for the why behind non-obvious decisions. DEV-GUIDE.md for the how-to-run-it. API.md for the REST surface.
The 30-second mental model
┌───────────────────────────────────────────────────────────────────┐
│ Public users + devs │
└───────────────────────────────────────────────────────────────────┘
│ │ │
│ web │ SDK / curl │ extension
│ (/launch, /score, │ (@hatch/sdk, │ (browser
│ /leaderboards, …) │ REST) │ MV3)
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ apps/web │ │ apps/api │
│ Next.js 15 │ │ Hono + Drizzle │
│ App Router │──▶│ /v1/… + /api/v1/… │
│ i18n EN + 中文 │ │ webhooks + admin │
└──────────────────────┘ └──────────┬───────────┘
│
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Postgres │ │ Anthropic │ │ BNB Chain │
│ (Supabase) │ │ Claude API │ │ HatchAttest │
│ │ │ │ │ HatchNest │
│ Drizzle ORM │ │ scoring LLM │ │ Hatcher NFA │
└──────────────┘ └───────────────┘ └──────────────┘
Three runtime layers:
- Web — Next.js 15 App Router on Vercel. Server components talk to the API directly (env base URL); client components go through Next route handlers so secrets never reach the browser.
- API — Hono on Railway (planned). Drizzle + Postgres for persistence, Anthropic for scoring, viem for chain writes, webhook dispatcher for event emission.
- Contracts — Four Solidity contracts on BNB testnet today, mainnet
after Sprint C.7.
HatchAttestis the one the API writes to; the others coordinate the treasury + hatchers.
Monorepo layout
apps/
web/ Next.js 15 App Router
src/app/ Route-driven pages (public + /admin)
src/components/ React components
src/i18n/ next-intl EN + zh message files
src/lib/ API client, helpers, blog+incidents data
api/ Hono + Drizzle
src/modules/ Per-domain (scoring, enrollment, launch, webhooks, admin, treasury, api-keys, public-api)
src/middleware/ rate-limit, request-logger, error-handler, api-key-auth
src/db/ Drizzle schema + client + migrations
src/config/ env loader (zod-validated, fail-closed)
packages/
contracts/ Foundry — HatchRegistry · HatchAttest · Hatcher · HatchNest
sdk/ @hatch/sdk — zero-dep TS client
shared/ Cross-cutting zod schemas + types
ui/ shadcn-based primitives
extension/ Browser extension (MV3)
chain-core/ Chain-agnostic adapter interface
chain-solana/ pump.fun adapter scaffold
chain-base/ Zora adapter scaffold
docs/ Guides, ADRs, runbooks, sprints
Core flows
1. Scoring a token
creator ──POST /v1/score──▶ apps/api
│
▼
scoringService.scoreToken()
│
├─▶ meme signal ── Anthropic tool-use
├─▶ creator signal── (stub until Bitquery key)
├─▶ image signal ── Anthropic Vision + SSRF-safe fetcher
├─▶ name signal ── deterministic
├─▶ social signal ── deterministic
└─▶ risk signal ── (stub until GoPlus key)
│
▼
weightedAggregate() (re-weight if stubs present)
│
▼
persist(row) ──▶ scoreRequests table
│
└─▶ emit('score.created', id, payload) ──▶ webhooks
- Each signal runs independently; one failure does not fail the whole score.
hasStubs: trueblocks on-chain attestation and leaderboard inclusion.- Raw LLM response is stored for audit + replay.
- Prompt version is part of the row (
meme@1.0.0,image@1.0.0).
2. Enrollment (creator onboarding)
creator ──GET /v1/enroll/message?scoreRequestId=…&creatorAddress=…
▼
canonical EIP-191 string (scoreId + address + domain)
│
├─▶ creator signs in wallet (personal_sign)
│
▼
creator ──POST /v1/enroll { scoreRequestId, creatorAddress, signature, scheduledFor? }
▼
apps/api
├─ verify signature (viem)
├─ refuse hasStubs: true
├─ refuse creator mismatch vs stored submission
├─ save enrollment (idempotent: same creator = same record)
└─ schedule notifications (T-60 / T-10 / T+0)
│
└─▶ emit('enrollment.created')
3. On-chain attestation
creator ──POST /v1/score/:id/publish
▼
apps/api
├─ refuse if hasStubs: true ◀─── trust rule
├─ refuse if not_configured ◀─── fail-closed on env
├─ canonicalize → keccak256 → digest (idempotency key)
├─ ABI-encode payload (schema hatch.score.v1)
├─ findExisting(digest) → return if already published
├─ viem writeContract(HatchAttest.attest)
└─ waitForTransactionReceipt
│
▼
BNB Chain receives the attestation, emits the Attested event; publisher
extracts attestationId + txHash, saves to attestations table, emits
'attestation.published' webhook.
See ADR 0006 for the canonicalization rules + ABI shape.
4. Launch scheduling
Enrollment auto-seeds T-60min, T-10min, T+0 notification slots. A
dispatcher tick (POST /v1/launch/dispatch, admin-gated) polls due slots
and fires them via a pluggable transport. D.5 swaps the default logging
transport for real Telegram + email.
See ADR 0007.
5. Webhook event bus
Every persist hook in scoring, enrollment, launch, attestation emits
a fire-and-forget event through webhooksService.emit(event, id, payload).
Delivery is at-least-once; the dispatcher retries failed endpoints with
exponential backoff. Subscribers verify payloads via HMAC-SHA256.
See ADR 0008.
Data model (key tables)
| Table | Purpose | Key columns |
|---|---|---|
scoreRequests |
Every submission + its computed score | id (UUID), contractAddress, aggregate, band, hasStubs, llmRawResponse, promptVersion, createdAt |
enrollments |
Creator signed commitment to launch | id, scoreRequestId (FK), creatorAddress, scheduledFor, signature, enrolledAt |
attestations |
On-chain publications | id, scoreRequestId (FK), digest (unique), txHash, chainId, subject, schemaId |
apiKeys |
Hashed API keys | id, prefix (indexed), sha256 hash, tier, label, revokedAt |
webhookSubscriptions |
Encrypted endpoints per event | id, endpoint, encryptedSecret, events[] |
webhookDeliveries |
Per-event delivery log | id, subscriptionId (FK), attempt, status |
launchPages |
Creator-editable pre-launch page | scoreRequestId (FK), tagline, description, ctaLabel, ctaUrl |
launchNotifications |
Scheduled notifs | id, scoreRequestId, offsetMinutes, scheduledAt, dispatchedAt, transport |
launchAttendance |
Session-hashed attendance beacon | id, scoreRequestId, sessionHash, firstSeenAt |
Full schema: apps/api/src/db/schema.ts.
Contracts
| Contract | Purpose | Where it's called from |
|---|---|---|
HatchRegistry |
Global config, admin, role-based access | every other contract |
HatchAttest |
ERC-8004-style attestations | apps/api publisher |
Hatcher |
Soulbound NFA (BAP-578) for verified humans | wallet mint UI (D.2) |
HatchNest |
Treasury with per-token/daily caps + time-locked withdrawals | E.* (seed LP, Crack) |
See ADR 0002 for why NFAs are soulbound, and HatchNest for the treasury caps (judge-panel audit item #1 — no single-sig above threshold).
Cross-cutting concerns
Observability
- Logs — pino JSON in prod, pretty in dev.
AsyncLocalStoragethreads request IDs through every line. - Metrics + traces — OTLP via OTEL SDK (A.4 scaffold; full wiring with L.5 deeper analytics).
- Errors — Sentry in production (planned for L.6).
Security
- CSP — route-scoped.
/badge/*gets relaxedframe-ancestors *for cross-origin embeds; everything else gets strict. - Rate limits — per-route override a global 300/min/IP catch-all.
- Admin — read-only, shared-secret bearer, timing-safe compare.
- Signatures — EIP-191 personal_sign for enrollment (creator-facing, readable in wallet).
- security.txt — RFC 9116 at
apps/web/public/.well-known/security.txt. - Bug bounty — gohatch.fun/bounty — activates at mainnet.
i18n
- next-intl, EN primary + 中文 parity.
- Every user-visible string has both translations.
- Locale switcher in header + footer.
- Non-negotiable rule (#7 in CLAUDE.md).
Trust rules (non-negotiable)
- Stubbed signals ⇒ preliminary flag ⇒ off-chain only.
- Preliminary rows excluded from leaderboards + percentile denominator.
- Attestation publisher refuses preliminary rows regardless of env wiring.
- Band-aware share copy — no "green" flex when you're actually amber.
- Admin panel is read-only.
Deployment
| Service | Platform | Trigger |
|---|---|---|
apps/web |
Vercel | main push → production, PRs → preview |
apps/api |
Railway (planned) | main push |
| Postgres | Supabase | managed |
| Contracts | BNB testnet today, mainnet after C.7 | forge script DeployAll |
See deployment guide for the hands-on playbook and vercel-deploy runbook for the Vercel specifics.
Non-goals
Worth writing down:
- Hatch is not a DEX. We don't run an AMM; Four.meme's bonding curve does that. Hatch sits alongside.
- Hatch is not a KYC provider. World ID handles humanness at the cryptographic layer; Hatch never sees a passport.
- Hatch is not a custodial service. The treasury is controlled by contract-level multisig + caps, not by anyone on the team.
- Hatch is not free on the graduation side. The 0.5% LP fee is the protocol's only revenue source. It's collected only when tokens succeed.
Where to go next
- Building something? → DEV-GUIDE.md and API.md.
- Understanding a specific feature? → guides/.
- Operating the system? → runbooks/.
- Making an architectural change? → read all eight ADRs first, then open a proposal PR.