Guidance for AI agents (and humans) improving this repo and the website. Read this first.
Castora is a sharper Farcaster client by Arca. Live at castora.social. It was bootstrapped from a Supercast base and is being rebuilt into an Arca-owned product spine, shipping improvements step by step.
- Update the changelog on every user-facing change. This is mandatory, not optional.
- Prepend an entry (newest first) to
src/app/changelog/entries.ts— the website source of truth at/changelog. - Mirror it in the repo-root
CHANGELOG.md. - Keep wording user-friendly (people read it, not just engineers). Pick the right
tag(launch | feature | fix | security | infra). Group related changes into one dated entry.
- Prepend an entry (newest first) to
- Commits must be authored as
arcabotai, or Vercel won't deploy. Vercel maps the commit author email to a Vercel-project member. Usearcabotai's email; the repo is already configured withgit config user.email "261107832+arcabotai@users.noreply.github.com"(arca22775@gmail.comalso works). Do NOT author commits asfeliramii@gmail.com— that maps tofelirami, who lacks project access, and the deploy is BLOCKED. maindeploys to production. Merging/pushing tomaintriggers a Vercel production deploy. Land work via PRs; keepmaingreen. CI gates every PR.- Keep deploys boring. Don't enable side-effectful background jobs, run prod DB migrations, or flip risky flags without explicit sign-off. Prefer feature flags + local testing first.
- Farcaster / Neynar: always go through the server-only client
@/lib/neynar(neynar.get('/v2/...')). Never hardcodehttps://api.neynar.comor readprocess.env.NEYNAR_API_KEYin a route — that was centralized on purpose. - API base URL: use
HOST_URLfrom@/utils/hostURL. It preferswindow.location.originon the client so localhost/preview talk to their own origin. Don't reintroduce a hardcoded prod URL. - Auth: API routes use
@/utils/auth/isAuthenticated+@/utils/auth/isAuthorized. Cron / scheduled routes use@/utils/auth/requireCronAuth(isCronAuthorized, fails closed onAuthorization: Bearer $CRON_SECRET). - Webhooks verify signatures/tokens (Stripe
constructEvent, Neynar signature, DaimoBasictoken). Keep it that way.
npm install
vercel env pull .env.local --environment=development --yes # needs `vercel link` to arcas-projects-d844d101/castora
# IMPORTANT: after pulling, set these so localhost talks to itself (pull resets them to prod):
# NEXT_PUBLIC_APP_URL="http://localhost:3000"
# NEXT_PUBLIC_URL="http://localhost:3000"
npm run prisma:generate
npm run dev # http://localhost:3000- Prisma uses
POSTGRES_PRISMA_URL(pooled) +POSTGRES_URL_NON_POOLING(direct). The Prisma CLI reads.env, not.env.local, so for CLI commands:set -a; . ./.env.local; set +afirst. - Login is via Privy (app id
cmpke74en00660cjvnb9aq32k; the auth iframe is hosted at the custom domainprivy.castora.social). For localhost login to work,http://localhost:3000must be in the Privy app's allowed origins (dashboard → Configuration → App settings → Domains). - Because the Privy auth iframe is cross-site (
privy.castora.social), a browser that blocks third-party cookies can't read the session → the app silently falls back to guest. If you log in but stay "guest" in a clean/automation Chrome profile, allow 3rd-party cookies (or launch Chrome with--disable-features=ThirdPartyStoragePartitioning,TrackingProtection3pcd). - Unauthenticated API routes returning
401/403is expected (auth gates), not a crash.
The Vercel Development, Preview, and Production environments all use the same Railway
Postgres database AND the same Privy app (cmpke74en…). There is no separate dev/staging data.
Consequences:
- Running
npm run devlocally talks to the live production database. Local reads/writes, account connections, and any posting hit real prod data and real Farcaster accounts. - A
prisma migrate/db pushrun locally would alter the production schema. Never run DB migrations/seeds from local. - "Run prod on localhost" =
vercel env pull .env.local --environment=productionthen re-apply theNEXT_PUBLIC_APP_URL=http://localhost:3000override. (A dev-env backup can be kept as.env.local.dev.bak.) Treat the local window as if you're operating on castora.social directly. - Recommended next infra step: stand up a real isolated dev environment (separate Railway DB + separate Privy dev app) so local work can't damage production.
- Hard gate:
typecheck+build. Non-blocking:lint(project setseslint.ignoreDuringBuilds). - Secret scan: gitleaks.
.gitleaks.tomlallowlists0x…EVM addresses (public, not secrets); add narrow allowlist entries for other confirmed false positives — never disable the scan.
- Workers (
src/scripts/*were never in this fork; being reconstructed one by one). All are gated byCRON_SECRET(isCronAuthorized, set in Vercel prod/dev). Status:- ✅
cast/send-scheduled-drafts→sendScheduledDrafts(Draft table; recurring + reactions via@/utils/drafts) andcast/send-scheduled→sendScheduledCasts(ScheduledCast table, standalone casts only — threads TODO). Both reconstructed but OFF: gated behindCASTORA_ENABLE_SCHEDULED_SENDER=trueAND not invercel.jsoncrons, so they post nothing until you set the flag + add the cron. End-to-end posting was NOT tested (would post real casts) — verify against a test account / empty schedule before enabling. - ⬜
cancel-expired-subscriptions(Stripe — cancel logic exists instripe/cancel-subscription; build for review, keep disabled). - ⬜
send-expiration-reminders(DMs via@/utils/direct-casts— needs a Farcaster Direct Cast API key; see Direct Casts note below). index-superanon-scores-from-duneis SHELVED (kept, not removed). Superanon was a Supercast-only feature; its account@superanon(fid 862100) has been dormant since 2025-10-28 and the Dune query was never in this fork, so there is no live data to index. The anon-posting idea lives on independently as Anoncast / Anon World ($ANON). Removed from vercel.json crons. Do NOT reconstruct unless a live scoring source returns. The leaderboard routes +SuperanonScoremodel +NEXT_PUBLIC_SUPERANON_FIDare dormant legacy.- Direct Casts (DMs):
@/utils/direct-casts.tssends viaapi.warpcast.com/v2/ext-send-direct-castwith a Farcaster app API key (WARPCAST_DM_API_KEY— rename toFARCASTER_DM_*when wiring it). Send-only and account-bound; there is no official read/receive DM API, so full native DM parity inside Castora is not feasible today (the on-protocol E2E "Direct Casts" FIP is still just a proposal). Fine for one-off sends like expiration reminders.
- ✅
src/utils/auth/isAuthenticated.ts wraps token verification and the
prisma.supercastPrivyUser.findUnique() DB lookup in one try/catch that, on any error, returns
{ authenticated: false }. So a transient DB outage (the Railway hobby Postgres blips) makes
every data route 401 → the timeline goes empty and the user can even get dropped into the
guest / "connect account" onboarding. Because local == prod data, that onboarding could write
duplicate records into production.
Fix: distinguish a genuine token-verification failure (→ real 401) from an infrastructure
error like a DB connection failure (→ surface a retryable 503, ideally with a short retry). Do
not treat "database unreachable" as "not authenticated." This was the root cause of the
"timeline not showing" incident debugged on 2026-05-30.
Shipped & merged to production this run (PRs #7, #8, #9, #10, #11, #12 — all CI-green):
- Localhost dev fixed (
hostURLpreferswindow.location.origin); CI + gitleaks secret scan added. - Neynar centralized behind
@/lib/neynar(68 files migrated). - Sensitive-route audit + cron routes gated by
CRON_SECRET(requireCronAuth);CRON_SECRETset in Vercel prod/dev. - Public
/changelog+CHANGELOG.md; thisAGENTS.md. - Superanon/Dune worker shelved; scheduled-cast senders reconstructed (OFF behind a flag).
What's needed next (priority order):
- Fix
isAuthenticated(above) — highest value; it's a live footgun given shared prod data. - Isolate environments — separate dev DB + dev Privy app so local ≠ prod.
- Workers: build
cancel-expired-subscriptions(Stripe, for review/disabled); wiresend-expiration-remindersonce a Farcaster Direct Cast API key is provided; verify the scheduled senders end-to-end on a test account before enabling (CASTORA_ENABLE_SCHEDULED_SENDER+ cron). - Tech debt: dedupe
react-queryv3 →@tanstack/react-queryv5; plan Next.js 13.5 → 15/16; re-enablereactStrictMode+ ESLint-on-build (both "todo" innext.config.js).
Live browser debugging: you can drive Chrome via CDP without an MCP — launch a debuggable
instance (open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=<dir> --disable-features=ThirdPartyStoragePartitioning,TrackingProtection3pcd) and talk to it with a small
ws-based script (Chrome 148 blocks debugging the default profile, so use a separate --user-data-dir).
- Other tech debt: two query libs coexist (
react-queryv3 +@tanstack/react-queryv5 — converge); Next.js is 13.5 (upgrade path to 15/16);reactStrictModeand ESLint-on-build are disabled (next.config.js, "todo switch back").
- Deploy happens automatically on merge to
main. To check: Vercel projectprj_h4VzZir37pMPnqBTeuQhDDd0ZNAE/ teamarcas-projects-d844d101. A deploy is healthy when the production deployment reachesReadyand is attributed toarcabot/arca22775@gmail.com.