Back to Browse

Forkcaster Xyz MCP Server

Developer ToolsLow Risk10.0MCP RegistryRemote
Free

Server data from the Official MCP Registry

Resolve Farcaster casts to deep-link URLs for any supported Farcaster client app. No auth.

About

Resolve Farcaster casts to deep-link URLs for any supported Farcaster client app. No auth.

Remote endpoints: streamable-http: https://forkcaster.xyz/mcp

Security Report

10.0
Low Risk10.0Low Risk

Valid MCP server (1 strong, 1 medium validity signals). No known CVEs in dependencies. Imported from the Official MCP Registry. Trust signals: 3 highly-trusted packages.

1 tool verified · Open access · No issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Found in Source Code

Found by scanning the linked source code. This listing connects to a hosted endpoint, so none of this runs on your machine: it describes what the server software does where it is hosted.

HTTP Network Access

Connects to external APIs or services over the internet.

file_system

Applies to the server that hosts this plugin, not to your machine.

How to Connect

Remote Plugin

No local installation needed. Your AI client connects to the remote endpoint directly.

Add this to your MCP configuration to connect:

{
  "mcpServers": {
    "xyz-forkcaster-mcp": {
      "url": "https://forkcaster.xyz/mcp"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub README.

forkcaster — agent brief

Audience: coding agents. Prefer facts over prose. Update this file when architecture or commands change.

Intent

Micro-site that mirrors Farcaster cast URLs for sharing without the Farcaster app.

  • Pattern: /{username}/{castHash} where castHash matches 0x[0-9a-fA-F]+ — short or full form
  • Examples: /zoo/0x895ecc74 and /decent-artlu/0xbb54368f → SSR cast card + links
  • Data: raced across a fleet of sources (worker/cast-source.ts + worker/shim.ts on the shared worker/race.ts kernel) — see "Cast source race" below. shim (https://shim.artlu.xyz/wc/{username}/{hash}, owner-run Snapchain reader with a keep-warm cron) anchors completeness and hosts the ban-signal read (/signed/{fid} — fleet-raced like every shim read); farcaster.tv is fast but quote-blind; raw nodes haatz/snap.farcaster.xyz:3381 are embed-complete but text-blind (the gateway strips the mention placeholder characters from text — shim owns the @username substitution — so mention-bearing raw wins are held until canonical text settles or is proven unavailable) and get the fid by racing the shim fleet's /f/{username} against their own /v1/userNameProofByName (first numeric answer wins, loser aborted; the shim-side winner is labeled with its fleet member id), plus a full hash requirement (castById 400s on short hashes). All upstream calls are capped at 5s.
  • Missing cast: a consensus miss — every miss-authoritative source consulted and missed — ⇒ 404 page; every source errored ⇒ link-only page (200). No single source's miss is a verdict. All upstream calls are capped at 5s (AbortSignal.timeout) — shim's cold sharded lookups can otherwise hang 15s+; timeout ⇒ unreachable. Strict statuses are the JSON variant's job (see below)
  • Banned accounts: embeds render for everyone — but some clients hide unsigned authors. Since Oct 15, 2026 (Network School takeover), accounts must accept the "social smart contract" (Network Code); acceptance is an on-chain access NFT. Shim exposes it: GET https://shim.artlu.xyz/signed/{fid} → {"signed":true|false}. Unsigned authors: registry clients flagged requiresSigned (e.g. farcaster) are suppressed from the action row; the check fails open — unknown state suppresses nothing
  • farcaster.tv as a cast data source is quote-blind (it drops {castId} embeds) and unofficial — it races as the partial tier only (700ms HTML box, client-side quote fill-in). react-farcaster-embed's own default endpoint is farcaster.tv — always pass castData so it never fetches. The live stats row (replies/recasts/likes) is still fetched client-side from farcaster.tv — it sends access-control-allow-origin: * and answers for unsigned authors too; any failure just leaves the row absent

Architecture

The system is a waypoint layer over Farcaster: every public URL shape maps to exactly one canonical surface, and every page answers one question — which app opens this? There is no database, no KV, and no edge cache on this side — data is raced across a fleet of sources on every request. The product is the routing verdict, not stored content.

Cloudflare Worker (Hono; dual-domain; registration-order routing)
├─ routes: cast · profile · casts-page · aliases · mcp · /transparency · /api · /t
├─ RESOLUTION KERNEL — worker/race.ts
│    staggerDelays · collectTerminals · collectiveRace
│    ├─ raceShimProbe — every shim-shaped read (profiles, quotes, signed,
│    │                  aliases, MCP): cloud + the potato fleet
│    └─ raceCast — provider roster (shim ∥ potatos ∥ haatz ∥ snap ∥
│                  farcaster.tv) + completeness box + race telemetry
├─ SOURCES: shim (cloud) · potato fleet (replicas) · haatz/snap (raw
│   nodes) · farcaster.tv (partial, quote-blind)
├─ RENDERERS: resolve once, render twice — SSR HTML + strict JSON twin
├─ STATEFUL LAYER: fc_client cookie → last-used badge, auto-open, pill
│   order (client-side writes; the server only reads)
└─ TELEMETRY: one anonymous Analytics Engine datapoint per resolve →
     offline snapshots → /transparency

Verdicts are collective — no single host is the source of truth. Fleet legs start staggered (tiny random steps behind the leader; a fast winner cancels the stragglers before they fire). The first affirmative answer wins; a consulted miss (asked, no data) is a vote — not-found settles only when every miss-authoritative leg has consulted and missed; errors surface only when every leg errored. Any one host — including cloud shim — can die without changing a single verdict. The price is deliberate: a true miss resolves as fast as the slowest leg, not the fastest.

Consumer-matched strictness. HTML fails open — a human always gets a page (found → card, partial → self-healing card, unreachable → link-only). JSON is the strict record (404 / 503 + Retry-After, availability-gated fields, quotes never dropped). Chat link-preview bots get HTML + OG meta unconditionally: negotiation is ?format=json / Accept, never UA sniffing, and SSR output is byte-identical for every device.

Design laws (enforced everywhere; detail in the sections below):

  • Waypoint law — one surface per account; aliases redirect, never render.
  • Identity law — the fname is the PRIMARY handle everywhere; ENS names are attestations.
  • Echo-the-valid-input — first-party links carry the gate-validated route input; display, OG, and client targets carry the primary.
  • Privacy: no identifiable tracking, only analytics. One anonymous click datapoint per client selection; the negative list (no cast IDs, no usernames, no IPs, no UA strings, no identifiers) is verbatim-checkable in worker/track.ts and declared on /transparency. Pages make no third-party requests — logos are self-hosted because a hotlinked logo would leak every visitor's IP to that client's servers. The one cookie carries a client slug, nothing else.
  • Personalization is client-side on cast pages. The page shell (the client sheet's pills included, gates applied) is cookie-blind server-side; SHELL_SCRIPT reads the local fc_client cookie and applies the last-used badge, the resolution-first primary ("Open in X" — a direct anchor with the pill's exact wiring — beside a muted "More apps" sheet trigger), and the auto-open state machine after parse (the sheet's auto-open toggle syncs from the same cookie). That is what makes the full page — cookie-holders included — servable from the 12h edge shell; profiles cache 10 minutes (they are mutable) under the same client-side model. Bump SHELL_SALT in worker/edge-cache.ts to invalidate every shell (there is no global purge; v3 dropped the pre-sheet UI — bare pill row + on-page auto-open chip; v4 reached cached shells with the Safari-safe sheet CSS; v5 reached them with the kind-aware chooser copy and single-offer bare-row shape).
  • Registry as the one roster — clients and their verification provenance are enumerated exactly once, in worker/client-registry.ts.

Stack

LayerChoiceNotes
Runtime / package mgrBunlockfile: bun.lock; scripts use bun run
UI (landing)Preact 11SPA assets; entry src/main.tsx
Cast pagesHono on Cloudflare WorkerSSR in worker/
Cast dataProvider race (cast-source.ts)shim ∥ farcaster.tv ∥ haatz ∥ snap ∥ shim-potato fleet — first validated win; completeness box for HTML, complete-leg fleet roster for JSON; see "Cast source race" below
URL embed cardsunfurl.artlu.workers.devcalled via the UNFURL service binding (workers.dev→workers.dev fetch is blocked, CF error 1042); GET /?u={url} → OG title/description/image (cached, fails open to hostname-only)
Cast embedreact-farcaster-embedrendering only — castData always supplied, never fetches
Live statsclient-side script → farcaster.tvLIVE_STATS_SCRIPT in cast-scripts.ts; fills the SSR-stripped stats slot from the browser; fails silent (row absent); the SSR data path is fleet-raced server-side
SSRpreact-render-to-stringafter await FarcasterEmbed({ castData })
BundlerVite 8@preact/preset-vite + @cloudflare/vite-plugin
LintBiomebun run lint
Typecheckbun checktsc-compatible, built into Bun; no typescript pkg
Testsbun:testbun test (cast route + client registry)

Commands

bun install
bun run dev          # wrangler dev (local worker + assets)
bun run build        # vite build → dist/
bun run typecheck    # bun check (tsc-compatible; reads tsconfig*.json)
bun run deploy       # build + wrangler deploy
bun run lint         # biome lint .
bun test             # worker tests (cast route + client registry)
bun run cf-typegen   # wrangler types → worker-configuration.d.ts

Layout

/
├── index.html                 # SPA shell → /src/main.tsx
├── vite.config.ts             # preact + cloudflare + react→preact aliases
├── wrangler.jsonc             # main=worker/index.ts; not_found_handling=none
├── public/farcaster-embed.css # linked from SSR HTML
├── public/logos/              # per-client marks for the action row
├── quilcast-router-shortcut.md # the iOS/macOS share-sheet router: mapping, build/sign, distribution
├── shortcuts/                 # generated .shortcut artifact (unsigned; `bun run shortcut:quilcast`)
├── worker/
│   ├── index.ts               # Hono app; export { app }; default { fetch }
│   ├── client-registry.ts     # client registry (verified templates; zero-dep, vendored-able)
│   ├── clients.ts             # fc_client cookie binding (last-used + auto-open prefs)
│   ├── shim.ts                # shim identity client (routes, ShimCast/ShimUser, /wc /h /f /u /user /casts-and-replies /signed, /refresh as the index warmer; fleetBases parses the fleet var)
│   ├── race.ts                # fleet-race kernel: RaceLeg, staggerDelays, collectTerminals, collectiveRace — shared by cast-source + shim
│   ├── cast-source.ts         # provider race: shim ∥ potato fleet ∥ haatz ∥ snap ∥ farcaster.tv (+ the /~/{hash} hash-only rescue); fleet stagger + collective miss parity
│   ├── cast-page.tsx          # resolve (race) → render orchestration: SSR / 404 / link fallback
│   ├── cast-data.ts           # pure Shim → embed-data mapping + embed helpers
│   ├── cast-actions.tsx       # client sheet — chooser shapes (single-offer bare row / trigger + dialog), pills byte-identical inside
│   ├── cast-scripts.ts        # inline client scripts (TRACK/CLIENT/DEEPLINK/AUTO/LIVE_STATS/QUOTE_FILL)
│   ├── profile-page.tsx       # profile landings: resolve-once/render-twice, waypoint law
│   ├── warpcast.ts            # Warpcast channel API client (zod contract; /v1/channel — keyless, first-party)
│   ├── channel-page.tsx       # channel landings: resolve-once/render-twice, waypoint law
│   ├── shell.tsx              # shared page frame (head/meta/CSS/footer) — cast pages + profiles + /transparency
│   ├── cast-ref.ts            # MCP cast-ref: zod contract + shim-direct resolution
│   ├── mcp.ts                 # MCP SDK v2 server factory (get_cast_link) — only SDK import site
│   ├── cast-route.test.ts     # bun:test; mock.module + exact-URL fetch mock
│   ├── clients.test.ts        # registry unit tests: templates, gating, signed rule, deeplinks
│   ├── cast-source.test.ts    # race unit tests: eligibility, box, miss/error, abort, adapter mapping
│   ├── race-stats-store.ts    # lmdis snapshot read-through TTL cache (10min TTL / stale 24h / 2s cap)
│   ├── fid-race-store.ts      # mirror of the above for the "Author id races" snapshot (key fid-race)
│   ├── vendor/lmdis-sdk.ts    # vendored lmdis SDK (Upstash-shaped seam; get/set only)
│   └── shims/server-only.ts   # empty stub for Next.js marker package
├── scripts/
│   ├── race-stats.ts          # offline stats job: AE SQL → snapshot → lmdis (dotenvx env; --dry)
│   ├── race-stats-lib.ts      # pure query builders (fixture-tested)
│   ├── fid-race.ts            # offline fid-race job: FID_RACE AE SQL → snapshot → lmdis (mirror of race-stats.ts)
│   ├── fid-race-lib.ts        # pure fid-race query builders (fixture-tested)
│   ├── client-rank.ts         # offline rank job: CLICK_STATS AE SQL → weighted snapshot → lmdis
│   ├── client-rank-lib.ts     # pure query builders + weighting (fixture-tested)
│   ├── stats-alert.ts         # ntfy threshold alerter over the live lmdis snapshots (`stats:check`; --dry)
│   ├── quilcast-shortcut.ts   # generates the quilcast → share-sheet shortcut (mapping in quilcast-router-shortcut.md)
│   └── probe/                 # live-service probes (lmdis, AE SQL, shim timing)
└── src/                       # landing only (App.tsx)

Routes

PathHandlerResponse
/static asset index.htmllanding SPA
/api/Honoagent-facing index: the JSON surfaces this worker serves (cast, profile, channel, raceStats, transparency)
/prefsHonoPOST JSON { auto?: "on" | "off", delay?: 0–5 } (at least one field) → 204 + Set-Cookie (fc_client); delay omitted from the cookie at the default 5s. Auto-open preference write (server cookie — Safari ITP exemption)
/tHonoPOST beacon (one per client selection): { c: slug, k: pill|auto|go-now, t?: web|native, d?: coarse|fine, f?: first|switch|repeat } → 204 always (invalid = dropped, indistinguishable). Allow-listed against the active registry; one anonymous CLICK_STATS datapoint — no cast IDs, no IPs, no identifiers
/transparencyHonoHTML facts page (what/data/tracking/domains — Domains carries registration expiry + RDAP confirm links — plus race-stats and client-rank tables filled server-side from lmdis snapshots, "stats as of" lines, and the registry-driven Client coverage matrix — clients × route kinds, straight from targetFor) or JSON twin via ?format=json/Accept (racingStats.status/clientRank.status: live | coming-soon; clients lists per-kind coverage); reserved exact path — cast URLs for a username named transparency still work
/race-stats.jsonHonoraw stats snapshot (same cached read as /transparency); 404 when absent; cache-control: public, max-age=21600
/mcpHono → MCP SDK v2MCP Streamable HTTP endpoint (POST JSON-RPC; public, no auth). One tool, get_cast_link: cast reference (longhash ∥ fid+shorthash ∥ username+shorthash) + target client → client URLs + author signed status; farcaster.xyz is the canonical surface.
/:username/:hash (0x…)Hono SSRHTML: 200 cast card + OG/Twitter meta (title = DisplayName (@primary) — identity law: the fname is the author handle everywhere on the page (og, canonical/og:url path, embed links, stats slot; ENS names are attestations, profile-only), legacy casts without fname degrade to the newest-proof username, description = whitespace-collapsed cast text, 280 chars, image = first image embed else avatar; summary_large_image card only when the image is a cast embed image; og:site_name = forkcaster) + registry-driven client action row (see "Client registry" below); unsigned author (/signed/{fid} false): requiresSigned clients (e.g. farcaster) suppressed, the embed's Show-on-Farcaster icon hidden (the library's hideFarcasterLogo — the farcaster.xyz page is dead for them, a local twin would self-link) and its stats slot flagged so client-built links go to the local waypoint; 404 when the fleet consensus is no-such-cast; 200 link-only when every source is unreachable
/:username/:hash?format=json (or Accept: application/json)Hono JSONStrict-status agent variant — see "Agent JSON variant" below
/:usernameHono SSRProfile landing (waypoint law, strict dial: the identity card IS the page): the hydrated shim /user/{fid} entity (displayName/pfp/bio/fid — bio collapses with explicit http(s) URLs linkified (new tab, noopener; mentions/#channels stay inert), primaryAddress as truncated <code> that is itself the Basescan link (new window) + copy button) + a "Casts and replies" pill; no cast list — the list lives on /:username/casts-and-replies; the way out is the STATEFUL menu: registry-driven pills (rank-ordered, data-client, badge on the flat resolution — own p: cookie slug else the cast slug), auto-open banner/overlay from the same resolution (last-used cookie; ?noredirect=1 supported), telemetry beacons on clicks; unsigned accounts (/signed/{fid} false, null fails open) get farcaster suppressed (requiresSigned on the kind target) + the read-only notice — Quilcast (/u/ route) + Quorum (source-verified profile deep link) remain via the registry; JSON twin stays cookie-free; OG meta (og:type profile, twitter:card summary, image = pfp); single segment registered LAST — reserved exact routes and the two-segment cast route win by registration order; trailing-slash forms (/{username}/, /{username}/casts-and-replies/) 301 to the canonical non-slash URLs (query preserved); charset gate + bot-path denylist 404 without upstream calls; 404 on a fleet-wide /f miss (or sparse /user shape), 503 + Retry-After when every source is unreachable; the HTML path skips the timeline call entirely; echo-the-valid-input: first-party links (the pill) carry the gate-validated route input — shim resolves any resolvable alias (fname/ENS/basename) to the same fid, and /user/{fid} answers the NEWEST username proof, which can differ from what was typed — while display, OG/canonical, and external client targets carry the PRIMARY handle (identity law: the fname — the account's first name — is primary; ENS names are attestations, rendered bold-primary + muted parens on the card @arfonzo (arfonzo.eth, …), capped at 3, and listed uncapped in the JSON; legacy entities without fname degrade to the newest-proof username)
/:username?format=json (or Accept: application/json)Hono JSONStrict-status profile twin: { username, fid, attestations?, displayName?, bio?, pfp?, primaryAddress?, casts?: [{ hash, text?, timestamp, url }], note? } — username is the PRIMARY handle (the fname; identity law), attestations lists every verified ENS name uncapped when known — 404 / 503 + Retry-After like the cast JSON; listed in /api/
/:username/casts-and-repliesHono SSRThe ONE list surface (farcaster.xyz URL-shape mirror): every cast rendered in full — exactly the individual cast page's pipeline (react-farcaster-embed SSR, unfurl-enriched URL embeds ≤3/cast, shim-hydrated quotes ≤3/cast via the same fetchQuote (/h with the /u→/wc fallback), hardened blank targets, per-card live-stats slot; LIVE_STATS_SCRIPT fills every slot on the page). Author attached from the hydrated entity (shim timeline rows are author-implied); minimal @handle head linking the profile (the link echoes the validated input, the visible handle carries the primary — identity law), plus the stateful FEED menu + auto-open (farcaster + quorum are the seeded feed targets — quorum's pill is coarse-only and scheme-only, its list shows top-level casts; suppressed to an empty row + read-only notice when unsigned; every card's farcaster.xyz cast anchors (Show-on-Farcaster icon + client-built stats links) retarget to the local waypoint via the shared renderEmbedCard — signed or not, the waypoint always resolves (profile anchors and author-typed farcaster.xyz links stay)); OG preview identical to the profile's (same card visual, deeper page — canonical/og:url stay on this URL); no JSON twin (alternate link omitted). Fleet /f verdict ⇒ 404; every source unreachable ⇒ 503 + Retry-After; timeline failure fails soft, ok-empty ⇒ "No casts yet."
/~/conversations/:hash (0x…)Hono redirectWarpcast legacy thread-shape alias — the purest waypoint, no surface: resolve via shim /h/{hash} (fid fallback via /u when the cast is username-less) and 302 (not 301 — mapping stays cache-free) to the canonical /{username}/{fullHash} cast waypoint — the PRIMARY handle (fname first; shim /u answers it too), relative Location (dual-serve hosts stay self-contained); a bare shim miss is no verdict — the index is partial (kept-warm fids only) — so it falls to the farcaster.tv hash-only rescue (/~/0x…, resolves the live chain; raw nodes can't — castById demands fid + full hash), which also fires a fire-and-forget shim /refresh warm so the next visitor hits /h; 404 only on an authoritative rescue miss / unresolvable username, 503 + Retry-After when the surviving source is unreachable; short hashes miss at shim (measured) and 404 honestly
/~/profiles/:idHono redirectfarcaster.xyz's fid-profile URL shape (it renders the profile in place; the {username} shape answers there too) — forkcaster keeps ONE canonical surface per account, so it's a waypoint, not a second page: all-digits ids resolve via shim /u/{fid} (profile-family discipline — 8s window, one retry; non-2xx/throw ⇒ 503 + Retry-After, HTTP-200 miss ⇒ 404) then 302 (never 301 — mapping stays cache-free) with a relative Location to /{username} (the PRIMARY handle — shim /u is fname-first); the username shape canonicalizes with zero fetches (charset gate, lowercased); ids past Number.MAX_SAFE_INTEGER and charset garbage 404 with zero fetches; trailing-slash form 301s to the non-slash alias (query preserved)
/~/channel/:idHono SSRChannel landing (waypoint law, identity-only): the identity card from the official Farcaster channel API (api.farcaster.xyz/v1/channel — the host formerly at api.warpcast.com, byte-identical; keyless, ~150ms, zod-wrapped in worker/warpcast.ts) — header banner/icon, name + /{id} handle, description (escaped plain text, no mention resolution), created date + lead-fid facts; engagement counts (follower/member) are withheld from HTML and live in the JSON twin; way out = a hand-verified pill trio (per-target provenance in the client registry) — Farcaster (farcaster://~/channel/{id} native attempt, source-verified against the open-source app; the ~/ is load-bearing), Firefly (firefly.social/channel/farcaster/{id}, web-only, owner-confirmed route) and a coarse-pointer-only Quorum pill (quorummobile://feed?channelKey={id}, source-verified; its web build has no channel page, so desktop never sees it) + the channel's own external link only when http(s); the row rides the cast page's stateless DEEPLINK_SCRIPT for click-time native attempts — no fc_client memory, no click telemetry (owner call: don't remember or track); SSR is byte-identical for every device (CSS pointer-class picks, no UA sniffing). OG og:type website, twitter:card summary, image = channel icon. The /~/channel/{id} form — farcaster.xyz's own channel URL shape, which canonical + og:url carry — is the only shape served: the bare /channel/{id} shape 404s with zero fetches (a plain id would shadow a user literally named channel, while /channel/0x… hashes fall through to the cast route — the tradeoff /transparency still carries). Registered BEFORE the two-segment cast route so hash-shaped ids route here; charset gate lowercases and 404s garbage with zero upstream fetches. 404 on the API's HTTP 400 miss; 503 + Retry-After on anything else (no fail-open source). One stateless script (the trio's DEEPLINK_SCRIPT), zero cookies, zero telemetry. The recent-casts skeleton is deferred: shim's channel feed measured 8–15s+ per call
/~/channel/:id?format=json (or Accept: application/json)Hono JSONStrict-status channel twin: { id, name?, description?, image?, headerImage?, createdAt?, parentUrl?, leadFid?, moderatorFids?, followerCount?, memberCount?, publicCasting?, pinnedCastHash?, externalLink?, farcaster } — 404 / 503 + Retry-After like the profile JSON; carries the engagement counts and routing data (parent key, pinned cast) the HTML withholds; listed in /api/
other missing pathsworker 404 / no SPA fallbacknot_found_handling: none

Multi-domain (dual-serve): the same worker serves forkcaster.xyz and warpcats.com (custom-domain routes in wrangler.jsonc). All code is host-agnostic (per-request origin, same-origin fill script, host-only cookie), and every page canonicalizes to the primary: cast pages emit <link rel="canonical"> + og:url from the PRIMARY_ORIGIN var (absent when unset — bare workers.dev keeps the pre-dual-serve output), the landing's canonical is baked into index.html. Race telemetry carries the request host as a blob for per-domain breakdown. Cookies/badges are per-domain by design.

Agent JSON variant (?format=json)

Shipped. Resolve once (fleet-raced cast lookup + unfurl + quote hydration + signed check → resolveCast()), render twice — renderCastHtml() / renderCastJson() in worker/cast-page.tsx. The return type morphs on the asker.

Negotiation — explicit first, no UA sniffing:

SignalResponse
?format=json query paramJSON (documented contract; wins over Accept)
Accept: application/jsonJSON
anything else (browsers, */*, chat link-preview bots)HTML

Decisions:

  • JSON only — no kv/plain-text form (kv is lossy; JSON's token overhead is negligible).
  • Strict statuses on JSON, lenient on HTML (deliberate divergence): JSON → 404 when the fleet consensus is "no such cast", 503 + Retry-After: 60 when every source is unreachable. HTML keeps its 404 page / 200 link-only page. Agents handle status codes better than rendered fallbacks.
  • Availability-gated payload — a field appears only when its data exists: author, ISO 8601 timestamp, text, image embed URLs, URL embeds (unfurl title appended when unfurl succeeded, bare URL otherwise), quote embeds, canonical farcaster.xyz + Firefly links. The author username, the farcaster link, and hydrated-quote pointers carry the PRIMARY handle (identity law: fname first; ENS names are attestations).
  • Quotes are pointers, never dropped: hydrated quote → forkcaster URL (requires the username from the hydrated cast — the quoted author's PRIMARY handle under the identity law); hydration failure → Firefly full-hash URL (quote embeds carry {fid, hash}, no username). HTML's rendering constraints (unguarded author.pfp.url) don't apply to JSON — failed hydration degrades to a URL the agent can fetch.
  • Ban signal: signed: false emits a short note field; unknown/failed check fails open silently (same fail-open semantics as the HTML action row).
  • Discoverability: HTML pages emit <link rel="alternate" type="application/json" href="?format=json">.
  • Deferred: UA-allowlist morphing (GPTBot, ClaudeBot, …) as a later progressive enhancement. Must stay an allowlist — Telegram/Discord/iMessage preview bots must keep receiving HTML + OG meta.
  • Cast HTML shells edge-cache 12h (worker/edge-cache.ts — found-complete only, cookie-free requests only, salted keys); every JSON variant bypasses the cache entirely and must stay that way.

MCP

https://forkcaster.xyz/mcp (also warpcats.com/mcp) — MCP Streamable HTTP endpoint, public, no auth, stateless. One tool: get_cast_link — resolve a Farcaster cast (full 40-hex hash, or fid/username + short hash) into deep-link URLs for any supported client, with the author's signed-contract status. Links-only clients (requiresExternalLinks) are gated: they answer client_ineligible and drop from all when the resolved cast carries no external URL embed. Wiring: worker/mcp.ts (SDK) + worker/cast-ref.ts (zod contract, shim-direct resolution).

Registry listing: server.json (repo root, official registry schema 2025-12-11, name xyz.forkcaster/mcp). To publish, install the official publisher CLI — a GitHub-release binary. ⚠️ do NOT bunx/npx the npm package mcp-publisher — it's an unrelated browser-automation project that collides with the name:

curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher && sudo mv mcp-publisher /usr/local/bin/

One-time domain verification: generate an ed25519 keypair (openssl genpkey -algorithm Ed25519 -out key.pem), add the apex TXT record it prints (v=MCPv1; k=ed25519; p=<public key>), then:

mcp-publisher login dns forkcaster.xyz --private-key <hex>   # signs the challenge → xyz.forkcaster namespace
mcp-publisher publish                                        # submits server.json to registry.modelcontextprotocol.io

key.pem is the publishing credential — keep it safe, never commit it.

Cast source race

Cast lookups race the provider fleet (worker/cast-source.ts, rpc-racer pattern: per-provider abort + 5s cap armed at actual leg start, first validated win, losers aborted; a win must resolve the requested hash, else it's downgraded to an error). The roster: the shim-potato fleet (the SHIM_POTATO_BASE var carries the FULL shim fleet, comma-separated, cloud's own base included — var order is the stagger/primary order, first entry leads at 0ms; a fast winner cancels the stragglers before they fire), the haatz/snap raw nodes, and quote-blind farcaster.tv (partial). No var = the legacy cloud-led roster. The strict JSON twin rides the complete-leg subset via jsonRoster (fleet members only, no box, no partial legs).

Miss semantics are collective — no single host is the source of truth (SPOF softened Oct 2026; cloud retires into the fleet): the first affirmative answer wins outright; a consulted miss (HTTP 200, index asked, no data) is a vote, and not-found settles only when every miss-authoritative leg has consulted and missed — a pending leg can always rescue a landed miss; errors surface only when every leg errored (503 + Retry-After). The price: a true miss 404s as fast as the slowest leg instead of the fastest. farcaster.tv (partial) never votes.

The fleet races every shim read (worker/shim.ts, raceShimProbe): the profile family (/f, /user, /signed, timeline), the quote hydrator's /h→/u→/wc chain, both aliases, the signed check on cast pages, and the MCP tool each race cloud against every configured potato. Same collective contract. Without the var every wrapper is a pass-through (exactly one fetch, byte-identical behavior). The /refresh index warm covers every host in the fleet.

Sufficiency matrix (measured Oct 2026) against the internal ShimCast contract (text, hash, unix-seconds ts, raw {url}/{castId} embeds, inline user data):

ProviderResolves (username, hash)?Quotes ({castId})Verdict
shim✅ short + full✅complete — canonical, wins anytime
haatz (haatz.quilibrium.com/v1)⚠️ full hash only + fid via shim /f/{username}✅embed-complete, text-blind — full-hash requests
snap (snap.farcaster.xyz:3381, official gateway; 443 hangs)⚠️ same as haatz✅embed-complete, text-blind — full-hash requests
farcaster.tv✅ short (prefix-match)❌ silently droppedpartial — quote-blind (text canonical)

HTML path (700ms completeness box): complete providers win anytime; a quote-blind farcaster.tv win is held until the box expires, then served as a partial page — the SWR tradeoff the OG meta is immune to: title/description/image never depend on quotes, so chat-bots' cached embed cards are always complete. The partiality only reaches live browsers, which self-heal: partial pages ship QUOTE_FILL_SCRIPT, which fetches ?embeds=quotes (fleet-raced shim resolution + quote-hydrated SSR fragment, no-store) and injects it — same silent-degradation contract as the live-stats script. Text-blind wins (mention-bearing raw-node casts — the gateway strips the mention placeholder characters shim substitutes) are held harder: they never ship while a canonical-text leg (shim + the fleet) is pending, box or not, and fail open only once those legs settled miss/error — marked partial + textCanonical:false, never edge-cached (a body can't self-heal) and QUOTE_FILL withheld (their quotes are already inline). Mention-free raw casts are byte-identical to canonical text and win outright. Shell caching: every HTML request on cast pages may be served a cached found, text-canonical shell (worker/edge-cache.ts, 12h s-maxage, salted query-free keys — SHELL_SALT v2 bumped to purge shells cached from mention-stripped raw wins, hits rewritten no-store) — personalization is client-side (SHELL_SCRIPT), so cookie-holders get the same bytes; link-only pages, 404s, 503s, text-blind founds and JSON variants never cache (quote-blind partials do — OG complete, QUOTE_FILL heals per view).

JSON path: the complete-leg fleet roster (jsonRoster — shim + potatos, boxMs: null) — always complete, strict statuses (404 / 503 + Retry-After), the never-drop-quotes invariant holds unconditionally.

Miss semantics: misses are votes — not-found settles only when every miss-authoritative complete leg has consulted and missed (a pending leg can always rescue a landed miss); farcaster.tv 404 alone never produces a 404 (cast might exist). Every-leg-errored ⇒ link-only page (HTML) / 503 (JSON), fail-open as usual.

Ban signal: not part of the cast provider race — it's orthogonal to cast content — but /signed/{fid} is a fleet-raced shim read like every other (first answer wins, unknown fails open), time-boxed to the HTML box.

Telemetry: every resolve writes one Analytics Engine datapoint (RACE_STATS binding, non-blocking via waitUntil): index = winning provider (or outcome kind), doubles = total ms / box / partial flag, blob = per-provider id:kind:ms rows measured from each leg's actual (staggered) start — race-abort losers record abort, not error, and legs whose stagger never fired record nothing. Raw-node legs that settle their identity race also write one FID_RACE datapoint each (index = winning source shim/node, blob = leg + host, double = ms). Query via the AE SQL API when tuning the box (scripts/probe/, scripts/race-stats.ts); tests and callers without an executionCtx skip it.

Follow-up: quote hydration could race haatz/snap (/h/{fullHash} vs castById) for extra completeness — the shim-internal /u/{fid} → /wc fallback (Oct 2026) already rescues casts the hash-only /h index misses.

Observed full Hypersnap nodes (documentation only — not wired into the race roster)

Two additional public Hypersnap nodes were observed live via their self-reported /v1/info endpoint (the same public info endpoint haatz exposes). They are full Hypersnap nodes (Rust Farcaster node, HTTP on :3381) operated by independent parties — not part of cast-source.ts's race roster (shim / farcaster.tv / haatz / snap), and no code references them. Listed here for operator visibility only.

NodeVersionMessagesFIDsApprox sizeShardsPeer ID
https://hypersnap.rainonmeproductions.com0.15.0914,368,0203,356,878~812.9 GB3 (0,1,2)12D3KooWMzgU6PgzsJ4…
https://hypersnap.castrater.xyz0.16.0914,368,0253,356,878~797.0 GB3 (0,1,2)12D3KooWRjMdSgmsuh1n…

For comparison, the wired nodes report: haatz 0.16.0 / 914,368,020 msgs / ~797.6 GB / peer 12D3KooWMYfkXiNcn9L…; snap 0.16.0 / 914,368,024 msgs / ~551.3 GB / peer 12D3KooWH527dqZTziqz…. Message and FID counts match across all nodes (same network); approxSize differs per-node (local RocksDB storage). Both answer GET /v1/info with dbStats (numMessages, numFidRegistrations, approxSize), numShards, shardInfos (shardId, maxHeight, numMessages, blockDelay), version, and peer_id. No race integration, no cache, no telemetry wiring — purely informational.

Race stats (/transparency)

The racing-stats table is filled offline — the render path stays cache-free and stats-free. bun run stats (env per .env.example) queries Analytics Engine through the SQL API — sample-weighted (sum(_sample_interval), quantileExactWeighted; never raw count()) — assembles a versioned snapshot (v: 1; wins/requests/p50/p95/errorRate per provider + outcome counts) and publishes it as one lmdis key (race-stats on lmdis.artlu.xyz, the owner-run Redis-shaped KV) through the vendored Upstash-shaped SDK. Overwrite-only-on-success; --dry previews.

The transparency route reads the snapshot through a per-isolate TTL cache (worker/race-stats-store.ts: 10 min TTL, 60s failure backoff, last-good served ≤24h, 2s read cap) and fills the table, the "stats as of" line, and the JSON twin's racingStats — absent/unreachable snapshot ⇒ "coming soon" dashes, silently. /race-stats.json serves the raw snapshot (404 when absent). Both responses are edge-cached (Cache API: the transparency page s-maxage=60, the raw snapshot s-maxage=300) — the reads and the render run at most once per colo per TTL window; cached hits trigger zero lmdis reads, and the three store reads fire as one parallel fan-out (concurrent GETs don't queue server-side). bun run stats:check reads the live snapshots directly and pushes to ntfy.sh when thresholds breach (provider p95/errorRate, snapshot staleness as the pipeline's dead-man's-switch, lmdis unreachable; --dry previews; env in .env.example). Alerting layout: Uptime Kuma (owner-hosted, ntfy) owns uptime probes + a stats-pipeline push monitor; stats:check owns data-quality thresholds; the self-hosted VPS runs a box-local potato-watch cron (disk/load/memory → ntfy). Synthetic monitors never target cast URLs — each probe would trigger a full provider race and pollute the published stats. SQL dialect + store behavior are live-verified by scripts/probe/ (the AE dialect has no CONCAT and only toUInt32/toUInt8 conversions; SQL errors come back as plain-text 422s; the SQL API rejects Account Owned Tokens — use a User token with Account Analytics:Read).

Fid races (/transparency, parallel to race stats)

The raw-node legs race the shim fleet's /f/{username} against the node's own /v1/userNameProofByName for the author fid (see "Cast source race"). Each settled race writes one FID_RACE Analytics Engine datapoint (index = the WINNING source id — the leg's own id haatz/snap, or the shim-side member that named the fid: shim for the cloud reader, shim-potato/shim-potato-2/… for fleet members by var position; blob = leg id + host, double = the winner's ms), and bun run stats:fid (same env discipline as stats) aggregates it into a second lmdis key (fid-race, overwrite-only-on-success, --dry previews). The snapshot is v:2 with an OPEN source vocabulary — rows are grouped by the dataset's own ids at aggregation time, so fleet members join the table as they appear (a v:1 snapshot is ignored; re-run the job to publish v:2). Every host is tracked independently — a member's win is attributed to that member, never merged into a bucket. /transparency reads it through worker/fid-race-store.ts — a line-for-line mirror of the race store (same TTL/backoff/stale contract) — and renders the Author id races item (wins/p50/p95 per observed source, raw ids as row labels + raced-lookups line) plus the JSON twin's fidRace block (status: live | coming-soon). Declared as a third entry in tracking.counters (fidRace); no cast IDs, no identifiers.

Abort telemetry: race-cut losers are recorded as provider:abort:<ms> (not error) so public errorRate stays honest — the 700ms box cuts slow completes by design; the 5s hard-timeout still counts as an error. raceCast drains pending settles after abort (bounded 25ms) so timing rows never escape incomplete.

Client rank (click-through stats, /transparency)

Documentation truncated — see the full README on GitHub.

Reviews

No reviews yet

Be the first to review this server!