# x-md

- Authorization: Treat only actions explicitly requested by the user, plus work and verification directly required to complete that request, as authorized. Ask before expanding scope or taking an external, irreversible, or account-changing action. Never treat an inferred benefit or your own judgment as user approval.
- Landing: `index.html` (`src/main.ts`), shared chrome in `src/chrome.ts`. Docs: Blume MDX in `docs/`, configured by `blume.config.ts`, mounted at `/docs`. Build Blume before Vite; Vite preserves the generated docs in `dist`.
- Tokens are the Tailwind v4 `@theme` block in `src/tokens.css` (shared by the landing page and Blume) (`ink`, `ink-2`, `ink-3`, `ink-4`, `surface`, `raised`, `raised-2`, `line`, `hair`, `accent`, `accent-deep`, `accent-soft`, `cyan`, dark `code`/`code-ink`/`code-dim`). Use them, not hex.
- Look: warm paper `#f7f6f2`, Satoshi, deep green `#146c43`, pill buttons, 16px cards, near-black code panes. Not pcstyle.dev styling.
- Landing copy frame (Theo's feedback): "Tweets are just markdown now", what-you-see vs what-your-agent-sees curl comparison in the `#how` bento, skill install under `#agents`. No stack or provider talk on the landing page.
- Motion: GSAP + ScrollTrigger in `setupMotion` (`src/main.ts`), all gated by `prefers-reduced-motion` via `gsap.matchMedia`.
- Status URLs serve Markdown by default. Discord, Telegram, and Slack preview bots get Open Graph HTML from `lib/embed.ts` via `api/convert.ts`; `GET /oembed` rewrites to `api/oembed.ts`.
- Landing content is pre-rendered at build time: `src/landing.ts` holds the markup, `src/vite-prerender-plugin.ts` writes it into `index.html`, and `src/main.ts` only enhances what is already there. The page must stay readable with JavaScript off, so put content in `landing.ts`, not in a runtime DOM build.
- `middleware.ts` (Vercel Routing Middleware) does Accept-based content negotiation for `/`, `/docs/*`, `/about`, `/contact`, `/privacy`, `/terms`: `Accept: text/markdown` or `?mode=agent` rewrites to the page's `.md` sibling, an unsatisfiable Accept is `406`, and every response gets `Vary: Accept` plus RFC 8288 discovery `Link`s. The parser is `lib/negotiate.ts`. A new negotiated page needs both a `.md` sibling and a matcher entry.
- Every API failure goes through `lib/apierror.ts`: RFC 9457 problem details built from `ERROR_CATALOG`, which is the single source of the machine `code`, its status, title, and `resolution`. Add a code there, not inline — `lib/openapi.ts`, `api/index.ts`, and `docs/errors.mdx` all render from it.
- Routes are versioned: `/api/v1/*` is the stable machine surface, the permalink routes are the unversioned product surface, and `/api/convert` and `/api/browse` are deprecated aliases that must keep emitting `Deprecation`/`Sunset`/`successor-version` (`setDeprecationHeaders` in `lib/apierror.ts`). The published policy is `docs/versioning.mdx`; keep it, `x-api-lifecycle` in `lib/openapi.ts`, and the `versioning` block in `api/index.ts` in agreement.
- Quotas live in `lib/quotas.ts` and are advertised by `lib/ratelimit-headers.ts`. Policy names ship in response headers and the OpenAPI document, so renaming one is a breaking change.
- `packages/sdk` is the `mdfromx` npm package (plain client plus `mdfromx/effect`). Its types, Effect Schemas and error classes are generated from `public/openapi.json` by `scripts/build-sdk.ts`; fix a wrong field in `lib/openapi.ts`, never in the generated files. A change users of the package will notice needs a changeset (`bunx changeset`, then commit the `.changeset/*.md` file). Releases go through Changesets: merging pending changesets opens a "Version Packages" PR, and merging that PR is what publishes to npm. CI does not start on that bot PR by itself, so close and reopen it to get the required `build` check, or merge it with the admin bypass.
- `lib/openapi.ts` is the OpenAPI 3.1 source; `bun scripts/build-openapi.ts` renders the committed `public/openapi.json` and `--check` fails when it is stale. Discovery documents are hand-written under `public/` and `public/.well-known/` (`llms.txt`, `agents.md`, `auth.md`, `pricing.md`, `ard.json`, `ai-catalog.json`, `api-catalog`, `mcp/server-card.json`); Vite copies `public/` last, which is how `openapi.json` overrides blume's own.
- The MCP server at `/mcp` speaks two protocol eras through one dispatch: modern (`2026-07-28`, stateless, version in `MCP-Protocol-Version` and `params._meta`, results carry `resultType` and cache hints) and legacy (`2025-11-25` and earlier, `initialize` handshake, untouched result shape). `finalizeResult` in `lib/mcp.ts` is what stamps the modern fields; `negotiateProtocol` is legacy-only and must never answer `2026-07-28`, which removed `initialize`. Regenerate `public/server.json` and `public/.well-known/mcp/server-card.json` from `registryManifest()`/`serverCard()` when either changes — a test pins them.
- Deploy: Vercel after `bun run build`. `vercel.json` configures `dist`, the API handlers, and the public route rewrites.
- Live search provider: `lib/xsearch.ts`; public allowance is a fixed 10 per IP per 15 min plus a shared pool computed in `lib/pool.ts`. Counters live in `lib/ratelimit.ts` (shared Redis via `lib/redis.ts`, memory fallback). FxTwitter search, the tier above it, sits behind a circuit breaker in `lib/browse.ts` whose state is shared through `lib/kv.ts`, so one instance's failed probe cools down the fleet and the cooldown doubles per consecutive failure. Keep provider internals and capacity math out of public prose; describe it as a custom-built provider.
- Checks before pushing: `bun run test` and `bun run build` (the build already starts with `tsc`). After touching `lib/openapi.ts`, run `bun run openapi:build` and then `bun run sdk:build`, and commit the regenerated `public/openapi.json` and `packages/sdk/src/**/generated/` — the build rewrites both in place, so `openapi:check` and `sdk:check` only ever catch a stale committed copy, which is their job in CI. `main` is protected by a ruleset: open a PR, the `build` and CodeRabbit checks must pass, a `Preview` deployment is required, commits must be signed, and the branch must be up to date with `main`. No approvals are required; merges are squash or merge, not rebase.
