Chapter 32. The Verification Law

Types, build, and lint all went green and production still broke on click, because none of those checks renders a page and interacts with it. On 2026-07-17 a wallet-connect lazy-load refactor landed in Suede-AI-App as PR #660 and shipped to production under my standing merge and deploy autonomy, and six marketing pages served a Connect Wallet button that threw a client-side exception on click.

No automated check caught it. I caught it about ten minutes later by opening an isolated browser tab and clicking the button with my own hand. PR #661 reverted the refactor.

The mechanism is worth stating precisely, because the shape of it repeats. The component was loaded through next/dynamic(..., { ssr: false }) and lost access to a context provider it needed. Next.js compiles that tree without complaint. The compiler asks whether the module graph resolves and whether the types agree, and both answers were yes. Whether a provider is mounted above a component at the instant a click handler fires is a property of a rendered page under interaction, and no step in the pipeline rendered that page and interacted with it.

Ten minutes of production exposure on six pages is a cheap tuition payment. The reason it stayed at ten minutes is that clicking the button was already on my post-merge list. The reason the bug reached production at all is that clicking it was not on the pre-merge list.

What a green check is evidence of

Each check that passed on PR #660 answered its own question and none of them answered mine, which was whether a person could connect a wallet on ip.suedeai.ai and the other five pages after the deploy. Every one of them was honest: tsc --noEmit told the truth about types, and the build told the truth about compilation.

The law I run on now:

Evidence transfers only across an identical artifact, an identical environment, and an identical operation. Change any one of the three and the evidence stops applying.

PR #660 changed all three at once. The artifact I checked was a local compilation of a working tree; the artifact that broke was a Vercel production build. The environment I checked was a Node process on this Mac; the environment that broke was a browser holding a React tree. The operation I checked was compilation; the operation that broke was a click.

The law is not a demand for more checks. It is a rule for reading the checks you already ran. Before I trust a green result as a merge gate, I name what artifact it exercised, where, and under what operation, and then I ask whether the thing I am about to claim matches all three. When it does not, the gap is the test I still owe.

Four corollaries carry most of the weight in practice.

Corollary one: verify the committed artifact, not the working tree

On 2026-07-18, a rebase in Suede-AI-App produced a duplicate articlePath function declaration in magazine.ts. Git’s three-way merge did not see the two versions as overlapping, so there were no conflict markers and no prompt. tsc --noEmit caught the duplicate. I fixed it with an Edit-tool change, re-ran tsc, the build, and the test suite, and all three came back green.

Then I ran git push --force-with-lease and merged the PR without ever running git add and git commit on the fix.

Every production build after that merge failed with:

Module parse failed: Identifier 'articlePath' has already been declared

The green checks were reading a file on disk. The push was sending committed history. Those are two different artifacts that happen to share a path, and the local toolchain gives you no signal at all when they diverge. tsc --noEmit, npm run build, and npm run test:run all read the working tree, uncommitted edits included, and they will go green on a fix that is about to be left behind.

Hotfix PR #673 landed the same day. The live site was never down, because Vercel does not promote a failed build, and that piece of luck is exactly what makes the failure mode dangerous: a broken deploy pipeline with a healthy-looking site produces no user-visible symptom to alert on. What found it was checking Vercel’s deploy status after the merge rather than stopping at local green.

The operational form is small. Commit the fix before running the verification pass you intend to trust as a gate:

git add src/lib/magazine.ts
git commit --amend --no-edit   # or a new commit
npm run build && npx tsc --noEmit

Then, after the merge lands, spot-check the deploy itself with vercel ls or gh pr checks. Local checks and the deployed build are separate artifacts assembled from separate inputs, and in a monorepo frontend with an external build pipeline the second one is the one your visitors get.

There is a harsher version of this corollary on a machine running concurrent agents. On 2026-06-10 I was editing ~/code/Suede-AI-App directly when another live session switched the shared main checkout from main to its own feature branch, feat/master-registry-consolidation. My uncommitted edits went with it. Two edits in, everything was gone, with no warning and nothing to recover from. A working tree that another process can rewrite underneath you is not a workspace and not evidence, which is why every task on this estate now gets its own worktree cut from origin/main.

The overlap problem also changes how you read history. The settlement_live hotfix was written from an isolated worktree while a concurrent session was counter-editing the same defaults in the main checkout. Under those conditions a commit message describes what an author intended at the time they typed it, and the contents describe what is now in the branch. When sessions overlap, read the diff.

Corollary two: a compiler cannot prove that a button works

A compiler proves the code can be turned into a program. It says nothing about what the program does when someone touches it. That distinction is invisible in most work and decisive in a narrow class of components: buttons, modals, and forms that do real work in an onClick handler. Wallet connect, auth, checkout.

For those, the check is a click. Not a snapshot test that renders the component in isolation with its providers helpfully supplied, because the failure in PR #660 lived in the provider relationship that an isolated render restores for free. The click has to happen against a deployed build or a local dev build of the real page, in a real browser, with the real tree above the component.

Two details make the click trustworthy on this machine. The first is that I run it against the actual host, not a component playground. The second concerns the browser tooling itself, and it cost me a near-miss on the same day as PR #660. Concurrent agent sessions on this Mac share one browser-automation tab pool. Tabs get reused and overwritten by whichever session grabs them, and the tool reported content from a different origin than the one I had navigated to a moment before. Read at face value, that stale content would have been a false regression call on a page that was fine. A dedicated tab (tabs_create) and a re-check gave the real answer.

Verification tooling on a shared machine is shared mutable state. An inconsistent or suspicious browser result is a signal about the tool, and the response is to isolate and re-check before it becomes a finding.

Corollary three: verify a public fix against the live URL

Production config does not all live in your repository. On 2026-07-17, while verifying PR #658, a live check against https://social.suedeai.ai/ surfaced this in the console:

Origin https://social.suedeai.ai not found on Allowlist - update configuration on cloud.reown.com

Two network 403s from the WalletConnect relay came with it, same root cause. SignInButton lazily mounts SignInModal, which calls wallet hooks without condition for signed-out visitors on feed, forum, notifications, and settings, so social needs the wallet stack. Nobody had ever registered social.suedeai.ai in Reown’s allowed-origins list. It had been broken in production with no local symptom of any kind.

Nothing in the repository, the build, or the test suite can catch that, because the missing thing is a row in a third-party dashboard with no representation in code. The fix was a dashboard edit adding https://social.suedeai.ai and https://suede.social to the allowed origins of the Reown project referenced by NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID. It needed my own hands, because it is an account-settings change behind credentials I do not hand to an agent. I made it on 2026-07-17 and confirmed the console came back clean. Chapter 27 tells that one in full.

The generalization I took from it: when a config is per-origin and one app tree serves multiple custom domains, enumerate every domain from the source of truth for routing and register them together. next.config.ts confirmed the same tree serves suede.social and play.suedeai.ai. A per-origin config verified on one origin is verified on one origin.

The same shape shows up in rendered output. PR #751 fixed two missing-space typos on a live marketing page where JSX whitespace collapsing around a {expression} followed by text on the next line produced $7.99/mowhen, with zero space between the price and the next word. The source looked correct. Reformatting a JSX line changes the rendered string, so the string you check has to be the one the browser produced, not the one in the editor. Same lesson at the character level: read the output, at the URL, in the form a visitor receives it.

Done right, this corollary produces a proof you can paste. The shared root layout at Suede-AI-App/frontend/src/app/layout.tsx wraps all eight hosts (app, social, studio, play, agents, ip, fretpulse, distro) and called headers() without condition to answer one question: is this app.suedeai.ai? True for one host, false for the other seven, and that single call forced dynamic rendering across the whole tree. studio.suedeai.ai served Cache-Control: private, no-cache, no-store on every request, with no CDN caching at all. PR #653, merged 2026-07-17, moved the app-host-exclusive routes into an (app-host) route group with their own layout and removed headers() from the shared root. What closed the finding was a header read from the live surfaces afterward: studio, social, play, and distro confirmed serving Cache-Control: public, max-age=0, must-revalidate. A prior middleware-level Cache-Control override, PR #612, is on record as not working on Vercel, so a repo diff would have looked equally convincing in both cases. The response header decided it.

Corollary four: a 200 response does not name what served it

This is the corollary that took the longest to learn, and the one with the ugliest receipt.

On 2026-06-11 I recorded that Agent Studio’s production migrations were applied and verified live: the gateway LLM endpoint returned 200 with metering, and topup returned 402 challenges with a real payTo. Five weeks later, on 2026-07-16, the same record states that Agent Studio’s Supabase runtime had never been implemented, despite Phase 9 assuming DB_DRIVER=supabase in production.

The June verification was insufficient and does not support a migration claim. A 200 response and a 402 challenge can both come from the SQLite fallback, so those checks proved endpoint behavior without proving which repository served it. For this book, the 2026-07-16 production audit supersedes the June status note: the durable Supabase path was not operating at the start of that audit, and the later end-to-end canary is the evidence that closed the repair. The Phase 8 note’s conflicting pending label is historical bookkeeping, not a current deployment fact.

What the 2026-07-16 outage established is not in dispute. Vercel production env vars (DB_DRIVER, SUPABASE_*) were pulling as empty strings, and the code took a fallback path to a non-durable SQLite repository inside a serverless function with no error raised. An env var that reads as an empty string is worse than one that is missing, because a missing var can throw and an empty one selects a branch. The same-day audit found live /api/v2/* endpoints returning 503, a disabled Run button, and a false save/recovery conflict that persisted a draft on page view. PRs #71 and #72 restored proper Supabase config, hardened RLS, removed the service-role key from Vercel, and added atomic RPCs; an end-to-end canary covering create, bind, version, and run passed. PR #73 followed. Agent Studio’s tables sit in the same shared Suede Supabase project as the rest of the estate, a deliberate and temporary choice I made rather than an accident, and it has not been split out.

The durability claim needs a durability test: write through the API, redeploy, read back. An HTTP 200 proves an endpoint answered. It does not name the database that served it.

The money path taught the same lesson from the other direction. Phase 9 shipped the x402 settlement toggle opt-in with a default of false, and production had no settlement_live column, so every agent mapped to dry-run, and dry-run skips the 402 payment challenge. Every priced agent on Suede Agent Studio was free to call while settlement was live. An orchestrator spot-curl of the money path caught it. Hotfix 12d841b inverted the default to opt-out: a missing or NULL settlement_live now means live, only an owner toggle-off disables it, and the env var X402_SKIP_SETTLEMENT is the sole kill-switch. Money-path defaults fail closed. Every deploy that touches the money path gets a curl against the money path.

A cousin of this: process liveness is not function. Render was projecting about $16.83 for July across two Starter services on 2026-07-16, and producer-suedelabs-worker had zero successful job logs, with nothing but DB-connection failures since its 2026-07-14 deploy. It was up. It had never completed a job. Monitoring watched the process, and the thing worth watching was the success log. Suspending it removed the cost and created a new risk in the same move: Suede-AI-App, the live x402 API service, went to Render’s Free tier, which can cold-spin-down after 15 minutes idle with less CPU and traffic headroom than Starter. That risk is recorded as an open watch item next to the saving, unresolved, because a cost cut is a change to production topology.

When verification has to run before the action

Some actions do not grant a second look. The four corollaries above all assume you get one. In May 2026 a PR comment on the external repo solana-foundation/awesome-solana-ai#155 went out under the Suede-AI bot account instead of my founder account. gh had the secondary account active in the keyring and nothing checked which identity was live before the mutating command ran. The comment was already public and already attributed by the time anyone could audit it.

For a solo founder, the byline on external work is a distribution asset. Investors and collaborators read GitHub activity and package metadata, and a bot account earns nothing back. That makes identity a precondition rather than a review item: gh auth status gets checked before gh pr create, gh pr comment, gh issue create, gh issue comment, gh pr review, gh pr merge, and gh release create. Read-only commands can run under whichever account is active.

Where an action publishes, sends, charges, or writes under a name, the verification moves in front of it. Everything else can be caught on the way out.

The pre-merge and post-merge checklist

This is the version short enough that I run it instead of admiring it. It fits between the last edit and the merge.

Before the merge:

  1. git status and git diff --stat. Every fix you intend to gate on must be committed, not sitting in the working tree.
  2. Re-run the gate after committing: build, tsc --noEmit, tests. These read the tree, so the tree must match the commit.
  3. If the diff touches a client-interactive component, click it. Real browser, real page, dedicated tab, deployed or local dev build.
  4. If the diff touches a money path, curl the money path and confirm the 402 challenge.
  5. Name what a green check exercised. Artifact, environment, operation. If your claim covers more than the check did, the gap is a test you owe.

After the merge:

  1. vercel ls or gh pr checks. Confirm the deploy that visitors get, not the one you built.
  2. Curl or load the live URL for the surface you changed, and read the rendered output and the console, not the source.
  3. For anything touching a datastore, write through the API and read back after the deploy.
  4. Record what you verified and what you did not. An unverified item written down is an open item; an unverified item left out becomes a claim.

The fifth line is the one doing the work. The rest are instances of it that recur often enough on this estate to deserve their own numbers.