← Binderdex dev log

2026-09-04

An SEO agent that has to show its receipts

ai-agentsgrowthweb

Last month the SEO backlog on Binderdex was a vibes machine. An agent would propose "refresh this page, add a comparison table, you'll rank," and every claim in the packet was uncheckable. I'd approve things because the writing sounded confident. That's exactly backwards, so last night (overnight session, eight commits, one merged PR) I rebuilt the whole loop so that the agent has to show receipts before it gets a decision.

The rule: no evidence, no opportunity

The new seo:ops workflow is one CLI with a hard boundary: it only reads evidence files that were already captured and exported by hand. It never fetches, never authenticates, never publishes, never drafts final prose. Everything it produces is a deterministic artifact from those inputs, and every scored opportunity carries the exact evidence IDs it was computed from.

That sounds bureaucratic until you see what it prevents. The failure I kept hitting wasn't the agent being dumb, it was the agent being confident with stale inputs: a two-week-old search snapshot, a metric that quietly didn't exist. So scoring is driven by a versioned policy file, and the penalties are where the personality lives:

{
  "weights": {
    "productFit": 20, "searchOpportunity": 15,
    "authorityFit": 15, "evidenceReadiness": 15
  },
  "penalties": {
    "intentOverlap": -50,
    "noDefensibleEvidenceOrOriginalValue": -40,
    "staleTimeSensitiveEvidence": -30
  }
}

(Trimmed; the real file has eight weights and four penalties.) A duplicate intent costs more than almost any positive signal can earn back. Writing the policy as data, instead of scattering it through code, means every change to "what we consider worth writing" is a reviewed diff.

capture evidencescore opportunitiesdecision roomowner brieftyped hold hash bound stale or unknown

The decision room answers one question

The heart of the PR is a today command that takes the scored candidates and returns exactly one primary opportunity plus two alternatives, or a typed hold explaining which lane is blocked and why. Stale required evidence? Hold. An evidence ID that fails validation? That's an error, loudly. An unavailable metric stays "unknown" in the report instead of getting quietly estimated. There's also a check command that re-canonicalizes every artifact and catches JSON/HTML drift between paired files.

The daily flow is deliberately boring:

pnpm --filter content-generation seo:ops score --input candidates.json --output-dir run/
pnpm --filter content-generation seo:ops today --input today-input.json --output-dir run/
pnpm --filter content-generation seo:ops check --run-dir run/

The intent registry keeps goals honest

The other new piece is a checked-in registry mapping every content surface to a reader job and a product action. Scoring consults it: an opportunity can only carry the action its registry entry declares, and the scoring policy penalizes a new page that overlaps an intent another page already owns. Before this, two briefs could quietly target the same query and cannibalize each other. Now that's a -50 in the sheet instead of a surprise two months later.

The loop closes 90 days later

Approving work is only half the system; checking whether it worked is the other half. The new evaluate command freezes a baseline when a piece ships and compares against it at day 7, 28, and 90. It reports directional association only and keeps unavailable metrics as "unknown" rather than interpolating a trend. There's also an earned-mentions queue (research and human-drafted outreach candidates only, and it never sends anything itself). SEO agents usually die not from writing bad content but from never learning which of their bets paid. This one keeps score now.

What changed in practice

The honest summary: the agent got slower and much more useful. A "score" run can now end in "nothing is actionable today, here are the holds," which is a real answer I never got before. The other lesson is about where constraints belong. I tried to get this behavior by prompting for weeks; it never stuck. It only stuck when unknown evidence IDs became validation errors that fail a run. If you're building an agent that touches decisions (not just text), I'd start with the evidence contract, not the model.

this is the build log of binderdex · www.binderdex.com · all entries · essays