5 static read endpoints. 12 live application endpoints. One Stripe webhook. Honest framing on stability.
What this page is. The Factory exposes two API surfaces. The static surface is just files Caddy serves at fixed paths. The application surface is a small Node service that powers the catalog's buyer journey (dossier unlocks, intent capture, ratings, Stripe webhook).
What this page is not. Not a third-party-developer API contract. There is no versioning, no SLA, no rate limits. Shapes can change when we change them. We will not break you on purpose, but we will not coordinate either.
The honest framing. Treat the static endpoints as "public artifacts" you can rely on. Treat the application endpoints as "the same routes the catalog uses internally" - you can hit them, they are running, they work today. The Stripe webhook is live and logs every event.
GET /factory/state.json
GET /factory/adoptability.json
GET /factory/admin/health.json
GET /factory/healthz
GET /factory/dossiers/<slug>/teaser.md
GET /factory/api/dossier/<slug>
POST /factory/api/mint-token
POST /factory/api/stripe-webhook
POST /factory/api/intent
POST/GET /factory/api/rate
POST /factory/api/feedback
POST /factory/api/event
POST/GET /factory/api/validation
POST /factory/api/afterhours/<route>
Files Caddy serves from disk. No backend. Refreshed by cron. Safe to poll.
Mission-control state snapshot. The numbers on the homepage live here first. Refreshed every 5 minutes by regen-state-json.py.
{
"generated_at": "2026-05-14T06:15:01.496803",
"products": {
"total": 249,
"tiers": { "showcase": 8, "solid": 238, "rough": 0, "sketch": 3 }
},
"assets": {
"founders": 39, "brand_pitches": 33, "elevator_pitches": 223,
"hero_ui_mocks": 10, "skeptic_memos": 232
},
"cost": { "total_usd": 171.78 },
"director": {
"last_tick": 1,
"last_tick_ts": "2026-05-14T05:55:07-07:00",
"last_decision": null
}
}
adoptability.json's total, which counts only products with a current Adoptability score.Every scored product with its 10-axis Adoptability breakdown, composite score, tagline, archetype, and category. The rawest catalog feed we publish. Refreshed every 15 minutes by adoptability-score.py.
{
"generated_at": "2026-05-14T13:15:01.993825Z",
"total": 244,
"wes_picks": ["bookkeeper-ai", "dispatch-ai", "nurture-ai", "..."],
"products": [
{
"slug": "bookkeeper-ai",
"name": "Bookkeeper AI",
"tagline": "Your books, done by morning.",
"archetype": "marketplace-or-tool",
"category": "finance",
"axes": {
"pain_intensity": 8, "buyer_clarity": 10, "speed_to_mvp": 7,
"distribution_difficulty_easy": 10, "market_saturation_open": 10,
"implementation_upsell": 7, "...": "..."
},
"score": 80
}
]
}
Live health-check across 73 endpoints. Each entry includes HTTP status, response time in ms, response size, and description. This is what /quality-report/ reads. Refreshed every 10 minutes.
{
"generated_at": "2026-05-14T13:10:04.207726Z",
"total": 73,
"passing": 73,
"failing": 0,
"avg_response_ms": 26,
"results": [
{
"name": "homepage",
"method": "GET",
"path": "/factory/",
"expected": 200,
"actual": 200,
"size": 18960,
"elapsed_ms": 213,
"ok": true,
"description": "Mission control homepage"
}
]
}
failing is either zero or it is not.One-line text probe for monitoring. Format: OK last_tick_age=Nm. Updated every minute.
OK last_tick_age=23m
last_tick_age means. Minutes since the Director loop completed its most recent tick. If this climbs above ~30 during work hours, something has stalled and the autonomous claim is at risk. We get paged at 60.A small Node service Caddy reverse-proxies under /factory/api/*. CORS-enabled. JSON in, JSON out unless noted.
Returns the full Markdown dossier for a product. Token is an HMAC-signed JWT-like string bound to the slug, minted at unlock. Default response is text/markdown; pass format=html for a styled standalone page.
$ curl "https://wishdeal.com/factory/api/dossier/bookkeeper-ai?t=<token>"
# Bookkeeper AI - Full dossier
## ICP
Two-bookkeeper firms billing $300k - $800k...
[full markdown body]
# Without a valid token:
$ curl "https://wishdeal.com/factory/api/dossier/bookkeeper-ai?t=invalid"
{"error":"access denied","reason":"malformed","message":"Unlock the dossier at
https://wishdeal.com/factory/unlock/bookkeeper-ai/ to receive a valid token."}
# HTTP 403
Mints an HMAC token for a slug. Used by the Stripe webhook on successful payment. Also callable directly with the admin key for ops (issuing a token to a buyer who paid via a different rail).
$ curl -X POST https://wishdeal.com/factory/api/mint-token \
-H "x-admin-key: $KEY" \
-H "content-type: application/json" \
-d '{"slug":"bookkeeper-ai","ttl_seconds":31536000}'
{
"ok": true,
"slug": "bookkeeper-ai",
"token": "eyJ...",
"url": "https://wishdeal.com/factory/api/dossier/bookkeeper-ai?t=eyJ...",
"expires_in": 31536000
}
Receives Stripe events. Logs every payload to /home/ubuntu/factory/inbox/stripe-webhook.jsonl for ops review. When STRIPE_WEBHOOK_SECRET is set in env, validates the Stripe-Signature header (HMAC-SHA256 over t=...,v1=...) before processing. Without the secret it runs in "placeholder" mode (logs, no signature check, no auto-mint).
# Placeholder mode response (secret not set):
{ "ok": true, "mode": "placeholder",
"message": "Stripe webhook endpoint is up but not yet validating signatures.
Set STRIPE_WEBHOOK_SECRET to enable." }
# Signature mismatch:
{ "error": "invalid signature" } # HTTP 400
# Expected event types: checkout.session.completed -> mints dossier token,
# emails buyer the URL. Pipeline is wired in code; full delivery activation
# is a Wes-blocker (SMTP creds + final QA on email template).
stripe-webhook.jsonl file is the audit trail.Captures buyer intent: which product, what tier (unlock / adopt / operator), email, optional note. Used by the "Talk to us" CTA on product pages and the operator-partnership page. Appended to inbox/intent-capture.jsonl.
$ curl -X POST https://wishdeal.com/factory/api/intent \
-H "content-type: application/json" \
-d '{"slug":"bookkeeper-ai","tier":"operator","email":"a@b.com",
"note":"Interested in white-labeling for my accounting agency"}'
{"ok": true}
# Validations:
# - slug required, max 80 chars, [a-z0-9_-]
# - tier must be one of: unlock | adopt | operator
# - email must match basic regex
Records or reads Wes's product tier ratings. The wes_picks field in adoptability.json is derived from these. Tiers: pursue / curious / shelf / not-for-me.
# Read all ratings:
$ curl https://wishdeal.com/factory/api/rate
{ "bookkeeper-ai": {"tier":"pursue","note":"clear pain","ts":"..."}, "..." }
# Set a rating:
$ curl -X POST https://wishdeal.com/factory/api/rate \
-H "content-type: application/json" \
-d '{"slug":"bookkeeper-ai","tier":"pursue","note":"clear pain"}'
{"ok": true, "ratings_count": 47}
Generic buyer feedback. Captures slug (optional), message, optional email. Stored at inbox/feedback.jsonl. Used by the "Found a problem?" footer on product pages.
Lightweight analytics event. Captures event_name, slug, optional props object. Stored at inbox/event-capture.jsonl. Used by the catalog's own client-side instrumentation (page view, scroll depth, CTA click).
Per-product validation tracker. Records events like "demo booked", "operator signed", "first paying customer". Used by the foreman ticks and the per-product validation page.
# Read validation for a slug:
GET /factory/api/validation/bookkeeper-ai
{ "slug":"bookkeeper-ai", "events":[...], "operators":[...] }
# Read everything:
GET /factory/api/validation
{ "generated_at":"...", "products":{ "bookkeeper-ai":{...}, ... } }
# Record an event:
POST /factory/api/validation/event
{ "slug":"bookkeeper-ai", "type":"demo_booked", "data":{...} }
Product-specific endpoints for the afterhours build. demo runs the live in-browser demo against a fixture; signup appends to builds/afterhours/data.jsonl. Treat these as product-internal - not stable across the catalog.
/factory/api/<product>/*. So far only afterhours has live routes. The convention is documented for the Director loop's reference.state.json keeps products, assets, cost, director. adoptability.json keeps products[].slug, products[].score, products[].axes. admin/health.json keeps total, passing, failing, results[].ok.{"reason":"malformed"|"expired"|"sig mismatch"|"slug mismatch"}.wes_picks. It changes weekly based on his rating activity./factory/admin/ that is not health.json, anything under /factory/api/ we have not documented here yet).If you are building something that needs a Wishdeal Factory feed we have not exposed (per-archetype rollups, vertical-filtered slices, founder list with handles, etc.), we will most likely just publish it. Email wes@wishdeal.com with the shape you want. The catalog is meant to be read.