Skip to main content
All Projects
IN·PROGRESS

Evidenced

A QA primitive for agents: it verifies a user outcome in a real browser and returns a structured, evidence-backed verdict (passed, failed, or inconclusive) an agent can loop on instead of a raw action log

Evidenced screenshot 1

Role

Full Stack Developer

Team

Solo

Stack
Frontend
Next.js 16React 19TanStack QueryTailwind CSSnext-themes
Backend
Fastify 5Node.js 22Better AuthZod
Engine
PuppeteerOpenRouterMCP (stdio)
Data & Queue
PostgreSQLDrizzleRedisBullMQ
Infra
TurborepopnpmDockerS3 / R2
Billing
Dodo Payments
Challenges
  • Distinguishing failed (the app is wrong) from inconclusive (the environment prevented a verdict) so an agent loops on real defects, not on DNS failures or timeouts
  • Five deterministic guards that rule before any LLM judgment, with a guard failure short-circuiting the model and the LLM never allowed to override a guard
  • One hosted execution path shared by the MCP server and the dashboard, so both produce the same verdict from the same job, queue, worker, and code
  • Envelope-encrypted per-org secrets decrypted only at enqueue, carried through Redis on the job, scrubbed from errors, and never logged or written into evidence
  • URL allowlist checked at enqueue and re-checked in the worker before navigation, so an agent cannot widen its own target
  • Open-core split where the commercial ee/ tier is reached only through a guarded dynamic import, keeping the OSS verdict loop functionally identical and unmetered
Insights
  • Returning evidence (screenshots, DOM snapshots, console, network) by reference keeps verdicts compact so agents get a decision, not megabytes of artifacts
  • A pure plan to execute to judge to assemble engine with no transport, storage, or queue imports, everything external supplied as an adapter
  • Subpath exports keep concrete adapters (Puppeteer, OpenRouter, S3) out of the barrel so a types-only consumer never drags in an SDK
  • Tenant isolation by construction: every row carries org_id, all access goes through forOrg(orgId), and orgId is resolved server-side, never from the request body
  • Verdict schema validated on write, on read, and on receipt, with an additive-only schemaVersion
  • Runtime open-core switch (BILLING_ENABLED / REQUIRE_BILLING) rather than a compile-time fork, so the seams resolve to no-ops when billing is off

Overview

Evidenced is a QA primitive for coding agents. Agents can write code but cannot reliably tell whether the result actually works for a user. Evidenced closes that loop: it drives a real browser toward a goal and returns a structured, goal-relative verdict (passed, failed, or inconclusive) that an agent can act on directly, rather than a raw action log it has to interpret.

Two entry points, an MCP server a coding agent calls and a dashboard a human uses, drive one hosted, server-side execution path, so both produce the same verdict from the same code. Evidenced is open-core and self-hostable under Apache-2.0, with a paid hosted tier living in ee/.

3

Verdict states

5

Deterministic guards

4

Stage engine pipeline

2

Entry points, one path


How It Works

  1. 1

    Trigger a run

    An agent (evidenced goal:"...") or a human (dashboard "Run") hits the backend, the single front door. It authenticates the caller, enforces tenancy and the app's URL allowlist, gates on credits (hosted only), decrypts the run's secrets, and enqueues a validated job on BullMQ.

  2. 2

    Execute in isolation

    The worker pulls the job, re-checks the allowlist, launches an isolated browser, and runs the engine. The only difference between an MCP run and a dashboard run is the recorded source.

  3. 3

    Plan, execute, judge, assemble

    The engine turns the goal and starting URL into an ordered journey, drives each step in the browser while capturing evidence, runs five deterministic guards, then consults the LLM only if no guard fired.

  4. 4

    Return a verdict, loop

    The verdict and evidence references land in Postgres and the object store. The agent reads the verdict and loops (fix, re-run); the human reviews it in the dashboard as the lifecycle streams back.

The engine is a pure pipeline with no transport, storage, or queue concerns; everything external is supplied by the worker as an adapter:

packages/core
  planner/    goal + URL -> ordered journey (one model call)
  executor/   step execution, intent resolution, evidence capture
  judge/      guards.ts (the five guards) + verdict.ts (LLM judge)
  evidence/   evidence collector
  assemble.ts verdict assembly + schema validation
  adapters/   interfaces (types.ts) + concrete impls per subdirectory

Key Features

Outcome-level verdicts

A verdict is relative to a goal, never a transcript the caller must parse. failed means the app is wrong; inconclusive means the environment (unreachable target, timeout) prevented a verdict. An agent loops only on real defects.

Deterministic guards first

Five guards (HTTP status, URL assertion, element presence, console error, page load) run on every step before any LLM is consulted. A guard failure is conclusive and short-circuits the model, which can never override it.

Machine-readable failures

Every failed verdict carries a dossier: the failing step, the reason, a root-cause hypothesis, evidence references, and a suggested next action, so an agent gets a plan, not a mystery.

Context economy

Evidence (screenshots, DOM snapshots, console, network) is stored and returned by reference. Agents receive compact verdicts instead of megabytes of artifacts.

One execution path

MCP and dashboard runs share the same job, queue, worker, and verdict. No functional gap between hosted and self-hosted; only the commercial layer differs.

Secrets sealed by construction

Model keys and app credentials are sealed with a per-org data key wrapped by a boot-time KEK, decrypted only at enqueue, carried through Redis, and scrubbed from every error before it reaches a client.

Allowlist cannot be widened

The target URL is checked at enqueue in the backend and re-checked in the worker before navigation. An agent has no path to expand its own reach.

Pluggable adapters

Browser (Puppeteer), model (OpenRouter), resolution (DOM ladder), and storage (disk / S3 / R2) are all adapters behind interfaces. Swapping one is a config change in the composition root, not an engine change.


Architecture

A pnpm + Turborepo monorepo (Node 22, TypeScript, ESM throughout): five apps and four packages behind a single backend front door.

apps/backend     Fastify API: auth, tenancy, allowlist, secrets, enqueue, reads
apps/worker      BullMQ consumer: executes runs, persists verdicts + evidence
apps/mcp         Thin stdio MCP server an agent runs locally
apps/dashboard   Next.js app: runs, apps, keys, credentials, billing
apps/web         Public Next.js marketing + auth + SEO surface

packages/contracts  Every cross-boundary shape (verdict, tool I/O, jobs). Pure Zod.
packages/core       The engine: planner, executor, judge + guards, adapters
packages/db         Postgres schema (Drizzle), org-scoped DAL, sealed secrets
ee/                 Commercial hosted tier: billing, metering, gating
Open-core, runtime switch

The commercial ee/ package is an optional dependency reached only through a guarded dynamic import, gated by BILLING_ENABLED / REQUIRE_BILLING. The switch is runtime, not compile-time, so the OSS build and the hosted build run the identical verdict loop; with billing off the seams resolve to no-ops and runs are unlimited.

failed is not inconclusive

An unreachable target (DNS failure, connection refused, timeout) yields inconclusive, not failed: the environment prevented a verdict, the app was never proven wrong. Collapsing these two would make an agent chase phantom defects every time the network hiccuped.

Purity keeps SDKs off consumers

packages/core exports the engine and adapter interfaces only. Concrete adapters live behind subpath exports (@evidenced/core/adapters/model/openrouter, etc.), so importing the package for its types never drags Puppeteer or the model SDK into a consumer that does not touch them.


Outcome

Evidenced demonstrates a production open-core agent-QA system: a pure four-stage browser verification engine, five deterministic guards gating the LLM, a single execution path shared by an MCP server and a human dashboard, per-org envelope-encrypted secrets, and strict server-side tenant isolation. The result an agent receives is a compact, schema-validated verdict with an evidence-backed failure dossier it can loop on, not an action log it has to reverse-engineer.