Stackbilder Platform API Reference
Base URL: https://stackbilder.com
Last updated: 2026-05-02
Worker: stackbilt-web (Astro SSR on Cloudflare, 47 API route files / 60 HTTP method handlers bundled in one worker)
All API routes live at /api/* paths on stackbilder.com. They are Astro server-side endpoints compiled into the stackbilt-web Cloudflare Worker at deploy time. There is no separate API worker. Cross-worker concerns (authentication, TarotScript execution, img-forge rendering, CodeBeast ledger) are handled via Cloudflare service bindings from inside this worker.
The API is the canonical contract for all four consumer types: the web UI at /app/*, the MCP gateway, the Charter CLI, and direct API consumers. No consumer is privileged. See memory feedback_two_consumer_fractal for the principle.
Table of contents
Authentication
Two auth modes:
| Mode | How | Use |
|---|---|---|
| Session cookie | better-auth.session_token (dev) or __Secure-better-auth.session_token (prod) | Web UI from a logged-in browser |
| API key | Authorization: Bearer ea_* | CLI, MCP, server-to-server, CI |
Routes typically accept either unless noted. A handful (admin endpoints, form-handling helpers) restrict to one mode.
For API keys, resolve your identity with GET /api/account/me — useful for CLIs, MCP callers, and for determining the orgId used in entitlement checks.
Mint an API key from the web UI at /settings or via POST /api/keys.
Common patterns
Identity scope — split-by-policy contract
Per edge-auth’s split-by-policy contract (receipt cbfcacecda58e33be77f9cd2b6afb142ecd18a2b30a68a31906658a723d5c16e):
- Org-level scope — SKU, tier, plan, features, Trust Page ownership. Every entitlement check uses
user.orgId. - Per-resource scope — quota attribution, D1 row tenant_id, rate-limit keys. Uses
user.tenantId || user.orgId || user.id.
This is why API-key auth (where tenantId is typically null) and session auth (where tenantId is the current tenant) resolve to the same org-level answer for entitlements but different values for per-resource scopes.
Error response shape
Most routes return JSON errors in this shape:
{ "error": "human-readable message", "code": "machine_code_optional", "upgrade": "/pricing_optional" }
Codes you’ll see in practice:
already_subscribed— 409 on checkout when the tenant already has a paid subscriptioncross_tenant_conflict— 409 on trust/slug or bundle write when another tenant owns itcross_tenant_forbidden— 403 on delete when another tenant owns itlibrary_limit— 402 on Evidence library when at tier capno_stripe_customer— 422 on billing portal when no customer existsevidence_gap_fill_requires_pro— 402 when tier isn’t pro/teamevidence_gap_fill_rate_limited— 429 on 30/hr defense rate limitevidence_gap_fill_quota_exceeded— 429 on edge-auth quotaempty_library— 400 on gap-fill when no assets exist for tenant
Rate limits
| Route family | Limit | Source |
|---|---|---|
/api/billing/checkout | 10/hr per user | stackbilt-web |
/api/billing/downgrade | 5/hr per user | stackbilt-web |
/api/v1/evidence/gap-fill | 30/hr per tenant | stackbilt-web (defense layer) |
/api/v1/evidence/gap-fill | evidence_gap_fills quota | edge-auth |
/api/agents/* | scaffolds quota | edge-auth |
/api/flows/* | scaffolds quota | edge-auth |
/api/images/generate | images quota | edge-auth |
/api/contact | 3/hr per IP | stackbilt-web |
/api/subscribe | 5/hr per IP | stackbilt-web |
/api/support/tickets | 5/hr per user | stackbilt-web |
/api/scaffold/preview | 10/min per IP | stackbilt-web |
/api/mcp tools | 5–10/min per isolate | stackbilt-web |
Service bindings
The worker uses these Cloudflare service bindings to reach sibling workers (RPC / Fetcher over internal network):
| Binding | Target | Purpose |
|---|---|---|
AUTH_SERVICE | edge-auth | Sessions, API keys, entitlements, quotas, Stripe integration |
TAROTSCRIPT | tarotscript-worker | Agent consultations, scaffold harness, receipt signing |
IMG_FORGE | img-forge-gateway | Image generation |
CODEBEAST | codebeast | /decide receipts ledger (Trust Page governance timeline) |
Plus direct bindings: OBSERVE_DB, TRUST_DB, EVIDENCE_DB (D1), TRUST_BUNDLES (R2).
Reference
Health & meta
GET /api/health
Purpose: Public liveness and dependency health check for external uptime monitors.
Auth: None
Success responses:
// 200 OK (healthy) or 503 (degraded)
{
status: "healthy" | "degraded",
checks: {
auth_service: { status: "pass" | "fail", latency_ms: number },
tarotscript: { status: "pass" | "fail", latency_ms: number },
img_forge: { status: "pass" | "fail", latency_ms: number }
},
latency_ms: number,
timestamp: string
}
Example:
curl https://stackbilder.com/api/health
Source: src/pages/api/health.ts
Notes: Probes AUTH_SERVICE, TAROTSCRIPT, IMG_FORGE bindings.
Authentication & account
GET /api/auth/relay
Purpose: OAuth session relay — receives session token from auth.stackbilt.dev after OAuth callback, sets secure cookie, redirects to target page.
Auth: None
Query params:
token(required) — Session token from auth serviceredirect(optional) — Target path after login; default/dashboard. Validated as safe relative path.
Response: 302 redirect with Set-Cookie: better-auth.session_token=...
Redirects to /login if no token, or /dashboard if redirect is unsafe.
Notes: Called by edge-auth during SSO flow. Cookie is HttpOnly, Secure, SameSite=Lax, 7-day Max-Age.
Source: src/pages/api/auth/relay.ts
GET /api/account/me
Purpose: Introspect the authenticated caller’s identity — { userId, orgId, tenantId, email }.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
userId: string,
orgId: string | null,
tenantId: string | null,
email: string | null // empty when using API key auth
}
Error responses:
401— no valid session or API key
Example:
curl -H "Authorization: Bearer ea_YOUR_KEY" https://stackbilder.com/api/account/me
Source: src/pages/api/account/me.ts
Notes: Used by Charter CLI (charter whoami), MCP tools for caller identity, and for determining the orgId used in CONSULT_DOGFOOD_ORGS or entitlement checks.
POST /api/account/name
Purpose: Update the authenticated user’s display name.
Auth: Session cookie only
Request body:
{ name: string } // 0–100 chars; empty string clears
Success responses:
// 200 OK
{ ok: true, name: string | null }
Error responses:
400— invalid JSON or name exceeds 100 chars401— not authenticated403— session token invalid or insufficient permissions (forwarded from AUTH_SERVICE)502— AUTH_SERVICE unavailable
Notes: Forwards session token to AUTH_SERVICE.updateUser() for server-side ownership verification.
Source: src/pages/api/account/name.ts
API keys
GET /api/keys
Purpose: List the authenticated user’s API keys (masked).
Auth: Session cookie only
Success responses:
// 200 OK
[
{
id: string,
name: string,
maskedKey: string, // last 4 chars shown
createdAt: string,
lastUsed: string | null
}
]
Source: src/pages/api/keys/index.ts
POST /api/keys
Purpose: Generate a new API key.
Auth: Session cookie only
Request body:
{ name: string } // required, non-empty
Success responses:
// 201 Created
{ id: string, name: string, key: string, createdAt: string }
The full key (format ea_*) is returned once; save it now — it cannot be retrieved later.
Error responses:
400— name missing or empty401— not authenticated
Source: src/pages/api/keys/index.ts
DELETE /api/keys/:id
Purpose: Revoke an API key.
Auth: Session cookie only
Success responses: 204 No Content
Source: src/pages/api/keys/[id].ts
Usage
GET /api/usage
Purpose: Current quota usage and tier for the authenticated user.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
scaffolds: { used: number, limit: number },
images: { used: number, limit: number },
tier: "free" | "pro" | "team",
cycle_ends_at: string // ISO 8601
}
Tier limits: free = 3 scaffolds / 5 images, pro/team = 50 scaffolds / 100 images.
Notes: SKU/tier is org-level per edge-auth split-by-policy contract. Defaults to free tier if AUTH_SERVICE lookup fails or tenant not provisioned.
Source: src/pages/api/usage.ts
Agent consultations
Structured C-level agent consultations powered by TarotScript. Both CTO and CISO receive signed receipts independently verifiable at https://tarotscript-worker.blue-pine-edf6.workers.dev/verify/<hash>.
POST /api/agents/run
Purpose: Execute an agent consultation — returns structured analysis + guidance + receipt.
Auth: Session cookie or Bearer ea_*
Request body:
{
role: string, // cto, ciso, cfo, cmo, cpo, architect
intention: string, // what decision or question
context?: Record<string, unknown>, // optional structured context
painSignals?: string[], // optional pain points
responseMode?: "symbolic" | "natural" | "structured-only",
seed?: number // optional for deterministic results
}
Success responses:
200 OK—AgentRunResultTypefrom@stackbilt/contracts/agent-response(or its stackbilt-web mirror atsrc/contracts/agent-response.ts):{ schema_version, success, role, response, symbolicResponse, analysis, guidance?, receipt, responseMode, metadata, envelope? }
Error responses:
400— “role and intention are required”401— Unauthorized429—{ error, upgrade: "/pricing" }— scaffolds quota exceeded502— upstream TarotScript error
Example:
curl -X POST https://stackbilder.com/api/agents/run \
-H "Authorization: Bearer ea_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "cto",
"intention": "Evaluate Cloudflare Workers vs AWS Lambda for a multi-region edge API",
"context": { "stage": "seed", "teamSize": "2_5" }
}'
Source: src/pages/api/agents/run.ts
Notes: Forwards to tarotscript /agents/run with inscribe: true so receipts persist to D1 for verification. Consumes one scaffolds quota credit. CTO runs keep claims private (publish_claims not set); CISO intake flow opts in via /ciso-intake route below.
POST /api/agents/ciso-intake
Purpose: Structured CISO posture intake — returns a signed Trust Bundle receipt or a refusal with remediation guidance.
Auth: Session cookie or Bearer ea_*
Request body: CisoIntakePayload (see src/contracts/ciso-intake.ts)
{
schemaVersion: 1,
companyProfile: {
name: string,
employeeCount: number,
foundingDate: string, // ISO date YYYY-MM-DD
fundingStage: "pre-seed" | "seed" | "series-a" | "series-b" | "series-c" | "growth" | "bootstrap" | "public",
customerTypes: ("smb" | "mid-market" | "enterprise" | "consumer" | "government" | "nonprofit" | "other")[],
dataResidency: ("us" | "eu" | "uk" | "ca" | "apac" | "latam" | "global")[]
},
techStack: {
cloudProvider: CloudProvider[], // aws | gcp | azure | cloudflare | vercel | fly | digitalocean | on-prem | hybrid | other
compute: Compute[], // containers | serverless | vm | kubernetes | workers | lambda | cloud-run | ec2 | edge-functions | other
database: Database[], // postgres | mysql | mongodb | dynamodb | firestore | d1 | cloudflare-kv | redis | sqlite | spanner | other
monitoring: Monitoring[], // datadog | new-relic | sentry | honeycomb | grafana | cloudflare-analytics | workers-analytics | splunk | none | other
cicd: CiCd[] // github-actions | gitlab-ci | circleci | jenkins | cloudflare-pages | vercel | manual | wrangler | other
},
dataHandled: ("pii" | "phi" | "pci" | "financial" | "biometric" | "credentials" | "health" | "none")[],
controlBaseline: {
mfa: { scope: "universal" | "privileged-only" | "partial" | "none", owner: string, notes?: string },
encryptionAtRest: { algo: "aes-256" | "aes-128" | "customer-managed-keys" | "cloud-provider-default" | "none", owner: string, notes?: string },
encryptionInTransit: { tlsVersion: "tls-1.3" | "tls-1.2" | "tls-1.1" | "tls-1.0" | "legacy" | "none", owner: string, notes?: string },
iam: { model: "least-privilege" | "role-based" | "attribute-based" | "flat" | "ad-hoc" | "none", owner: string, notes?: string },
logging: { retentionDays: number, centralized: boolean, owner: string, notes?: string },
workstationSecurity: { screenLock: boolean, diskEncryption: boolean, edr: boolean, owner: string, notes?: string },
passwordManager: { tool: string, coverage: "universal" | "partial" | "none", owner: string, notes?: string },
peerReview: { model: "peer" | "single-maintainer" | "multi-agent" | "automated-only" | "none", owner: string, notes?: string }
},
subprocessors: Array<{
name: string,
purpose: string,
dataAccess: SubprocessorDataAccess[], // pii | phi | pci | financial | biometric | credentials | health | logs | telemetry | metadata | billing | support | none
jurisdiction: string,
dpaSigned: boolean
}>,
incidentHistory: Array<{
date: string,
severity: "low" | "medium" | "high" | "critical",
scope: string,
resolution: string
}>,
certifications: {
soc2: { status: CertStatus, lastAudit?: string },
iso27001: { status: CertStatus, lastAudit?: string },
penTest: { status: CertStatus, lastAudit?: string, scope?: string }
}, // CertStatus = "none" | "in-progress" | "scheduled" | "type-i" | "type-ii" | "certified" | "completed"
compensatingControls: Array<{ gap: string, mitigation: string, owner: string }>
}
Success responses:
// 200 OK — accepted
{
accepted: true,
refusal: {
refused: false,
findings: Array<{
id: string, path: string,
description: string, remediation: string,
recoveredBy?: { gap: string, owner: string }
}>,
summary: string
},
receipt: {
hash: string,
verifyUrl: string, // path — prepend tarotscript-worker origin
claimsSchemaVersion: number
},
seed: number,
latencyMs: number
}
Error responses:
400— Invalid JSON or Zod validation error from TarotScript401— Unauthorized422— refused on unrecovered instant-disqualifier:{ refused: true, summary: string, findings: Array<{ id, path, description, remediation }> }429— scaffolds quota exceeded withupgrade: "/pricing"502— upstream TarotScript error
Source: src/pages/api/agents/ciso-intake.ts
Notes: Zod-validated on the TarotScript side (tarotscript#203). Consumes one scaffolds quota credit. Refusal runs server-side and is authoritative — client-side disqualifier detection in CisoConsultation.tsx is UX-only. Successful receipts can be registered to a Trust Page slug via POST /api/trust/slugs for public publication at trust.stackbilder.com/<slug>.
POST /api/agents/bootstrap
Purpose: Deck research — analyze domain gaps and assign research methodology tier.
Auth: Session cookie or Bearer ea_*
Request body:
{
role: string, // cto | ciso | cfo | cmo | cpo | architect
domain?: string, // optional domain or business context
internalSignals?: string[] // optional internal signal / pain data
}
Success responses:
200 OK—BootstrapResultTypewith methodology metadata (signalType, signalClass, gapType, gapClass, researchStrategy, researchType, evidenceBar, tierAssignment, methodologyConfidence)
Error responses:
400— “role is required”401— Unauthorized429— scaffolds quota exceeded502— upstream TarotScript error
Source: src/pages/api/agents/bootstrap.ts
Notes: Consumes one scaffolds quota credit.
GET /api/agents/:role/memory
Purpose: Query agent memory state (zone counts, entropy, recent cards) for a specific role.
Auth: Session cookie or Bearer ea_*
Path params:
role— cto | ciso | cfo | cmo | cpo | architect
Success responses:
200 OK—MemoryQueryResultType:{ role, exists, tick?, zones: { discard, deck, metaInsight, identity }, entropy: { shannonDiversity, elementCounts, depletionRatio, transCapacity, totalCards }, recentMemories?, updatedAt? }
Error responses:
400— “role is required”401— Unauthorized502— upstream TarotScript error
Source: src/pages/api/agents/[role]/memory.ts
Notes: Read-only; does NOT consume quota. Use to monitor agent memory saturation.
Evidence Engine
E-E-A-T content quality tooling. See AEGIS wiki evidence-engine-gap-fill-architecture for full architecture.
POST /api/v1/evidence/validate
Purpose: Run E-E-A-T validation against content. Deterministic (no LLM, no library access).
Auth: Session cookie or Bearer ea_*
Request body:
{
content: ContentInput | string, // required; bare string wraps to { body: string }
policyVersion?: "google_november_2024_reputation" // default
}
Max JSON payload: 128 KiB.
Success responses:
// 200 OK
{
validationId: string,
cached: boolean, // true if returned from 1-hour dedup cache
score: number, // 0-100, equal-weight active policy requirements satisfied
hasGaps: boolean,
gapCount: number,
policyVersion: string,
policyName: string,
gaps: Array<{
pillar: "Experience" | "Expertise" | "Authoritativeness" | "Trustworthiness",
type?: string,
description: string,
severity?: string
}>,
suggestions: string[]
}
Error responses:
400—{ error: "content is required" | "invalid_json" | "Unknown evidence policy version ..." }401— session invalid / tenant resolution failed413—{ error: "payload_too_large", limit: 131072 }429—{ error: "evidence_validation_quota_exceeded", upgrade: "/pricing" }
Notes: Dedup cache: identical (tenant, content, policy) within 1 hour returns cached validationId without consuming quota or writing a new audit row. Input hash is SHA-256 of normalized payload.
Source: src/pages/api/v1/evidence/validate.ts
POST /api/v1/evidence/gap-fill
Purpose: Iterative evidence gap-fill loop — validate → match library assets → merge → LLM revise → re-validate. Pro-gated.
Auth: Session cookie or Bearer ea_*; Pro/Team only (402 otherwise)
Request body:
{
content: ContentInput | string,
policyVersion?: string, // default "google_november_2024_reputation"
maxIterations?: number, // clamped 1–5, default 3
targetPillars?: EvidencePillar[],
assetPreferences?: {
domains?: EvidenceDomain[],
types?: EvidenceAssetType[]
}
}
Max JSON payload: 128 KiB.
Success responses:
// 200 OK
{
gapFillId: string,
iterations: number,
initialGaps: number,
finalGaps: number,
converged: boolean, // true iff finalGaps === 0
revised: ContentInput,
iterationLog: Array<{
iteration: number,
gapsBefore: number, gapsAfter: number,
assetsInjected: string[],
llmCallCostEstimate: number,
note?: string
}>,
creditsUsed: number, // USD, accrued from canonical llm-providers TokenUsage.inputTokens/outputTokens
gapFillHash: string, // SHA-256 of canonical projection bound into v2.2 receipts (#115)
bailReason?: "max_iterations" | "cost_ceiling" | "llm_bail" | "schema_drift" // omitted if converged
}
Error responses:
400—{ error: "content is required" | "invalid_json" | "empty_library" | ... }401— session invalid / tenant resolution failed402—{ error: "evidence_gap_fill_requires_pro", upgrade: "/pricing" }413—{ error: "payload_too_large", limit: 131072 }429— rate-limit (30/hr) ORevidence_gap_fill_quota_exceeded502— LLM provider misconfiguration, LLM failure, asset match DB failure, orgap_fill_persist_failed
Source: src/pages/api/v1/evidence/gap-fill.ts. Loop + persistence helpers live in src/lib/evidence-gap-fill.ts so /attest can run the same path with runGapFill: true.
Notes:
- Pro/team only (plan lookup via
user.orgId). - 30/hr per-tenant rate limit +
evidence_gap_fillsquota from edge-auth. - $0.25 cost ceiling per request; LLM model = Cerebras GLM-4.7.
- Fast-path: if initial validation finds 0 gaps, returns immediately with no LLM spend.
- Asset matching: pillar→type heuristic, ordered by
usage_count ASC, created_at DESC; up to 6 assets per iteration. - Persisted (#115): every successful run writes an
evidence_gap_fillsrow with the canonical iteration log +gap_fill_hash. The hash is the binding the v2.2 receipt commits to, and the row is whatgetGapFillForPublicRenderre-hashes for the trust-page render. - Calibration telemetry (#95 item 1): every run writes a summary row to
OBSERVE_DB.traceswithworker_name = "internal:evidence-gap-fill"andtrace_id = gapFillId.root_attrsJSON captures iterations, gap counts, bail reason, and credits used. Hidden from user-facing Observe endpoints viaINTERNAL_WORKER_SQL_FILTER. Fail-soft — telemetry errors are logged and swallowed. Per-iteration spans are deferred to item 6.
POST /api/v1/evidence/attest
Purpose: Composite “survived adversarial review” pipeline. One call runs initial validate → critique + revise → optional gap-fill polish → final validate → signed publish receipt. Pro/Team only.
Auth: Session cookie or Bearer ea_*; Pro/Team only (402 otherwise)
Request body:
{
content: ContentInput | string,
publicPayload: Record<string, unknown>, // tenant-authored projection returned by /verify
policyVersion?: string, // default "google_november_2024_reputation"
approvedPlan?: string[], // optional Collaborative Planning v2.1
runGapFill?: boolean, // #115 — opt into v2.2 polish + binding
gapFillOptions?: {
maxIterations?: number,
targetPillars?: EvidencePillar[],
assetPreferences?: { domains?: string[], types?: string[] }
}
}
Success response (201):
{
receiptHash: string, // 64-hex HMAC; lookup key on trust.stackbilder.com/evidence/:hash
verifyUrl: string, verifyJsonUrl: string,
receiptVersion: "2" | "2.1" | "2.2", // 2.2 when runGapFill bound; 2.1 with approvedPlan; 2 critique-only
policyVersion: string,
contentHash: string,
publishedAt: number,
revised: ContentInput, // post-pipeline content the receipt's HMAC binds
planHash?: string, // present on v2.1+
gapFillHash?: string, // present on v2.2 only
gapFillSkipped?: "empty_library" | "loop_failed", // runGapFill requested but stage skipped → falls through to v2.1/v2
pipelineSummary: {
initialGaps: number, finalGaps: number,
attacksLogged: number, revisedDiffersFromInput: boolean,
bailReasons: string[],
gapFill?: { iterations, initialGaps, finalGaps, assetsInjected, bailReason? }
},
creditsUsed: number // sum of critique + (optional) gap-fill USD
}
Error responses:
400—{ error: "content is required" | "invalid_public_payload" | "invalid_approved_plan" | "Unknown evidence policy version ..." }401— session invalid / tenant resolution failed402—{ error: "evidence_attest_pro_required", upgrade: "/pricing" }413—{ error: "payload_too_large" | "public_payload_too_large", limit: ... }429—{ error: "evidence_attest_rate_limited" }(20/hr) |evidence_validations_quota_exceeded|evidence_gap_fills_quota_exceeded409—{ error: "receipt_conflict" }(HMAC collision — vanishingly rare)502—{ error: "attest_failed", at: "initial_validate" | "critique" | "critique_persist" | "final_validate" | "validation_persist" | "publish_insert" }|attest_critique_bailed
Quota cost: 1 evidence_validations per call. With runGapFill: true, +1 evidence_gap_fills. Defense rate-limit is 20/hr per tenant; the LLM cost-multiplier of the polish pass is bundled into that allowance — no separate cap.
Receipt versions:
v2(DBreceipt_version=2) — critique onlyv2.1(DBreceipt_version=3) — addsplanHashfromapprovedPlanv2.2(DBreceipt_version=4, #115) — addsgapFillHash. The matchingevidence_gap_fillsrow stores the canonical iteration log;getGapFillForPublicRenderre-hashes and compares before the trust page renders the section.
Source: src/pages/api/v1/evidence/attest.ts. Receipt primitives in src/lib/evidence-receipts.ts; gap-fill loop in src/lib/evidence-gap-fill.ts.
GET /api/v1/evidence/library
Purpose: List tenant’s evidence assets with optional filtering.
Auth: Session cookie or Bearer ea_*
Query params:
type(optional) — one of:case_study|customer_quote|expert_quote|original_visual|proprietary_data|first_hand_experience|technical_diagram|research_findingdomain(optional) — one of:medical|legal|financial|technology|marketing|business|education|health_wellness|food|food_science|food_regulation|ecommerce|real_estate|automotive|generallimit(optional) — 1–100, default 20cursor(optional) — opaque pagination cursor from priornextCursorresponse
Success responses:
// 200 OK
{
assets: AssetResponse[], // id, type, title, content, domains[], tags[], qualityScore, verificationStatus, authorName, authorBio, sourceUrl, usageCount, createdAt, updatedAt
nextCursor?: string // present if more results
}
Error responses:
400— invalid type/domain enum or malformed cursor401— session invalid / tenant resolution failed
Source: src/pages/api/v1/evidence/library/index.ts
Notes: Tenant-scoped (per-resource scoping, user.tenantId || user.orgId || user.id). Cursor is base64-encoded (createdAt, id) tuple, DESC order.
POST /api/v1/evidence/library
Purpose: Create a new evidence asset.
Auth: Session cookie or Bearer ea_*; tier-gated (Free: 0 cap, Pro/Team: 50 cap)
Request body:
{
type: EvidenceAssetType, // required
title: string, // required, 10–200 chars
content: Record<string, unknown>, // required, opaque JSON
domains: EvidenceDomain[], // required, non-empty
tags?: string[],
qualityScore?: number | null, // 0–100 or null
verificationStatus?: "verified" | "unverified" | "pending" | "disputed",
authorName?: string,
authorBio?: string,
sourceUrl?: string
}
Max JSON payload: 128 KiB.
Success responses:
201 Created—{ asset: AssetResponse }
Error responses:
400—{ error: "invalid_json" | "invalid_input", errors?: [{ field, message }] }401— session invalid / tenant resolution failed402—{ error: "library_limit", tier, limit, current, upgrade: "/pricing" }413—{ error: "payload_too_large", limit: 131072 }502— D1 batch insert failure
Notes: Post-insert capacity enforcement via atomic D1 batch — INSERT + COUNT + optional compensating DELETE. Safe for concurrent writers at cap boundary.
Source: src/pages/api/v1/evidence/library/index.ts
GET /api/v1/evidence/library/:id
Purpose: Retrieve a single evidence asset.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { asset: AssetResponse }
Error responses: 400 invalid id | 401 | 404 (or cross-tenant — same response to avoid leaking existence)
Source: src/pages/api/v1/evidence/library/[id].ts
PUT /api/v1/evidence/library/:id
Purpose: Partial or full update of an evidence asset.
Auth: Session cookie or Bearer ea_*
Request body: Same shape as POST, all fields optional. Max JSON payload: 128 KiB.
Success responses: 200 OK — { asset: AssetResponse }
Error responses: 400 invalid input | 401 | 404 | 413 payload too large | 502 batch update failure
Notes: If domains change, join table is re-synced atomically. updated_at always refreshes.
Source: src/pages/api/v1/evidence/library/[id].ts
DELETE /api/v1/evidence/library/:id
Purpose: Remove an evidence asset and its domain join rows.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { id, deleted: true }
Error responses: 400 | 401 | 404 | 502
Source: src/pages/api/v1/evidence/library/[id].ts
Trust Page
Slug→hash admin API that backs trust.stackbilder.com/<slug>. Ownership is org-level per split-by-policy contract. See AEGIS wiki trust-page-governance-timeline-architecture and trust-bundle-storage-architecture.
GET /api/trust/slugs
Purpose: List all trust slugs owned by the caller’s org.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
tenantId: string, // historical name; semantically the orgId
slugs: Array<{
slug: string,
current_hash: string, // 64-char lowercase hex
tenant_id: string,
created_at: string,
updated_at: string
}>
}
Notes: Cross-tenant listing not supported.
Source: src/pages/api/trust/slugs/index.ts
POST /api/trust/slugs
Purpose: Create or update a slug → receipt-hash mapping.
Auth: Session cookie or Bearer ea_*
Request body:
{
slug: string, // 1–64 lowercase alphanumeric chars, optional interior hyphens
hash: string // 64-char lowercase hex (SHA-256 receipt hash)
}
Success responses:
200 OK— updated existing slug; returns row201 Created— created new slug; returns row
Error responses:
400— malformed body, invalid slug format, or invalid hash format401— no auth409—{ error: "...", code: "cross_tenant_conflict" }— slug owned by a different org
Example:
curl -X POST https://stackbilder.com/api/trust/slugs \
-H "Authorization: Bearer ea_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"acme-corp","hash":"<64-char hex>"}'
Source: src/pages/api/trust/slugs/index.ts
Notes: Slug and hash normalized to lowercase. First-come, first-owned at the org level. Idempotent: same org re-POSTing the same slug updates current_hash + updated_at.
GET /api/trust/slugs/:slug
Purpose: Fetch a single slug row if the caller’s org owns it.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — single slug row
Error responses: 400 invalid slug | 401 | 404 (not found OR owned by another org — no existence-leak)
Source: src/pages/api/trust/slugs/[slug].ts
DELETE /api/trust/slugs/:slug
Purpose: Remove a slug mapping.
Auth: Session cookie or Bearer ea_*
Success responses: 204 No Content
Error responses:
400invalid slug401no auth403{ error, code: "cross_tenant_forbidden" }— exists but owned elsewhere (explicit permission signal)404slug doesn’t exist at all
Source: src/pages/api/trust/slugs/[slug].ts
GET /api/trust/bundle/:hash
Purpose: List Trust Bundle artifact metadata for a receipt hash (bytes not included).
Auth: Session cookie or Bearer ea_*
Path params: hash — 64-char lowercase hex
Success responses:
// 200 OK
{
receipt_hash: string,
artifacts: Array<{
receipt_hash, artifact_slug, tenant_id,
content_hash, // sha256 of the artifact bytes
mime_type, byte_size,
display_name, excerpt: string | null,
signed: 0 | 1,
created_at: string
}>,
count: number
}
Notes: Tenant-scoped. Cross-tenant receipts return an empty list (not 403).
Source: src/pages/api/trust/bundle/[hash]/index.ts
GET /api/trust/bundle/:hash/:slug
Purpose: Fetch a single artifact’s manifest row (metadata only).
Auth: Session cookie or Bearer ea_*
Path params: hash + artifact slug
Success responses: 200 OK — single BundleArtifactRow
Error responses: 400 invalid | 401 | 404 (not found or cross-tenant)
Notes: Artifact bytes are served from the public route /trust/:slug/bundle/:artifact_slug (not the admin API).
Source: src/pages/api/trust/bundle/[hash]/[slug].ts
POST /api/trust/bundle/:hash/:slug
Purpose: Upload or overwrite an artifact for a Trust Bundle.
Auth: Session cookie or Bearer ea_*
Path params: hash + artifact slug
Request headers (required):
Content-Type— real MIME type of the artifact. Allowed:application/pdf, JSON (application/json,application/ld+json,application/schema+json), text (text/plain,text/markdown,text/csv), zip/xlsx, or legacyapplication/vnd.ms-excel.X-Display-Name— human-readable artifact name, max 120 chars.
Request headers (optional):
X-Excerpt— one-paragraph card summary, max 500 chars.X-Signed: true— artifact embeds its own signature (e.g. signed PDF).
Request body: Raw artifact bytes, max 10 MiB. The API validates file signatures for PDFs, zip/xlsx, legacy XLS, JSON, and text-like formats before writing to R2.
Success responses:
200 OK— updated —{ row, content_hash, created: false }201 Created— created —{ row, content_hash, created: true }
Error responses:
400— invalid hash, invalid slug, missing/empty body, missing/oversizedX-Display-Name, oversizedX-Excerpt, or invalidContent-Type401— no auth413— artifact exceeds 10 MiB409—{ error, code: "cross_tenant_conflict" }415— unsupported MIME type or declared type does not match artifact bytes
Known artifact slugs (informational; not enforced): posture-overview, stride-threat-model, data-flow, ir-plan, shared-responsibility, subprocessors, caiq-lite, oscal-json.
Notes: R2 write occurs first (keyed <hash>/<slug>), then D1. Safe to retry — R2 puts are idempotent per key. content_hash is threaded into tarotscript receipt signing to enable tamper detection.
Source: src/pages/api/trust/bundle/[hash]/[slug].ts
DELETE /api/trust/bundle/:hash/:slug
Purpose: Remove an artifact — deletes R2 object, then D1 manifest row.
Auth: Session cookie or Bearer ea_*
Success responses: 204 No Content
Error responses:
403— artifact exists, owned by another tenant404— not found (or invalid hash/slug format)
Source: src/pages/api/trust/bundle/[hash]/[slug].ts
Scaffolder (Flows)
Stackbilder’s original product — deterministic governed scaffold output optionally polished with LLM Oracle.
POST /api/flows
Purpose: Create a new scaffold — classify intent, run scaffold-cast spread, optionally run Oracle polish.
Auth: Session cookie or Bearer ea_*
Request body:
{
intention: string, // required, non-empty
project_type?: string, // default "api"
oracle?: boolean // true to run Oracle LLM polish (Pro/Team only)
}
Success responses:
// 201 Created
{
id: string, // receipt hash
classification: { pattern: string, confidence: string },
traits: Record<string, string>,
tier2_recommended: boolean,
output: string,
facts: Record<string, unknown>
}
Error responses: 400 | 401 | 429 quota exceeded with upgrade: "/pricing" | 502
Notes: Consumes one scaffolds quota credit. Oracle runs only if body.oracle === true AND tier is pro/team. Oracle result persists to SESSION KV with 90-day TTL under key oracle:<id>.
Source: src/pages/api/flows/index.ts
GET /api/flows
Purpose: List the user’s scaffolds from the TarotScript grimoire.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
flows: Array<{
id: string, intention: string,
project_type: string,
status: "completed",
created_at: string
}>
}
Notes: Filters grimoire by spreadType: "ScaffoldCast" | "scaffold-cast". Gracefully returns empty array on upstream error.
Source: src/pages/api/flows/index.ts
GET /api/flows/:id
Purpose: Full scaffold detail — governance, files, next steps, Oracle status.
Auth: Session cookie or Bearer ea_*
Path params: id — receipt hash
Success responses:
// 200 OK
{
id, intention, project_type, status, created_at, seed,
governance: { threat_model, adr, test_plan },
scaffold: { files: Array<{ path, content, role }> },
next_steps: string[],
output,
oracle_enhanced: boolean,
oracle_summary: string
}
File role values: scaffold | config | governance | test | doc.
Error responses: 401 | 404 | 502
Source: src/pages/api/flows/[id].ts
GET /api/flows/:id/oracle
Purpose: Retrieve persisted Oracle enhancement status/result for an existing scaffold.
Auth: Session cookie or Bearer ea_*
Path params: id — receipt hash
Success responses:
// 200 OK, not enhanced yet
{ enhanced: false }
// 200 OK, enhanced result persisted
{ enhanced: true, files, summary }
Error responses:
401— Unauthorized
Source: src/pages/api/flows/[id]/oracle.ts
Notes: Reads SESSION KV key oracle:<id>. Does not enforce Pro/Team; it only reports whether an already-persisted result exists.
POST /api/flows/:id/oracle
Purpose: Trigger LLM Oracle polish on an existing scaffold; persist enhanced files to KV.
Auth: Session cookie or Bearer ea_*; Pro/Team only
Path params: id — receipt hash
Success responses:
200 OK—{ enhanced: true, files, summary }
Error responses:
401— Unauthorized403—{ error: "Oracle requires a Pro subscription", upgrade_url: "/pricing" }404— flow not found422— flow has no prompt context (predates Oracle support)502— TarotScript or Oracle service error
Source: src/pages/api/flows/[id]/oracle.ts
Notes: Persists result to SESSION KV at oracle:<id> with 90-day TTL. Idempotent — returns cached result if already enhanced.
Images
GET /api/images
Purpose: List user’s generated image jobs.
Auth: Session cookie or Bearer ea_*
Query params:
limit(default 20),offset(default 0),state(optional filter, e.g.completed,pending)
Success responses:
// 200 OK
{ images: Array<{ id, prompt, quality_tier, status, image_url, created_at }> }
Source: src/pages/api/images/index.ts
POST /api/images/generate
Purpose: Create a new image generation job.
Auth: Session cookie or Bearer ea_*
Request body:
{
prompt: string, // required
quality_tier?: string, // default "standard"
negative_prompt?: string
}
Success responses:
// 201 Created
{ id, job_id, state, original_prompt, final_prompt, asset_url, created_at }
Error responses: 400 | 401 | 429 images quota exceeded with upgrade: "/pricing" | 502
Notes: Consumes one images quota credit.
Source: src/pages/api/images/generate.ts
GET /api/images/:id
Purpose: Retrieve single image job status.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { id, prompt, quality_tier, status, image_url, error, created_at }
Source: src/pages/api/images/[id].ts
DELETE /api/images/:id
Purpose: Delete an image job (cascades to img-forge asset cleanup).
Auth: Session cookie or Bearer ea_*
Success responses: 204 No Content
Error responses: 401 | 404 | 409 conflict (e.g. still processing) | 502
Source: src/pages/api/images/[id].ts
Worker observability
Pro feature. See CLAUDE.md “Worker Observability” section for full COGS-containment architecture.
Internal telemetry filter. All user-facing read endpoints in this section (/summary, /traces, /traces/:id) apply INTERNAL_WORKER_SQL_FILTER from src/lib/observe.ts — worker_name NOT LIKE 'internal:%'. stackbilt-web writes internal calibration telemetry (e.g. evidence gap-fill runs per #95) directly to OBSERVE_DB using the internal:* prefix so those rows never surface as phantom workers in a tenant’s Observe UI. If you add a new user-facing query that reads traces/spans/logs by worker_name, apply this filter.
POST /api/observe/ingest
Purpose: Unified batch telemetry ingest — metrics, spans, logs, alerts — from @stackbilt/worker-observability consumers.
Auth: Session cookie or Bearer ea_*; Pro-gated
Request body:
{
service: string, // worker name (required)
metrics?: MetricPoint[], // { name, value, unit, timestamp, tags, type: "counter" | "gauge" | "histogram" | "summary" }
spans?: TraceSpan[], // { traceId, spanId, operationName, service, startTime, endTime, duration, status, attributes, events }
logs?: LogEntry[], // { timestamp, level: "debug" | "info" | "warn" | "error" | "fatal", message, context, error, metadata }
alerts?: AlertIncident[] // { id, rule: { name, severity }, status: "firing" | "resolved", startTime, endTime, value, message, metadata }
}
Success responses: 202 Accepted — { ok: true }
Error responses:
400— missingservicefield, empty payload, or invalid JSON401— no auth413— payload exceeds 64 KB403—{ error, tier, limit, upgrade }— worker count cap reached for tier (Free 1 / Pro 5 / Team 20)429—{ error, tier, dailyLimit, used, upgrade, retryable: false }— daily event budget exhausted (Free 10K / Pro 500K / Team 2M per worker per day)502— DB failure
Notes: Retention: Free 24h / Pro 30d / Team 30d. Writes to OBSERVE_DB D1. Root spans (no parentSpanId) upserted to traces; all spans inserted to spans. All records include expires_at for TTL cleanup (hourly cron).
Source: src/pages/api/observe/ingest.ts
GET /api/observe/summary
Purpose: Per-worker summary stats (error rate, p95 latency, request count) over a time range.
Auth: Session cookie or Bearer ea_*
Query params:
range—1h|6h|24h(default) |7d|30dworker(optional) — filter to specific worker name
Success responses:
// 200 OK
{
workers: Array<{
worker_name, total_traces, error_count, error_rate,
avg_duration_ms, p95_duration_ms, last_seen
}>,
range: string
}
Notes: p95 computed client-side (D1 lacks PERCENTILE_CONT).
Source: src/pages/api/observe/summary.ts
GET /api/observe/traces
Purpose: Paginated trace list with filters.
Auth: Session cookie or Bearer ea_*
Query params:
range— time window (default24h)worker(optional) — filter by workerstatus(optional) —ok|errorlimit(default 50, max 200),offset(default 0)
Success responses:
// 200 OK
{
traces: Array<{ trace_id, worker_name, status, started_at, finished_at, duration_ms, error_msg }>,
total: number
}
Source: src/pages/api/observe/traces/index.ts
GET /api/observe/traces/:id
Purpose: Full trace detail with correlated spans + logs.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
trace: { ... },
spans: Array<{ span_id, trace_id, parent_span_id, operation, service, start_time, end_time, duration_ms, status, attributes, events }>,
logs: Array<{ id, level, message, ts, error }>
}
Error responses: 400 missing id | 401 | 404
Source: src/pages/api/observe/traces/[id].ts
GET /api/observe/alerts
Purpose: List fired and resolved alert incidents.
Auth: Session cookie or Bearer ea_*
Query params:
status(optional) —firing|resolved; omit for all
Success responses:
// 200 OK
{
alerts: Array<{
id, worker_name, rule_name, severity, status,
message, value, started_at, ended_at
}>
}
Notes: ended_at is null for active alerts. Returns up to 100 rows ordered by started_at DESC.
Source: src/pages/api/observe/alerts.ts
Billing
⚠️ Live-mode Stripe (account acct_1T8cxHL8cDQ0gdtT) since 2026-04-11. All billing routes hit real customers.
POST /api/billing/checkout
Purpose: Create a Stripe Checkout session. Two modes: legacy Pro tier, or SKU (CISO Trust Bundle / Hosting, CTO Consultation).
Auth: Session cookie or Bearer ea_*
Request body:
// Empty body → legacy Pro flow (uses STRIPE_PRO_PRICE_ID)
{}
// OR SKU flow:
{ productKey: "ciso-trust-bundle" | "ciso-trust-page-hosting" | "cto-consultation" }
Success responses: 200 OK — { url: string } (Stripe Checkout session URL)
Error responses:
400— invalid productKey, returnsaccepted: [...]401— Unauthorized409—{ error, code: "already_subscribed", currentTier }(legacy Pro pre-flight only)429—{ error: "Too many checkout attempts" }(10/hr)502— upstream Stripe/edge-auth error
SKU catalog (as of 2026-04-19):
| productKey | Price | Mode |
|---|---|---|
ciso-trust-bundle | $499 one-time | payment |
ciso-trust-page-hosting | $149/mo | subscription |
cto-consultation | gated / not priced | — |
CISO SKUs are invite-gated at the edge-auth layer (server-side CISO_INVITE_ALLOWLIST_ORGS secret).
Source: src/pages/api/billing/checkout.ts
POST /api/billing/portal
Purpose: Create a Stripe Billing Portal session for subscription management.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { url: string }
Error responses:
401— Unauthorized422—{ error, code: "no_stripe_customer" }— no Stripe customer (admin override, comp account, or lapsed sub)502— upstream error
Source: src/pages/api/billing/portal.ts
POST /api/billing/downgrade
Purpose: Downgrade tenant to free tier. Handles active subscription (cancel at period end), admin override, or past-due.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
ok: boolean,
previousTier: "free" | "pro" | "team",
stripeAction: "canceled" | "no_subscription" | "already_canceled",
effectiveAt: string,
noop: boolean
}
canceled— active Stripe sub scheduled for cancellation; tier stays untileffectiveAtno_subscription— admin override or comp account; immediate tier flipalready_canceled— reconciling a danglingstripe_subscription_id; immediate flip
Error responses:
400—"You're already on the free plan."(short-circuit when tier is already free)401— Unauthorized403—{ error: "Forbidden: ..." }— ownership/role denial from edge-auth429— 5/hr rate limit502— upstream error
Source: src/pages/api/billing/downgrade.ts
Notes: Idempotent. Safe to call repeatedly.
GET /api/billing/promos
Purpose: List all promotion codes (admin endpoint).
Auth: Session cookie
Success responses: 200 OK — { codes: Array<{ id, code, percentOff, duration, maxRedemptions, timesRedeemed, active }> }
Source: src/pages/api/billing/promos.ts
POST /api/billing/promos
Purpose: Create a new promotion code.
Auth: Session cookie
Request body:
{
code: string,
percentOff: number, // 1–100
duration: "once" | "repeating" | "forever",
durationInMonths?: number,
maxRedemptions?: number
}
Success responses: 201 Created — { id, code, couponId }
Error responses: 400 invalid | 401 | 502
Notes: Code is uppercased on submission.
Source: src/pages/api/billing/promos.ts
DELETE /api/billing/promos/:id
Purpose: Revoke a promo code.
Auth: Session cookie
Success responses: 204 No Content
Error responses: 401 | 502
Source: src/pages/api/billing/promos/[id].ts
Marketing & support
POST /api/scaffold/preview
Purpose: Anonymous scaffold preview (Phase 1 only — no LLM, deterministic). Used by the public homepage hero.
Auth: None (rate-limited by IP)
Request body:
{ intention: string }
Success responses:
// 200 OK
{
classification: { pattern: string, confidence: string },
traits: Record<string, string>,
tier2_recommended: boolean,
governance: { threat_model, adr, test_plan },
files: string[]
}
Error responses: 400 missing/empty intention | 429 10/min per IP | 502 upstream TarotScript
Source: src/pages/api/scaffold/preview.ts
POST /api/contact
Purpose: Public contact form → Resend email to admin@stackbilt.dev.
Auth: None (rate-limited)
Request body: { email: string, message: string (10+ chars) }
Success responses: 200 OK — { ok: true }
Error responses: 400 invalid | 429 3/hr per IP | 502 Resend error | 503 Resend not configured
Source: src/pages/api/contact.ts
POST /api/subscribe
Purpose: Newsletter signup → Resend audience.
Auth: None (rate-limited)
Request body: { email: string }
Success responses: 200 OK — { ok: true }
Error responses: 400 invalid email | 429 5/hr per IP | 502 Resend error
Source: src/pages/api/subscribe.ts
Notes: Gracefully accepts signup even if Resend not configured (returns 200). Sends welcome email to subscriber + admin notification.
POST /api/support/tickets
Purpose: In-app bug/support ticket. Auto-triaged via TarotScript triage-cast spread.
Auth: Session cookie
Request body:
{
message: string, // 10–5000 chars
url?: string,
userAgent?: string
}
Success responses: 200 OK — { ok: true, triage: { category?, urgency? } }
Error responses: 400 invalid | 401 | 429 5/hr per user | 502 Resend delivery failed
Source: src/pages/api/support/tickets.ts
Notes: TarotScript classifies category, urgency, sentiment, complexity, and escalation flag. Email sent to admin@stackbilt.dev with full classification. Best-effort — delivers ticket even if classification fails.
Telemetry
POST /api/csp-report
Purpose: Public CSP violation report endpoint. Logs to console, persists to OBSERVE_DB with 30-day retention.
Auth: None
Request body: Browser-standard CSP report shape (document-uri, violated-directive, etc.)
Success responses: Always 204 No Content (swallows all errors)
Notes: Visible via wrangler tail. Filters reports missing document-uri or violated-directive (ignores scanner garbage).
Source: src/pages/api/csp-report.ts
POST /api/errors
Purpose: Client-side error telemetry (from React ErrorBoundary components) → Worker console.
Auth: None
Request body: { type?, component?, message?, url?, timestamp? }
Success responses: 204 No Content
Error responses: 413 payload exceeds 8 KB
Notes: Truncates message and url to 500 chars.
Source: src/pages/api/errors.ts
MCP server
POST /api/mcp / GET /api/mcp
Purpose: JSON-RPC 2.0 MCP server exposing admin Cloudflare tools (analytics, inventory, cost forecasting).
Auth:
POST— Session cookie orBearer ea_*, admin-onlyGET— Public (tool discovery)
POST request body:
{
jsonrpc: "2.0",
id: number | string | null,
method: "initialize" | "tools/list" | "tools/call" | "ping" | "notifications/initialized",
params?: {
name?: string, // for tools/call
arguments?: Record<string, unknown> // for tools/call
}
}
POST success:
{
jsonrpc: "2.0",
id: ...,
result: { /* method-dependent; tools/call returns { content: [{ type: "text", text }], isError? } */ }
}
JSON-RPC error codes:
-32700parse error |-32600invalid request |-32601method not found |-32602invalid params |-32000unauthorized
Tools available: cf_worker_analytics, cf_resource_inventory, cf_cost_estimate, cf_cost_forecast
Rate limits: 10/min (analytics, inventory, estimate); 5/min (forecast)
Source: src/pages/api/mcp/index.ts
Notes: Server info — name=stackbilt-cf-admin, version=0.1.0, protocolVersion=2024-11-05. Each tool backed by 5-min KV cache to bound CF API cost.
Admin
All admin routes require a session with an admin user (admin@stackbilt.dev or equivalent). Admin check lives in src/lib/auth.ts::isAdmin.
GET /api/admin/cf/inventory
Purpose: List all Cloudflare resources (Workers, D1, R2, KV) owned by the configured account.
Auth: Admin-only
Success responses:
// 200 OK
{
configured: true,
inventory: {
workers: string[],
d1Databases: Array<{ name, uuid }>,
r2Buckets: string[],
kvNamespaces: Array<{ title, id }>,
counts: { workers, d1, r2, kv }
}
}
Graceful error: 200 OK { configured: false, message, inventory: null } when CF integration not configured.
Error responses: 401 | 403 not admin | 502 CF API error
Notes: Env vars: CF_ANALYTICS_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN (falls back to CF_ANALYTICS_TOKEN). Uses 4 parallel CF REST calls. KV-backed cache, 5-min TTL.
Source: src/pages/api/admin/cf/inventory.ts
GET /api/admin/cf/cost-estimate
Purpose: Estimate monthly Cloudflare cost from a 1–30 day worker-request sample.
Auth: Admin-only
Query params: days (optional, 1–30, default 7)
Success responses:
// 200 OK
{
configured: true,
estimate: {
period: string,
dailyRequests, monthlyRequests,
freeTier: Array<{ resource, used, limit, unit, pctUsed, overage, estimatedCost }>,
totalEstimatedMonthlyCost: number,
note: string
}
}
Source: src/pages/api/admin/cf/cost-estimate.ts
Notes: Only Worker Requests wired up; D1/R2/KV cost constants exist but CF billing API returns 403. 5-min KV cache per (accountId, days).
GET /api/admin/cf/forecast
Purpose: Cost forecast over 7/14/30-day rolling windows.
Auth: Admin-only
Success responses: 200 OK — { configured: true, forecast: { window7d, window14d, window30d } }
Notes: Fan-out to 3 GraphQL calls — 5-min internal KV cache prevents repeat work. MCP-layer rate limit is 5/min (not 10) due to expense.
Source: src/pages/api/admin/cf/forecast.ts
GET /api/admin/cf/worker-analytics
Purpose: Top 50 Workers by request volume over 1–30 days, with error counts and CPU time.
Auth: Admin-only
Query params: days (optional, 1–30, default 7)
Success responses:
// 200 OK
{
configured: true,
days: number,
workers: Array<{ scriptName, requests, errors, cpuTimeMs }>
}
Source: src/pages/api/admin/cf/worker-analytics.ts
Notes: GraphQL workersInvocationsAdaptive, filtered by datetime_gt, ordered by sum_requests_DESC. 5-min KV cache.
GET /api/admin/export-scaffolds
Purpose: Bulk export scaffold flow data (grimoire readings + receipts + Oracle enhancements) for audit and recursive improvement.
Auth: Admin-only (session cookie or ea_*)
Success responses:
// 200 OK
// Content-Disposition: attachment; filename="scaffold-export-YYYY-MM-DD.json"
{
count: number,
scaffolds: Array<{
id, intention, classification, projectType,
governance: { threatModel, adr, testPlan },
files: Array<{ path, content, role }>,
oracleEnhanced: boolean,
oracleFiles: [...],
oracleSummary,
seed, createdAt
}>
}
Error responses: 401 | 403 not admin | 500 upstream | 502 KV read error
Source: src/pages/api/admin/export-scaffolds.ts
Notes: Iterates all scaffolds in tarotscript grimoire (/grimoire/<userId>?limit=100), fetches full receipts, merges Oracle data from SESSION KV under oracle:<hash>. Uses Promise.allSettled — skips failed receipt fetches.
Appendix
Related docs
CLAUDE.md— canonical repo-level context (bindings, security headers, auth, billing, observability architecture)- AEGIS wiki
consult-harness-web-surface— architecture for/app/consult/*+ agents routes - AEGIS wiki
evidence-engine-gap-fill-architecture— architecture for/api/v1/evidence/* - AEGIS wiki
trust-page-governance-timeline-architecture— CodeBeast/decideseam for/trust/* - AEGIS wiki
trust-bundle-storage-architecture— R2 + D1 flow for/api/trust/bundle/*