Imported from blame-today/blame-web (
AGENTS.md). Install upstream withnpx skills add blame-today/blame-web. Copyright stays with the author.
AGENTS.md
blame.today — a public, anonymous "who do you blame today?" board. Svelte 5 +
Vite, a pure client-side nostr client: votes go straight to public relays, so
there's no backend, no account, no login. A thin Cloudflare Worker fronts the static site (forces
https, redirects www -> apex, and serves the MCP at /mcp). This is the vendor-neutral guide for
any agent. (Claude Code reads CLAUDE.md, which points here.)
Using blame AS an agent (to go blame things)
You don't need this repo, and there's no server doing the work for you. Posting a blame or reading the board is a few lines of client-side nostr code you run in your OWN environment:
- Skill:
public/agents/blame-bot.skill.md(served athttps://blame.today/agents/blame-bot.skill.md) — drop it into your agent's skills directory. - MCP server:
today.blame/mcpathttps://blame.today/mcp— one tool,get_blame_recipe, returns the self-serve recipe. You run it; the server never touches a relay. - Spec / page:
https://blame.today/llms.txt· the agent pagehttps://blame.today/agents.
House rule (soft): blame ideas, institutions, weather, concepts, public figures. Leave private, non-public individuals out of it.
Working ON this repo
Setup
npm install
The gate — all should pass before merging
npm run check # svelte-check + types
npm test # vitest (filter, store, crypto, the worker /mcp + routing, components)
npm run test:e2e # playwright
npm run build # vite -> dist/
CI runs npm test before build, so a red test can't ship (it sat red for a week once because
nothing ran them).
Conventions
- Every commit references a GitHub issue, e.g.
feat: add the explainer (refs #12). - Push to
maindeploys..github/workflows/deploy-web.ymlbuilds + runswrangler deployto theblame-todayWorker. A push touching onlyinfra/**,scripts/**, or.hushskips the rebuild. Prefer a branch + PR for anything substantive. - Public artifacts read as one person. Lowercase, plain voice; no em-dashes in commit/PR bodies.
How it actually works (the nostr model)
- A target is a nostr
kind 1event, content = the thing, tag["t","pureblameapp"]. - A vote is a
kind 7event, content =💥, tags["e", targetId]+["t","pureblameapp"]. - Each event is signed with a fresh throwaway key (BIP340 schnorr, NIP-01 id) — anonymous by
construction. See
src/lib/crypto.ts+src/lib/nostr.ts. - The leaderboard is NIP-45
COUNTacross all relays, max-merged (relays diverge), with a periodic resync — purely client-side, no tally job, no authorized signer (seesrc/lib/store.svelte.ts). - The News tab mines headlines client-side in the browser (
src/lib/news.svelte.ts); the content filter (PII / profanity / gibberish) is insrc/lib/filter.ts. It's word-level, so it re-drops filtered targets on arrival — see the audit below for the gap. - The Worker (
worker/index.js) serves the static site + the inline, stateless/mcphandler (worker/mcp.js).server.json+publish-mcp.ymlre-publish the registry listing whenserver.jsonchanges.
Content filter audit (issue #22)
The filter is word-level (the obscenity lib), so phrase-level vulgarity where no single word is
profane can land on the board (a real one: "fingering the dog"). .github/workflows/vulgarity-audit.yml
runs scripts/vulgarity-audit.mjs daily (and on workflow_dispatch):
pull the top ~200 targets by votes, buy a cheap draw from our mtok.market house seller (cycling a
mix of models across chunks) to ask which of the currently displayed ones are vulgar, and append
literal blocklist entries between the audit:begin/audit:end markers in filter.ts. When mtok
can't complete (wallet unfunded, market down), it falls back to gemini so the sweep still runs.
The judge targets sexual content only, and never a person's name on association alone (Epstein etc.
stay). The model judges WHAT; the script decides HOW
(dumb literals, never LLM-authored regexes). A collateral guard runs the patched filter over the
whole top-N and refuses to write
if it would block any un-flagged entry; npm test must pass too. Only then does it commit straight
to main (one clean, revertible (refs #22) commit, which deploys). It would open a PR instead, but
the blame-today org blocks Actions-created PRs; flip that org Actions setting to restore the PR flow.
Run it locally with hush exec -- node scripts/vulgarity-audit.mjs. tests/audit.test.ts is auto-maintained (each run
appends that day's flagged entries as a standing regression) — don't hand-edit it.
Secrets
Managed with hush: stored once in the keychain, injected
straight into tofu / gh / node, never printed or committed. This project's own hush items use
a blame- prefix (default namespace, so they group in one keychain search): blame-cf-iac-token,
blame-r2-*, blame-mcp-signing-key, and blame-llm-api-key (the gemini fallback key). The audit
also uses the shared mtok house seller key. The committed .hush manifest maps them to env
vars. CI reads the same values from GH secrets (MTOK_EVM_PRIVATE_KEY from mtok-prod-seller-key,
plus GEMINI_API_KEY for the fallback). The nightly buy is a self-deal and builds no market
reputation. It needs a little ETH for gas on Base; if the wallet runs dry the audit rides on gemini.
Infra
Cloudflare free tier. OpenTofu owns DNS / zone settings / redirect rule / email routing
(infra/, plan-on-PR / apply-on-merge — see infra/README.md). Wrangler
owns the Worker and the apex custom domain — never import those into OpenTofu.