System architecture
Mochi Network consists of three parts and one VPS. The three parts divide the work clearly; the VPS is the only place that holds secrets.
| Part | Where | Role |
|---|---|---|
| Contracts | Monad testnet (chain id 10143) | Source of truth: posts, votes, karma, ranking algorithms |
| Indexer | VPS | Cache: chooses which posts are considered and returns content. Never ranks |
| Web | CloudFront + S3 | Static client. No secrets, no server |
| VPS | api.mochi.meme | Runs the indexer and faucet. The faucet holds the system's only private key |
The chain is the source of truth
Karma, weight, and feed order all live on-chain. The indexer only answers "which posts", not "which posts are better". See Karma and weight.
Diagram
CloudFront + S3 (mochi.meme) VPS (api.mochi.meme)
static bundle, no secrets ├── indexer (SQLite, WS tail)
└── faucet /drip (private key)
│ │ │
│ └──── content · candidate selection ──┘
│
└──────────────► Monad testnet ◄─────────────
PostRegistry · 4 feeds · 3 registries
= source of truth
Cloudflare Pages (docs.mochi.meme) — static docs, public, connected to nothingThe web reads contracts directly for karma, weight, and ranking score, and calls the indexer for post content and candidate lists. The indexer reads chain logs. The faucet writes transactions to the chain with its own key.
Contracts — source of truth
Eight deployed contracts. Full addresses in Contracts and addresses.
PostRegistryholds posts, votes, and karma (karmaOf,weightOf).ChronoFeed,HotFeed,BestFeed,ControversialFeedare the four ranking algorithms.AlgorithmRegistrymaps feed slots to algorithms. The default slot isHotFeed.IdentityRegistryholds handles.CommunityRegistryholds community names and membership.
No contract has a governance function. There is no follow graph: vote weight is derived directly from the voter's own karma, not from who they follow.
Indexer — cache, not ranking
The indexer reads Monad logs into SQLite, runs one backfill, then tails over WebSocket.
It does two things:
- Select candidates.
/candidatesreturns the list of post ids an algorithm should consider, according to one of five strategies (recent,popular,trending,community,joined). This is the indexer's only influence on the feed, and it is deliberately naive and public: no secret weights, no personalization. - Return content.
/posts,/profile,/communities,/thread… return text, images, raw counts, and notifications.
Scoring happens on-chain: the web gets ids from the indexer and performs an eth_call to rank(viewer, ids) on the contract. The indexer cannot manipulate ranking because it does not rank at all. See Indexer API.
Web — static client
The web is statically exported, uploaded to S3, and served through CloudFront. It:
- Runs no server and holds no secrets. Every value in the bundle is public.
- Signs transactions with a passkey (WebAuthn), so there is no seed phrase to store.
- Reads the chain to display karma and weight, reads the indexer for content.
The documentation you are reading is a separate VitePress build (source in web/docs), hosted on Cloudflare Pages at docs.mochi.meme — it is not part of the app bundle. The old /docs URL on the app is permanently redirected to that host by an edge function.
On-chain and off-chain
| Data | Where | Notes |
|---|---|---|
Posts, comments, parentId, community label | Chain (PostRegistry) | Written as events, immutable |
| Like/dislike votes and each vote's weight | Chain | like/dislike write both karma and weighted counters |
karmaOf, weightOf | Chain (PostRegistry) | No intermediary reputation service |
| Ranking score | Chain (rank) | Web calls directly, free |
| Handle | Chain (IdentityRegistry) | |
| Community registration, join/leave | Chain (CommunityRegistry) | |
| Selected feed slot | Chain (AlgorithmRegistry) | |
| Post index by author / community / thread | Indexer | Derived from logs, rebuildable |
| Candidate list | Indexer | Only selects posts, does not score |
Raw counts likeCount / dislikeCount | Indexer | For display |
| Profiles, notifications, search | Indexer | These are views of the chain |
Gate cookies, drips table, image CIDs | VPS / IPFS | Auxiliary state and secrets |
Karma is also cached by the indexer as postKarma/commentKarma to draw profiles, but the official number remains PostRegistry.karmaOf, read directly from the chain.
Why deleting the indexer is safe
Because the indexer is only a cache of facts already on-chain. Delete mochi.db and restart, and it rebuilds from DEPLOY_BLOCK:
- No posts, votes, or karma lost — all of it remains in Monad's logs.
- No ranking lost — ranking was never in the indexer.
- No accounts lost — addresses and handles come from the contracts.
That is what makes the claim "the indexer cannot manipulate the feed" literally true. In exchange, rebuilding is not free: Monad produces roughly 288,000 blocks per day, and the public endpoint rejects eth_getLogs for more than 100 blocks. A dedicated RPC is therefore required, and mochi.db is kept across deploys.
Rebuilding is not a routine operation
Every hour that passes raises the backfill cost. Keep mochi.db alive across restarts; only rebuild when truly necessary.
Private gate
A deployment can be held behind a shared access code until it is ready to go public. The mechanism has only one secret, GATE_SECRET, set in two places:
- Edge — a CloudFront Function (
rewrite-profile.js) is published with the secret substituted into the placeholder at deploy time, so the secret never sits in the repo. - API — an environment variable on the VPS, next to the contract addresses.
The flow:
- A user submits a code at
/gate/. This page doesPOST /gateto the API. - The API checks the code against
GATE_CODES, then sets an apex-scopedmochi_gatecookie. - The cookie has the form
expiry.HMAC-SHA256(secret, expiry). No DB, no session. - Both edge and API verify the signature with that same secret before responding.
The edge checks before serving any route; the API checks before answering any route except /health and /gate*. /health is open because the deploy script and monitoring need it to answer before anyone has a code, and it only reports liveness plus a few numbers.
Rotating the secret and restarting the indexer revokes every old cookie at once. The gate hides the application, not the data: posts and votes are on-chain events, still public whether the gate is on or not.
One secret, two ends
Because the edge and API share exactly one secret and verify independently, there is no session store to break, to expire inconsistently, or to attack.