Text version of this page

Tech Audit: 84 website mechanicals, measured

This is a plain-text copy of this page for readers and agents without JavaScript. Agents should start at /for-agents/, or read the site's plain-text index at /llms.txt.

Machine-readable access: the MCP endpoint at /api/mcp (JSON-RPC 2.0 over HTTP, read-only, no authentication), the OpenAPI description at /openapi.json, and the capabilities manifest at /agents.json.

Futurum mechanicals — preview.erikbethke.com

Futurum mechanicals

preview.erikbethke.com · probed 2026-09-24 14:21 UTC · probe 1.0.3 · pack mechanicals@2026.09.0 · supersedes the 2026-09-21 readout

The deterministic floor: 84 checks that are either correct or not, that nobody has to have an opinion about. This is the current readout for the new build — what is shipped and working, and the one thing left. Every line carries its own evidence. Both sites were probed within seconds of each other.

84measurable mechanicals a 2026 website needs

futurumgroup.com today

45

New build cutover day

78

45 → 7814 → 0 failing

Overview Capabilities What changed Shipped Left to go Security Publish Cutover to-do

A 2026 website has at least

84

measurable mechanicals that decide whether search engines, answer engines and AI agents can find it, quote it and use it.

The new build passes 78 of 84 by cutover, up from 45 today, with zero failures. Every check carries its own evidence.

45

working on futurumgroup.com today

14 failing outright

→

78

working on the new build on cutover day: 70 today, plus 8 that switch on at cutover

0 failing

Show how the 84 are counted

futurumgroup.comlive today

45

9

14

4

12

New buildpreview.erikbethke.com

70

8

6

0of all 84 mechanicals84

working held for cutover (analytics, pixels, Search Console, MCP registry listing) partial failing not applicable can't be verified (no access to WordPress source)

72vs84

A higher ceiling. A WordPress site can only be scored on the 72 checks anyone can run from its URL. The new build can be held to all 84: 12 more, because every surface (llms.txt, sitemaps, the MCP server, feeds, .well-known) is a generated route in its source code, so it can be proven rather than hoped. The new build passes 11 of those 12; the twelfth is the analytics setting that switches on at cutover.

+25 mechanicals ahead today · +33 on cutover day · 14 → 0 failures · +12 checks only the new build can prove

Where all 84 stand on the new build

78 of the 78 that apply, on cutover day

70

8

6

70

Working today 59 public checks + 11 of the 12 repo checks. The live site's repo can't be scanned, so the comparison above counts its 12 as unverifiable.

8

Switch on at cutover, by decision GA4 · Tag Manager · Google Ads · Reddit pixel · Meta pixel · Search Console · the public MCP registry listing (it must name futurumgroup.com, so it can't be listed from the preview) · the GA env slot in the repo

6

Not scored, and not gaps

4 don't apply to this site: language variants (one language); OAuth login metadata (the MCP server is deliberately open); an alternative analytics tool (GA4 is the plan); www-vs-apex host (the preview has no www host)

1 this tool can't measure: a WebMCP hint (needs a real browser)

1 optional, skipped: humans.txt

One of those 6 turns into a real check at cutover: www.futurumgroup.com must 301 to the apex, as the live site already does. It is on the README cutover checklist (PR #64), with the MCP registry listing.

The 72 public checks, check by check (hover a dot for its check id)

futurumgroup.com

T0 crawl & index

T1 answer-engine files

T2 agent & MCP

T3 on-page & citability

T4 measurement

T5 hygiene

New build

T0 crawl & index

T1 answer-engine files

T2 agent & MCP

T3 on-page & citability

T4 measurement

T5 hygiene

The 12 repo-side checks apply to the new build only, because the incumbent's source is not ours to scan. 11 pass; the twelfth, a GA/GTM env slot, is the same cutover hold. The incumbent's GA4 and GTM show green because they are live there today.

12 of 13

the minimum bar · was 10 of 13

The one unmet item is GA4, held for cutover. The incumbent futurumgroup.com meets 7 of 13.

Conformance

100%

the must-pass checks: 30.5 of 30.5 weighted · was 91.8% · incumbent 72.7%

Adoption

9 of 11

optional extras: shipped, +2 partial · incumbent 4 of 11, +4 partial

Soft-404 host

No

a bogus path 404s

Open fails

0

was 3 · and no conformance warnings

Why the soft-404 check comes first

Seven of the eighteen hosts this probe has been run against answer HTTP 200 for paths that cannot exist, which makes every other reading on them fiction — robots.txt, llms.txt and sitemap.xml all “exist” because the CDN serves the homepage for any path. The preview is clean, so every number on this page is a real measurement.

In plain English

What the new site can do that the current one can't

A site now has three kinds of visitor: people, search engines and AI assistants like ChatGPT and Claude. The current site was built for the first two; the new one is built for all three. Tap any card to see the proof.

AI assistants can quote a whole article, not a scrap of it

When someone asks an AI “what does Futurum think about this?”, the AI can read the full article text directly, clearly labelled, and quote it accurately.

Todaythe article text isn't labelled for machines, and the homepage is heavy enough (414 KB) that AI readers cut it off before the end.

New siteevery article carries its full text for machines; the homepage is a third lighter (282 KB).

Show the proof

Run live against both sites, 2026-09-24

New site: the article text, labelled for machines

$ curl -s https://preview.erikbethke.com/insights/opswat-at-gisec-2026-can-you-secure-what-you-cant-detect/ | grep -o '"articleBody":"[^"]\{0,110\}' "articleBody":"OPSWAT debuted its AI Content Inspector at GISEC Global 2026 in Dubai, targeting a new class of file-borne ris

(1,020 words in full)

futurumgroup.com: the same article

$ curl -s https://futurumgroup.com/insights/opswat-at-gisec-2026-can-you-secure-what-you-cant-detect/ | grep -o '"articleBody":"[^"]\{0,110\}' (no output: the page describes itself to machines as a web page, an image and breadcrumbs, never as article text)

Homepage weight

$ curl -s -o /dev/null -w '%{size_download}' https://<site>/ futurumgroup.com 414,488 bytes preview.erikbethke.com 282,133 bytes

AI assistants get a reading guide to Futurum

A short guide tells an AI what Futurum is, what it covers and where to look, plus a complete text edition of the library it can read in one go.

Todaythe guide starts with an invisible stray character, so its title isn't recognised, and the complete edition is missing.

New siteboth present and correct.

Show the proof

Run live against both sites, 2026-09-24

futurumgroup.com: the first four bytes of its AI guide

$ curl -s https://futurumgroup.com/llms.txt | head -c 4 | xxd 00000000: efbb bf23 ...#

(an invisible byte-order mark sits before the "#", so readers expecting a title on line one don't find one)

The complete edition

$ curl -s -o /dev/null -w '%{http_code} %{size_download}' https://<site>/llms-full.txt futurumgroup.com 404 (missing) preview.erikbethke.com 200 2,071,305 bytes

New site: the guide opens cleanly

$ curl -s https://preview.erikbethke.com/llms.txt | head -3 # Futurum

> AI decision intelligence: research, benchmarks and live market signal for the people who have to choose.

AI agents can ask Futurum questions directly

Instead of guessing from search results, an AI agent can connect to the site and ask: search the research, list the analysts, show coverage of a company, fetch a report or the methodology. Think of it as a front desk for software.

Todaythere is no front desk. An agent can only scrape pages.

New site16 ready-made questions an agent can ask, using MCP, the open standard the major AI labs have adopted.

Show the proof

Run live against both sites, 2026-09-24

An AI agent asks: "What has Futurum published on quantum computing?"

$ curl -s https://preview.erikbethke.com/api/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "search_posts", "arguments": {"query": "quantum computing", "limit": 3}}}' { "count": 24, "posts": [ { "title": "Is Quantum Computing Languishing? – Report Summary", "path": "/press-release/is-quantum-computing-languishing-report-summary/", "publishedAt": "2024-12-19" }, { "title": "Are You “Quantum Ready,” Whatever That Means? – Report Summary", "path": "/press-release/are-you-quantum-ready-whatever-that-means-report-summary/", "publishedAt": "2025-06-02" }, { "title": "Will Quantum Computing Ever Be Useful for AI? – Report Summary", "path": "/press-release/will-quantum-computing-ever-be-useful-for-ai-report-summary/", "publishedAt": "2025-02-25" } ] }

The same question to futurumgroup.com

$ curl -s -o /dev/null -w '%{http_code}' -X POST https://futurumgroup.com/api/mcp -d '{...}' 404 (no front desk: nothing answers)

The preview carries an 881-record sample of the library until the full import lands, so counts here are smaller than they will be.

A quote can be checked, and cited properly

An AI that wants to be careful can confirm that a quote attributed to Futurum appears in its research, and get a proper citation with a link back.

Todayno way to check. A misquote looks the same as a real one.

New site&ldquo;verify quote&rdquo; and &ldquo;get citation&rdquo; are two of the 16 questions.

Show the proof

Run live against both sites, 2026-09-24

An AI checks a real quote before using it

$ curl -s https://preview.erikbethke.com/api/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "verify_quote", "arguments": {"quote": "Quantum computing is not new. The scientists Paul Benioff, Yuri Manin, and Richard Feynman proposed the"}}}' { "found": true, "title": "Is Quantum Computing Languishing? – Report Summary", "url": "https://preview.erikbethke.com/press-release/is-quantum-computing-languishing-report-summary/" }

...and a quote Futurum never published

$ curl -s https://preview.erikbethke.com/api/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "verify_quote", "arguments": {"quote": "this is a quote Futurum never published about quantum"}}}' { "found": false }

...then asks how to cite it

$ curl -s https://preview.erikbethke.com/api/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "get_citation", "arguments": {"path": "/press-release/is-quantum-computing-languishing-report-summary/"}}}' APA: The Futurum Group. (2024, December 19). Is Quantum Computing Languishing? – Report Summary. https://preview.erikbethke.com/press-release/is-quantum-computing-languishing-report-summary/ plain: The Futurum Group, "Is Quantum Computing Languishing? – Report Summary," December 19, 2024. https://preview.erikbethke.com/press-release/is-quantum-computing-languishing-report-summary/

Partners and developers can plug in without a meeting

The site publishes a machine-readable description of what it offers and how to use it, so another company's software can connect to Futurum's research on its own.

Todaynone published.

New sitefour: an API description, an agent &ldquo;business card&rdquo;, a capabilities file, and a page for agent builders.

Show the proof

Run live against both sites, 2026-09-24

New site: the API description

$ curl -s https://preview.erikbethke.com/openapi.json | jq -r '.info.title, (.paths | keys[])' Futurum Public Read API /api/content/companies /api/content/companies/{slug} /api/content/pages /api/content/pages/{slug} /api/content/people /api/content/people/{slug} /api/content/posts /api/content/posts/{slug} /api/content/practice-areas /api/search

The four machine-readable descriptions

$ for p in /openapi.json /.well-known/agent-card.json /agents.json /for-agents/; do curl -s -o /dev/null -w "$p %{http_code}\n" https://<site>$p; done futurumgroup.com new site /openapi.json 404 200 /.well-known/agent-card.json 404 200 /agents.json 404 200 /for-agents/ 404 200

Google finds every page, and old links still work

The site tells Google where its full page list is. Every one of the 36,533 addresses Google knows about today gets a clear answer: here it is, it moved (and where), or it was retired on purpose.

Todaythe page list exists, but the first file every crawler reads never mentions it.

New siteit does. Every old address is being mapped before the switch, so none fails without an explanation, and links other sites made years ago keep their value.

Show the proof

Run live against both sites, 2026-09-24

Does robots.txt, the first file every crawler reads, point to the page list?

$ curl -s https://<site>/robots.txt | grep -i '^sitemap' futurumgroup.com (nothing) preview.erikbethke.com Sitemap: https://preview.erikbethke.com/sitemap.xml

An old address that moved

$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}' https://preview.erikbethke.com/analyst/donald-jin/ 308 https://preview.erikbethke.com/donald-jin/ (moved for good; Google carries the link value across)

An old address retired on purpose

$ curl -s -o /dev/null -w '%{http_code}' https://preview.erikbethke.com/tag/creative-stack/ 410 ("gone, deliberately": Google drops it cleanly instead of retrying a broken link)

Every one of the 36,533 old URLs, checked

1,199 served directly, 132 redirected, 25,290 retired on purpose, the rest 404 for a tracked reason (import still landing, pending the content owner, or dropped on purpose). No unexplained 404s, down from 35,295 before this work &mdash; full breakdown in What changed.

Security researchers know who to tell

A standard file tells anyone who finds a security flaw exactly where to report it privately, instead of guessing, giving up, or posting it in public.

Todayno such file.

New sitepresent, with a contact address and an expiry date.

Show the proof

Run live against both sites, 2026-09-24

New site

$ curl -s https://preview.erikbethke.com/.well-known/security.txt Contact: mailto:security@futurumgroup.com Expires: 2027-08-23T00:00:00Z Preferred-Languages: en Canonical: https://preview.erikbethke.com/.well-known/security.txt

futurumgroup.com

$ curl -s -o /dev/null -w '%{http_code}' https://futurumgroup.com/.well-known/security.txt 404

It stays fixed, because it can't quietly break

Every item on this list is generated by the site's own code and tested on every change. An editor changing a page can't silently remove one, the way a plugin update can on WordPress.

Todaynobody can check, because WordPress's internals aren't open to inspection.

New site12 extra checks prove it: the &ldquo;72 vs 84&rdquo; above.

Show the proof

Run live against both sites, 2026-09-24

Where each item lives in the new site's code

$ pnpm audit:mechanicals --repo . # the 12 repo checks, summarised llms.txt generated route app/llms.txt/route.ts llms-full.txt generated route app/.../llms-full.txt/route.ts openapi.json generated route app/openapi.json/route.ts feed.xml generated route app/feed.xml/route.ts sitemap route handlers app/sitemap.xml/route.ts, app/sitemap/[shard]/... robots app/robots.ts MCP server app/api/mcp/route.ts .well-known app/.well-known/agent-card.json/route.ts, ...

Tested on every change

$ pnpm pre-deploy # the gate every pull request must pass type-check ok lint ok test 2,845 tests passed (PR #70's run) build ok

A guard test fails any pull request that makes a page read the whole library, so the site stays fast as it grows. WordPress's equivalents live in plugins and theme settings, which nobody outside can inspect.

Nothing is lost at the switch. Analytics, Search Console and the ad pixels switch on the day the new site goes live, so marketing keeps every number it has today.

What this doesn't promise. These are capabilities, not guaranteed traffic. Whether Google ranks a page, or an AI chooses to quote it, still depends on the research itself and on time. What changes is that nothing technical stands in the way, and after the switch we can measure the result.

Latest update, 2026-09-24 14:21 UTC (probe 1.0.3, PR #71; after PRs #64, #65 and #70)

Response time passes at 364 ms median of five samples (342&ndash;453 ms; the live site reads 391 ms).

The MCP registry check is now honest. It reads &ldquo;not listed&rdquo; on both sites, correctly; for the new build, listing is a cutover item.

URL parity, measured. All 36,533 URLs in futurumgroup.com's sitemap were requested on the deployed preview. No unexplained 404s, down from 35,295 before these PRs.

Show the URL breakdown (36,533 URLs)

Answer URLs

200, served directly 1,199

301 to an equivalent page 132

410 Gone, deliberately retired 25,290

404, not imported yet (lands with the slim runtime; its first phase merged as PR #70) 9,880

404, waiting on the content owner 18

404, mockup or test pages we're not carrying 14

What changed since 2026-09-21 PRs #51–#63 and a probe fix

&#10003;

MCP discovery manifest shipped — bar item 9 closed

/.well-known/mcp.json now serves and points at the live server; T2 went from 67% to 100%, and the server grew from 11 tools to 16.

t2.wellknown_mcp 200, valid JSON, 6,074 bytes &middot; servers[].url https://preview.erikbethke.com/api/mcp

&#10003;

MCP named in the first kilobyte of llms.txt — bar item 6 closed

llms.txt was restructured (36 KB to 12 KB, index-first); the MCP endpoint now sits inside the first 1,024 bytes, where a truncating fetcher reads it.

t1.llms_txt.first_kb_mcp MCP URL in first 1024 bytes: https://preview.erikbethke.com/api/mcp

&#10003;

Trailing-slash redirects no longer hang for 20 seconds

Every 308 redirect was sending an empty streamed body that never terminated, so CloudFront waited out its 20 s origin timeout. Redirects now answer in about 0.4–0.5 s.

before 308 20.42s /for-agents &middot; 20.41s /products &middot; 20.55s /methodology now 308 0.38s /for-agents &middot; 0.46s /products

&#10003;

Markdown mirrors, negotiated at the edge

Every page has a .md twin, negotiated at CloudFront. Agent-readiness Tier 3, done.

&#10003;

/ai4me, security.txt and a web app manifest

All three now ship. security.txt lists security@futurumgroup.com and expires 2027-08-23.

t1.ai4me 200, 3,980 chars server-rendered &middot; t5.security_txt 200, 163 bytes &middot; t5.manifest link=/manifest.webmanifest

&#10003;

The probe's false negatives are fixed

repo.sitemap and repo.well_known failed only because the probe didn't recognise App Router route handlers. Fixed; both now pass on their own.

&#10003;

Runtime and stage hygiene

The server Lambda runs Node 22 (the newest SST 3 accepts). Every non-prod stage serves noindex until cutover; only futurum-web-prod flips indexable.

Shipped and working the mechanicals that are done

The new build leads on every tier, and ships four capabilities the incumbent has none of.

preview (new build) futurumgroup.com (incumbent)

T0 crawl & index

100%

100%

T1 answer-engine files

100%

11%

T2 agent & MCP surfaces

100%

30%

T3 on-page & citability

100%

90%

T4 measurement

n/m

n/m

T5 hygiene

100%

94%

Per-tier only — these are never averaged into one grade. T4 reads &ldquo;n/m&rdquo;, not 0%: the tier holds no conformance checks, so there is nothing there to score.

What the new build ships that the incumbent does not

&#10003; Answer-engine files

llms.txt — 12,291 bytes, H1 + blockquote, MCP in the first KB (incumbent: no H1, no MCP)

llms-full.txt — 1,922,889 bytes (404)

agents.json — valid, 6,379 bytes (404)

agent card — valid, 8,476 bytes (404)

openapi.json — OpenAPI 3.1.0, 10 paths (404)

/for-agents — 19,562 chars server-rendered (404)

/ai4me — 3,980 chars (404)

feed.xml — 50 items (incumbent: RSS at /feed/, discovered via the homepage's advertised <link rel=alternate>; the probe now follows that link, so it passes on its own)

Markdown mirror of every page, edge-negotiated (none)

&#10003; A live, discoverable MCP server

/.well-known/mcp.json → /api/mcp

initialize → futurum-web-mcp 1.0.0, protocol 2025-06-18, open (no auth)

16 tools: search_posts, get_post, list_pages, get_page, verify_quote, list_analysts, get_analyst, list_companies, get_company_coverage, list_practice_areas, get_site_info, get_citation, get_practice_area, get_methodology, get_taxonomy, list_research_reports

Incumbent: no MCP endpoint (404 on /api/mcp and /mcp), though it does publish OAuth metadata.

&#10003; Citability — the crown jewel

articleBody in the deep-page JSON-LD: 7,353 chars / 1,020 words, plus SpeakableSpecification

4 JSON-LD blocks on the article, 4 on the homepage

Incumbent: no articleBody at all. Non-JS crawlers quote this, not the DOM. Checked on its real OPSWAT article, not just the auto-sampled careers page — same result.

&#10003; Crawl, index & hygiene

robots.txt 200, 8 groups, Sitemap: line present (incumbent has none — its only T0 miss)

Sharded sitemap index → 1,214 URLs (incumbent 729)

One canonical host; http 301s to https; bogus paths 404; redirects under 0.5 s

Homepage 281,350 bytes (incumbent 414,198 — over the truncation line)

security.txt and a web app manifest (incumbent: neither)

In the repo, as generated routes rather than static files

Every surface below is a real route that renders at request time, so it cannot drift from the content the way a hand-maintained file does. Repo checks: conformance 2 of 2, adoption 3 of 3.

Show the 9 routes

Surface Source State

llms.txt app/llms.txt/route.ts &#10003;pass

llms-full.txt app/llms-full.txt + app/practice-areas/[slug]/llms-full.txt &#10003;pass

openapi.json app/openapi.json/route.ts &#10003;pass

feed.xml app/feed.xml/route.ts &#10003;pass

robots app/robots.ts &#10003;pass

MCP app/api/mcp/route.ts &#10003;pass

sitemap app/sitemap.xml/route.ts + app/sitemap/[shard]/route.ts &#10003;pass

.well-known app/.well-known/*/route.ts (mcp.json, agent-card.json, security.txt) &#10003;pass

markdown mirrors app/api/md/[...segments]/route.ts + lib/seo/mirror/edge.ts &#10003;live

Left to go nothing that can close before cutover

! Held for cutover, by decision: analytics and pixels

The preview carries no GA4, GTM, Ads, Reddit or Meta tag yet, by decision: the pixels go in at cutover, not on a noindex preview where they would pollute production data. The incumbent is measured today — GA4 G-ES8BSET5RC and GTM containers GTM-KM6XDZR6 and GTM-W4999QMH. This is a cutover gate: measurement must not switch off on the day the before/after matters most.

The one unmet bar item

!

13. GA4 or a named alternative, browser-verified

The cutover hold above; the repo confirms the absence, and it is intended.

t4.ga4 — GA4 measurement id: not in server HTML repo.ga_env_slots — no GA/GTM env slot found

Cutover checklist items that ride with it

!

GTM, Google Ads, Reddit and Meta pixels

Same hold, same moment. Adoption warns only; none is scored.

t4.gtm · t4.google_ads · t4.reddit · t4.meta_pixel — not in server HTML

!

Google Search Console verification

Likely verified by DNS TXT, which carries over automatically. Confirm before cutover.

t4.gsc_verification — no google-site-verification meta (may be verified by DNS TXT or file)

!

Flip noindex off

Automatic: SITE_INDEXABLE is true only on futurum-web-prod.

Response time

&#10003;

t5.ttfb passes: 364 ms median of five (342&ndash;453 ms; the live site 391 ms) on a preview stage, not production

Why the earlier warning is goneAn earlier run graded one cold 964 ms sample as a warning. Three re-runs minutes later read 706, 408 and 403 ms, and all passed — the probe graded on a single request. Probe 1.0.3 (PR #71) discards one warm-up and scores the median of five. Production response time is measured at cutover, on the warm production path.

Security the September 2026 review

A white-hat security review ran on 2026-09-26: automated static analysis plus five independent, read-only reviews across the codebase, followed by two further pre-cutover passes. Every fix shipped as its own reviewed pull request, verified on a preview stage before merging. Nothing was deployed to production mid-review. There were no Critical findings.

Findings

28

2 High &middot; 12 Medium &middot; 14 Low

Fixed

27 of 28

one Medium item partly fixed, tracked below

Review passes

3

the initial review, plus two pre-cutover follow-ups

Prod deploys mid-review

0

every change verified on a preview stage first

Severity Found Fixed Open

High 2 2 0

Medium 12 11 1

Low 14 14 0

Total 28 27 1

Findings and fixes, by severity

All 28 findings (severity, what was found, what changed, and where)

Severity Finding Fix Where

High A patched Next.js image-processing vulnerability (remote code execution) plus a related cache-poisoning issue were present, and the image pipeline accepted more source hosts than it needed to. Fixed &mdash; upgraded Next.js and narrowed the accepted image hosts, with a test that forbids a wildcard host from being reintroduced. PR #100

High The image-rendering function bundled an image-processing library with known vulnerabilities. Fixed &mdash; pinned to a patched version, confirmed in the deployed build. PR #106

Medium The backend functions behind the site and its image pipeline could be reached directly, bypassing the CDN, its cache and its edge protections. Fixed &mdash; direct access now returns a clean rejection; all traffic must come through the CDN, which carries the protections below. PR #106

Medium The public site's backend held broader database and storage permissions than the code uses. Fixed &mdash; scoped to least privilege, matched to the commands the code sends. PR #106

Medium Scanning a publish request for credentials and unsafe content could take several seconds on a large request, before it was even rate-limited. Fixed &mdash; rewritten to run in linear time; the same requests now complete in well under a second. PR #108

Medium A publishing credential could edit, delete or reassign another author's posts. Fixed &mdash; ownership is enforced; only an editor-level credential can act on someone else's content. PR #101

Medium Content stored outside the normal git-reviewed path was not re-checked against the site's safety rules at render time. Fixed &mdash; re-checked against the same rules on every render; anything that fails renders as plain text and is logged. PR #104

Medium Search and the AI-agent interface (MCP) shared no rate limit, and a single agent request could trigger many expensive lookups at once. Fixed &mdash; results are briefly cached, batch size is capped, and both surfaces are rate-limited at the edge. PR #105, #110

Medium A deleted or unpublished item could still appear in search results for a while. Fixed &mdash; search now checks against what is published, and the cache expires in minutes rather than persisting until the next restart. PR #105

Medium The site's script policy (CSP) allows inline scripts on most routes by default, which would widen the impact if HTML injection ever occurred. Partly fixed &mdash; canvas documents (like this one) now render in a fully isolated sandbox, on their own web address, under their own tighter policy. The two most dynamic non-canvas routes moved to a stronger per-request policy. The rest of the site's routes are the one item still open &mdash; see below. PR #111, #116, #131

Medium A number of previously published articles still linked to a hostname associated with a past incident on the site's former hosting provider. Fixed &mdash; those links were rewritten to point at this site instead, with a test that blocks a recurrence. PR #109

Medium A now-unused legacy publishing path, and the shared access key that could use it, were still live. Fixed &mdash; retired entirely; the old key is now rejected outright. PR #128

Medium The edge network had no broad protection against common attack patterns beyond the site's own request checks. Fixed &mdash; managed protection rules and a site-wide request-rate limit were added, tuned in a monitor-only mode first to confirm no legitimate traffic was affected. PR #129

Medium Stored content had no point-in-time recovery or version history, so a bad delete or a bug could be permanent. Fixed &mdash; both enabled; a mistaken delete or cleanup is now recoverable. PR #129

Low Error responses from the AI-agent interface could include internal technical detail. Fixed &mdash; a generic message is returned; the detail is logged privately instead. PR #105

Low A markdown-format submission could slip an unsafe link scheme past the safety check, and a few text fields were not scanned at all. Fixed &mdash; the check was hardened and extended to cover those fields. PR #108

Low The standalone-document viewer used on pages like this one did not check where a document came from before rendering it. Fixed &mdash; it now only renders reviewed, git-committed documents; verified against every such document currently on the site. PR #104

Low The deployed server bundle included some source, documentation and test files it did not need. Fixed &mdash; excluded from the deployed package. PR #107

Low Automated build steps referenced third-party tooling by a movable label rather than a fixed, verifiable version, and dependency updates were not automated. Fixed &mdash; pinned to exact, verified versions; automatic dependency-update proposals turned on. PR #99

Low A strict, hard-to-reverse browser security header was being sent on every environment, including ones still under test. Fixed &mdash; scoped to when it is the right call. PR #107

Low Some links carried marketing tracking parameters, or routed through an old link-redirect service. Fixed &mdash; parameters stripped and links normalized across the affected content. PR #109

Low Content-import tooling followed a redirect without checking where it led. Fixed &mdash; every redirect hop is now checked against an approved list of hosts. PR #109

Low Deleted content bodies were never cleaned up in storage. Fixed &mdash; an operator-run cleanup tool was added, off by default, with a safety grace period before anything is removed. PR #117

Low A testing-only dependency carried a known low-severity advisory. Fixed &mdash; upgraded. PR #118

Low The build pipeline did not scan for accidentally committed secrets or run a dependency audit. Fixed &mdash; both added to the automated build. PR #127

Low There was no structured logging or alerting for security-relevant events, such as a spike in blocked or unauthorized requests. Fixed &mdash; dedicated logs and four automated alarms were added. Routing an alert to an actual person is still pending &mdash; see below. PR #129

Low Publishing credentials had no expiry or rotation, and no simple way to see which ones existed. Fixed &mdash; optional expiry and rotation added, plus a listing an operator can audit without ever seeing the credential itself. PR #133

Low The site depended on a third-party font-hosting service. Fixed &mdash; fonts are now self-hosted, and the dependency was dropped from the site's own security policy. (The standalone documents on this canvas lane still use it, deliberately, per this project's authoring rules.) PR #135

Checked and found clean

Data and history

Full git history, scanned for secrets, keys and account credentials &mdash; none found

Published content and static assets &mdash; no credentials, tokens, private IPs or personal data

Credential storage &mdash; modern password hashing with constant-time comparison, and safe, closed failure if a secret is ever missing

Surfaces and output

The published/draft visibility gate &mdash; holds identically across every read surface: the JSON APIs, the AI-agent interface, search, feeds, sitemaps and the plain-text AI-reading files

Structured data, feed output, redirect handling and file-path handling &mdash; checked for injection and traversal; clean

The backend's outbound network access and storage permissions, and the safeguards on its automated replication and cleanup tooling

Still open

!

Static-route CSP still allows inline scripts

Not on this page &mdash; see above. A tighter, per-response script policy for the rest of the site's routes is planned next.

!

No one is subscribed to the new security alarms yet

The four alarms added above fire correctly; routing one to an actual person is the remaining step.

!

Automated dependency-review scanning needs a platform feature enabled

The scan itself is wired up; it is waiting on a repository-level security feature to be turned on.

!

A couple of small infrastructure cleanups, deferred to the account cutover

A regional-configuration fix and an infrastructure-tooling upgrade are deliberately held for the move to Futurum's own hosting account. See the Cutover to-do tab.

!

The published security-contact mailbox needs confirming

A Futurum mail administrator needs to confirm it exists and receives outside mail before production launches on it. See the Cutover to-do tab.

Publish to this site one core behind every door, live in seconds

Everything above measures whether machines can read this site. This is what lets Futurum (or an AI acting for Futurum) write to it: one checked, safe call that gets a new post live in seconds, with no rebuild and no deploy.

The exhibit is live: preview.erikbethke.com/insights/sample-insight-publishing-pipeline/, created exactly the way the curl example below creates one. A second exhibit went further and published a report about itself: preview.erikbethke.com/research-reports/agentic-research-reports/, a research report on agentic research reports, written and published by an agent through this exact pipeline.

For developers: the publishing API (endpoints, curl, CLI and the AI system prompt)

Measured against this stage, 2026-09-24 &mdash; the exhibit below, no cache-busting

Step Time

Create answers 201 2.7 s

Article live at its own URL 9.9 s

/insights/ lists it 10.6 s

feed.xml carries it 11.3 s

Replaying the same request &mdash; same Idempotency-Key, same body &mdash; came back byte-identical with Idempotent-Replayed: true: no second revision, no duplicate post. No rebuild and no deploy anywhere in this; the whole round trip is HTTP.

Copy-paste for whichever AI you use

Four blocks, each complete on its own. Tap Copy and the exact text &mdash; with this site's own address filled in &mdash; is on your clipboard.

a. Give this to your AI a self-contained prompt any agent can follow

Paste this whole block into Claude, ChatGPT, Gemini or any coding agent, along with your token, and tell it what you want published.

Copy You can publish directly to this site over HTTP. Read this whole block before you write any code, then follow it exactly &mdash; every rule below is enforced server-side, not a suggestion.

ENDPOINT Create POST https://preview.erikbethke.com/api/posts Update PUT https://preview.erikbethke.com/api/posts/<slug> Delete DELETE https://preview.erikbethke.com/api/posts/<slug>?kind=<kind> (soft delete; append &force=true to hard-delete)

AUTH Authorization: Bearer <YOUR_TOKEN> A token looks like fxp_<id>.<secret>. You do not mint your own &mdash; it is issued by the site owner (see "For operators" below) and handed to you, usually as an environment variable.

IDEMPOTENCY Send an Idempotency-Key header on every write. Derive it from the request's own content &mdash; e.g. the slug plus a hash of the body &mdash; rather than a random value, so retrying after a timeout or a 5xx is always safe: the same key and the same body replay the original response with an Idempotent-Replayed: true header instead of creating a duplicate. The same key with a DIFFERENT body is refused outright (422 idempotency_key_reused) &mdash; mint a new key if the request is meant to change.

SUBMISSION BODY (POST and PUT), as JSON: { "kind": "insight", // insight | news | press-release | research-report &mdash; "page" is not a post; pages stay in the git lane "slug": "your-post-slug", // lowercase-kebab-case; fixed at creation, PUT cannot move it "title": "&hellip;", // <= 300 characters "dek": "&hellip;", // <= 1000 characters; the one- or two-sentence summary "body": { "format": "html", "source": "<p>&hellip;</p>" }, // format is "html" or "markdown" &mdash; "canvas" is refused, see below "authors": ["a-person-slug", { "guest": "A Guest Name" }], // optional; a guest is a name only, never a profile page "taxonomy": { "companies": ["nvidia"], // slugs &mdash; an unknown company is rejected, not invented "practiceAreas": ["ai"], // codes or slugs &mdash; unknown ones are rejected "featuredPeople": [], // person slugs &mdash; must resolve "insightType": "&hellip;", // optional, free text "verticals": [], "tags": ["free-text-tag"] // created on first use }, "seo": { "title": "&hellip;", "description": "&hellip;" }, // optional overrides only &mdash; there is no canonical override; the URL is derived from kind + slug "status": "draft" // draft | pending | publish }

HTML RULES (format: "html") Only these survive a server-side sanitizer; everything else is stripped (with its contents, for script/style/iframe/object/form and the like) or unwrapped: p, br, hr, h2, h3, h4, ul, ol, li, blockquote, figure, figcaption, img, a, strong, em, b, i, u, s, sub, sup, code, pre, and the full table family (table, caption, colgroup, col, thead, tbody, tfoot, tr, th, td). h1/h5/h6 are demoted to h2/h4. <a> keeps only href/title/class; <img> keeps only src/alt/title/width/height &mdash; no onclick, no style, no target. Every href or src must be http(s), mailto:, tel:, a root-relative path, or a #fragment. A TABLE NEEDS REAL <table> MARKUP &mdash; there is no markdown-table support anywhere on this site.

MARKDOWN RULES (format: "markdown") No raw HTML tags and no { or } anywhere in the source &mdash; either one is refused outright. That also rules out a table in a markdown body (see above): use format: "html" instead.

NOT ACCEPTED HERE format: "canvas" is refused (422 canvas_not_supported). A canvas document is a complete standalone HTML page and needs a sandboxed renderer this lane doesn't have yet &mdash; that stays a git-lane, reviewed-pull-request thing. A future publishAt is refused (422 scheduling_not_available). Publish now, or leave status as draft/pending and have a person publish it later.

IF IT FAILS 401 unauthenticated the token is missing, malformed, or wrong &mdash; check Authorization 403 forbidden the token's principal lacks the capability this needs (e.g. it can create but not publish) 409 duplicate_slug that slug already exists for this kind &mdash; pick another, or PUT to the existing one 422 scan_blocked the body reads like a credential, an internal hostname, or similar &mdash; find it and remove it 422 unknown_taxonomy a company, practice-area or featured-person slug doesn't exist &mdash; drop it or fix it 422 idempotency_key_reused this key was already used with a different body &mdash; mint a new one 429 (Retry-After: N) over the daily quota &mdash; wait N seconds, then retry with the SAME idempotency key

NEVER include a credential, an internal hostname, or an unreleased customer name in a submission &mdash; the server scans every field before it stores anything and blocks the request outright when it finds one.

b. curl, by hand create, replay, edit, delete

The same four calls, run straight from a terminal.

Copy export SITE="https://preview.erikbethke.com" # or your own site export TOKEN="<YOUR_TOKEN>"

# Create a draft insight curl -sS -X POST "$SITE/api/posts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: my-first-post-v1" \ -d @- <<'JSON' { "kind": "insight", "slug": "my-first-post", "title": "My first post", "dek": "A short summary of what this post covers.", "body": { "format": "html", "source": "<p>Hello from an AI-assisted publish.</p>" }, "taxonomy": { "tags": ["hello-world"] }, "status": "draft" } JSON

# Replay: run the exact command above again, unchanged. The response comes back # byte-identical, plus an Idempotent-Replayed: true header &mdash; nothing is created twice.

# Edit the title and publish it curl -sS -X PUT "$SITE/api/posts/my-first-post" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: my-first-post-edit-1" \ -d '{"kind":"insight","slug":"my-first-post","title":"My first post, now live","dek":"A short summary of what this post covers.","body":{"format":"html","source":"<p>Hello from an AI-assisted publish.</p>"},"taxonomy":{"tags":["hello-world"]},"status":"publish"}'

# Soft-delete it (restorable); append &force=true to hard-delete instead curl -sS -X DELETE "$SITE/api/posts/my-first-post?kind=insight" \ -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: my-first-post-delete-1"

c. The CLI pnpm publish:post, for a file on disk

Frontmatter in, a live post out. A .html file sends as HTML; anything else sends as markdown. The idempotency key is derived from the file's path and content, so re-running after a timeout is always safe.

Copy export FUTURUM_SITE_URL="https://preview.erikbethke.com" # or your own site export FUTURUM_PUBLISH_TOKEN="<YOUR_TOKEN>"

# Print the request and its Idempotency-Key without sending it pnpm publish:post content/drafts/my-post.md --dry-run

# Publish it for real &mdash; frontmatter (kind, slug, title, dek, status, taxonomy&hellip;) becomes the Submission pnpm publish:post content/drafts/my-post.md

# Edit the same file later, matched by its frontmatter kind + slug pnpm publish:post content/drafts/my-post.md --update

d. For operators only issuing and revoking a credential

Run by the site owner, never by the AI itself &mdash; this is the one place a credential is minted, and it needs infrastructure access the other three blocks don't. The token prints once, to stdout, and is never logged.

Copy # Issue a credential for a new agent, capped at 50 writes a day pnpm publishing:principal create my-agent --role agent --caps create,publish --quota 50

# Revoke it pnpm publishing:principal disable my-agent

Not built yet. Scheduling (a future publishAt is refused, full stop) &middot; the WordPress-compatible adapter and its signed webhook back to Polaris &middot; authenticated MCP write tools &middot; canvas documents on this API lane (a canvas page stays a git-lane, reviewed thing until a sandboxed renderer exists). Image upload isn't wired up either &mdash; there is no featuredImage media endpoint live yet &mdash; but a plain <img> with an absolute https:// src in an HTML body works today; the sanitizer keeps src, alt, title, width and height.

Cutover to-do issue #121, the Polaris-account move

Issue #121: the security and infrastructure work for moving this site to Futurum's own AWS account, safer to do on a freshly created environment than to migrate in place. Every item below is still open.

Account and infrastructure

!

Region pinning

A deployment-region setting needs to move to where the infrastructure tool reads it, so new stages are created in the right region from the start.

!

Move to the current major version of the infrastructure tooling

New stages get created on the newer major version directly. Today's version carries developer-tooling advisories only; none reach the deployed site.

!

Confirm the security-contact mailbox

A Futurum mail administrator needs to confirm it exists and receives outside mail before launch.

Email and DNS

added 2026-09-28, from a broader mail-security scan

!

Tighten the subdomain mail-spoofing policy

The anti-spoofing mail policy covers the main domain but not subdomains yet; it extends once reporting confirms every legitimate sender is accounted for.

!

Review the domain's approved mail-sender list

The approved-sender list is close to a technical lookup limit; retire unused senders before adding a new one.

!

Give any new site mail its own signed sender identity

If the new site ever sends mail as Futurum &mdash; alerts, publishing notices &mdash; it gets its own signed sender rather than joining the shared list.

!

Retire or tighten the preview domain's mail policy

The preview domain carries a looser anti-spoofing policy; retired or tightened alongside the account cutover.

!

Confirm the security-contact mailbox

The same mail-administrator task as above &mdash; grouped here because it is the same person doing the same check.

Live list. This tab summarizes issue #121 on GitHub as of 2026-09-28. None of these are new security findings by themselves &mdash; see the Security tab for the review they came out of.

packmechanicals@2026.09.0 (row probe_version 1.0.3, from PR #71)

probed_at2026-09-24T14:21:40Z — preview · 14:21:31Z — futurumgroup.com

deep page/insights/opswat-at-gisec-2026-can-you-secure-what-you-cant-detect/ (incumbent: auto-selected /about-us/careers/)

reposite source @ main d6a5641 · preview deployed from 1b05218

denominator30.5 weighted conformance points, T0–T5 (incumbent 32)

rowsthe probe's JSON rows, one file per site

This scan reads only public surfaces. It never submits a form, signs up, or logs in. The pack rots — llms.txt, agents.json and MCP conventions move monthly; treat a suite older than a quarter as suspect.