Engineering brief · Empire Homes pilot

Avalon Competitive Intelligence.

A working proof of the agentic-workflow pattern proposed for the Empire AI lab. Three real competitor pages, eleven Cloudflare services, full audit trail, gated behind Application Security. Click through the tabs below to see how it works, what it costs, and what scaling it would require.

11
Cloudflare services in play
3
competitor pages monitored daily
$0.0032
avg per run (Workers AI)
$0.0170
avg per run (Claude via AI Gateway)

What it does

What it proves

Secure by default

Application Security gates the hostname. Email-allowlisted users only. No customer PII in scope. Source policy: public pages, robots respected.

Scalable by primitive

Workflows for durable orchestration. R2 with zero egress. Workers AI for inference. The same pattern runs at 3 pages or 300.

Observable end-to-end

Every model call routes through AI Gateway. Every run, signal, cost, and latency is persisted to D1. Workers Logs covers the rest.

Safe to test on day one

Sandbox-deployed under a separate Worker, D1, R2, and AI Gateway. Nothing wired to Empire's internal systems.

What to look at on the dashboard

Pipeline

Cron Trigger (06:00 ET) ──▶ Workflow Instance │ ┌─────────────────────┼─────────────────────┐ │ │ │ Browser Rendering Workers AI Llama 70B AI Gateway ──▶ Workers AI 70B (3 competitor pages, (per-page normalize OR Claude Sonnet via real headless into common signal Unified Billing Chrome) schema) (one synthesis call) │ │ │ └─────────────────────┴─────────────────────┘ │ ┌───────────┼───────────┐ │ │ R2 archive D1 audit log (raw HTML + (runs, signals, brief.json) model calls, costs)

Services and roles

ServiceRole in this app
WorkersCompute. Hosts the cron handler, the API, the orchestrator, and serves the dashboard from a single deployment.
WorkflowsDurable orchestration. Each step retries independently — one failed competitor fetch doesn't kill the run.
Cron TriggersFires the morning workflow at 10:00 UTC (06:00 ET) every day.
Browser RenderingReal headless Chrome instances fetch competitor pages, including SPA-rendered content.
Workers AILlama 3.3 70B for structured signal extraction and (when selected) synthesis.
AI GatewayUnified observability. Every model call — Workers AI and Claude — logged + costed in one dashboard. Unified Billing for Anthropic.
R2Object storage. Raw competitor HTML, normalizer outputs, and final brief JSON. Zero egress fees, ever.
D1SQL audit log. Three tables: runs, signals, model_calls. Queryable record of everything the agent did.
KVConfiguration store for competitor URL list and prompt versions.
Workers LogsReal-time stack traces and console output. Powers the wrangler tail debugging loop.
Application SecurityCloudflare Access policy on the hostname. Email-allowlisted users only; everyone else is blocked at the edge before the Worker runs.

Data persistence

How the build proceeded

This log exists so anyone reviewing the demo can see what was attempted, what broke, and what was fixed. Governance is the record, not the intention.

Step 1

Scope

Picked the Community Competitive Intelligence use case from the eight in the Empire AI framework. Anchor community: Empire Avalon, Caledonia. Competitors: Losani, Mattamy, Branthaven — three Hamilton-area builders overlapping the catchment.

Step 2

Architecture lock-in

Every primitive runs on Cloudflare: Workers, Workflows, Browser Rendering, Workers AI, AI Gateway, R2, D1, KV, Cron, Logs, Application Security. Workflows specifically chosen so the orchestrator is durable and steps retry independently.

Step 3

Scaffold + provision

Project structure, wrangler config with 11 bindings, D1 migration applied to remote DB in 1.08 ms. Three CLI commands provisioned D1, R2, and KV.

Win

First deploy live in 7 seconds

23 KB worker, all bindings wired, custom Workflow class registered, cron schedule active.

Bug

Browser Rendering called wrong way

First end-to-end run failed. Used the REST API URL pattern via the binding's fetch. The binding is meant for puppeteer/playwright; the REST API is a separate auth surface.

Fix

Switched to @cloudflare/puppeteer

Rewrote the extractor: puppeteer.launch(env.BROWSER) → newPage() → goto() → content(). Extractions succeeded on the next deploy.

Bug

AI Gateway returned error 2001

"Please configure AI Gateway in the Cloudflare dashboard." First-time gateway names must be created before env.AI.run({gateway:{id}}) will route to them.

Fix

Gateway created, MCP authorized

Created empire-avalon-demo gateway. Authorized the Cloudflare API MCP so subsequent gateway management — provider config, Unified Billing, rate limits — runs programmatically.

Win

End-to-end run completed

All four model calls visible in AI Gateway. Workflow status: complete. Brief in R2. Audit row in D1. Cost: $0.0012. Wall time: 40 seconds.

Bug

Brief said "no movement" for every competitor

Pipeline running, signals empty. Inspected R2-archived HTML directly: Losani's homepage is a SPA that hadn't hydrated by domcontentloaded; Mattamy's URL was a 404 page; the 8B normalizer was returning {"signals":[]} regardless of input.

Fix

Custom domain + Application Security policy

Three API calls: zone lookup, custom-domain attachment with TLS, Access app + email-allowlist policy. Two minutes including cert provisioning. Whole hostname behind Application Security.

Fix

Tuned Browser Rendering for SPAs

Switched waitUntil from domcontentloaded to networkidle2. Added a 4-second hydration buffer. Reverted Mattamy to a URL path that returns content.

Fix

Normalizer upgraded to Llama 3.3 70B

The 8B fast model over-constrained under JSON mode and short-circuited. The 70B follows the schema from prompt instructions alone. Cost per page rose from $0.0002 to $0.001 — still trivially cheap. First 70B run extracted 10 real signals with prices, evidence, and confidence scores.

Bug

Brief empty despite real signals

Inspected the synthesizer's raw output in R2: the Workers AI binding wraps structured responses as {response: {sections}, tool_calls, usage}. The parser was looking for sections at the wrong nesting level.

Win

End-to-end working with real signals

11 signals, 3 brief sections, real prices ($499K Losani Benchmark Towns, $700–$800K Branthaven), recommendations grounded in Empire Avalon Caledonia. 30-second runtime. $0.0041 total cost.

Win

Claude via AI Gateway Unified Billing — working

Wired the AI Gateway authorization secret. Clicked the Claude toggle in the dashboard. Same workflow re-ran with synthesis routed to Claude Sonnet 4.6 via Unified Billing — no separate Anthropic key, Cloudflare bills the upstream provider spend. 13 signals, $0.0144. Brief quality jumped — Claude caught a "Losani launches HST Relief incentive" headline the Workers AI run had missed.

Refinement

Dashboard re-skinned in Empire brand

Adopted the wine, navy, orange, and cream palette from empirehomes.com. Playfair Display + Inter for editorial feel.

Refinement

UX polish

Cloudflare services moved to a dedicated Stack tab so the Daily brief view runs full-width. External source links added to every brief section. Tooltips on the provider toggle and Run workflow button. Hash-routed tabs so each view is its own URL.

Refinement

Run history → drawer

Clicking any row in Run history opens a slide-in drawer with the full run detail: brief sections, every signal extracted, every model call with cost and latency. Backdrop and Escape close it.

Refinement

Hard-coded cost numbers replaced with measured averages

Added a /api/stats endpoint that returns average cost per run by provider, sourced from D1. The About tab pillar cards now show real averages — "Avg over N runs" — instead of static placeholders.

Issues hit, lessons learned

SurfacedLessonFix time
Wrong Browser Rendering patternThe env.BROWSER binding is for puppeteer/playwright; the REST API is a separate auth surface.2 min
AI Gateway didn't auto-createFirst-time gateway names must exist before env.AI.run({gateway:{id}}) routes.90 sec
XSS via raw innerHTMLSecurity review caught LLM output being interpolated raw. Added an escape helper and a delta allowlist.5 min
SPA returned 404 before hydrationdomcontentloaded fires before client-rendered listings; networkidle2 + 4s wait fixed it.5 min
Llama 3.1 8B fast degenerated under JSON modeOver-constrained inference short-circuits; 70B follows the schema from prompt alone.8 min
Workers AI envelope wrappingThe binding nests structured output under .response; the parser was at the wrong level.4 min

What the pilot requires beyond this demo

This demo proves the architecture. Standing up the Empire AI lab properly requires the operating model around it.

The reframe. The deck calls for an AI lab. The lab is not the code — Cloudflare's primitives compress that to a few hundred lines. The lab is the operating system around it: the source-policy reviews, the governance gates, the use-case PRDs, the team capability transfer. This demo is the architecture pattern that lab would run on, proved on real competitor pages with real model calls and real per-run cost.

Try the demo

Open the live dashboard. Click Run workflow to fire a fresh competitive brief in real time. Toggle the provider between Workers AI and Claude to compare the selected-model estimate and output quality. Click any row in Run history to see the full per-run detail — brief, signals, model calls, cost.