Platform / 07 API Reference

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:

ModeHowUse
Session cookiebetter-auth.session_token (dev) or __Secure-better-auth.session_token (prod)Web UI from a logged-in browser
API keyAuthorization: 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 subscription
  • cross_tenant_conflict — 409 on trust/slug or bundle write when another tenant owns it
  • cross_tenant_forbidden — 403 on delete when another tenant owns it
  • library_limit — 402 on Evidence library when at tier cap
  • no_stripe_customer — 422 on billing portal when no customer exists
  • evidence_gap_fill_requires_pro — 402 when tier isn’t pro/team
  • evidence_gap_fill_rate_limited — 429 on 30/hr defense rate limit
  • evidence_gap_fill_quota_exceeded — 429 on edge-auth quota
  • empty_library — 400 on gap-fill when no assets exist for tenant

Rate limits

Route familyLimitSource
/api/billing/checkout10/hr per userstackbilt-web
/api/billing/downgrade5/hr per userstackbilt-web
/api/v1/evidence/gap-fill30/hr per tenantstackbilt-web (defense layer)
/api/v1/evidence/gap-fillevidence_gap_fills quotaedge-auth
/api/agents/*scaffolds quotaedge-auth
/api/flows/*scaffolds quotaedge-auth
/api/images/generateimages quotaedge-auth
/api/contact3/hr per IPstackbilt-web
/api/subscribe5/hr per IPstackbilt-web
/api/support/tickets5/hr per userstackbilt-web
/api/scaffold/preview10/min per IPstackbilt-web
/api/mcp tools5–10/min per isolatestackbilt-web

Service bindings

The worker uses these Cloudflare service bindings to reach sibling workers (RPC / Fetcher over internal network):

BindingTargetPurpose
AUTH_SERVICEedge-authSessions, API keys, entitlements, quotas, Stripe integration
TAROTSCRIPTtarotscript-workerAgent consultations, scaffold harness, receipt signing
IMG_FORGEimg-forge-gatewayImage generation
CODEBEASTcodebeast/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 service
  • redirect (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 chars
  • 401 — not authenticated
  • 403 — 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 empty
  • 401 — 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 OKAgentRunResultType from @stackbilt/contracts/agent-response (or its stackbilt-web mirror at src/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 — Unauthorized
  • 429{ error, upgrade: "/pricing" } — scaffolds quota exceeded
  • 502 — 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 TarotScript
  • 401 — Unauthorized
  • 422 — refused on unrecovered instant-disqualifier:
    { refused: true, summary: string, findings: Array<{ id, path, description, remediation }> }
  • 429 — scaffolds quota exceeded with upgrade: "/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 OKBootstrapResultType with methodology metadata (signalType, signalClass, gapType, gapClass, researchStrategy, researchType, evidenceBar, tierAssignment, methodologyConfidence)

Error responses:

  • 400 — “role is required”
  • 401 — Unauthorized
  • 429 — scaffolds quota exceeded
  • 502 — 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 OKMemoryQueryResultType: { role, exists, tick?, zones: { discard, deck, metaInsight, identity }, entropy: { shannonDiversity, elementCounts, depletionRatio, transCapacity, totalCards }, recentMemories?, updatedAt? }

Error responses:

  • 400 — “role is required”
  • 401 — Unauthorized
  • 502 — 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 failed
  • 413{ 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 failed
  • 402{ error: "evidence_gap_fill_requires_pro", upgrade: "/pricing" }
  • 413{ error: "payload_too_large", limit: 131072 }
  • 429 — rate-limit (30/hr) OR evidence_gap_fill_quota_exceeded
  • 502 — LLM provider misconfiguration, LLM failure, asset match DB failure, or gap_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_fills quota 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_fills row with the canonical iteration log + gap_fill_hash. The hash is the binding the v2.2 receipt commits to, and the row is what getGapFillForPublicRender re-hashes for the trust-page render.
  • Calibration telemetry (#95 item 1): every run writes a summary row to OBSERVE_DB.traces with worker_name = "internal:evidence-gap-fill" and trace_id = gapFillId. root_attrs JSON captures iterations, gap counts, bail reason, and credits used. Hidden from user-facing Observe endpoints via INTERNAL_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 failed
  • 402{ 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_exceeded
  • 409{ 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 (DB receipt_version=2) — critique only
  • v2.1 (DB receipt_version=3) — adds planHash from approvedPlan
  • v2.2 (DB receipt_version=4, #115) — adds gapFillHash. The matching evidence_gap_fills row stores the canonical iteration log; getGapFillForPublicRender re-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_finding
  • domain (optional) — one of: medical | legal | financial | technology | marketing | business | education | health_wellness | food | food_science | food_regulation | ecommerce | real_estate | automotive | general
  • limit (optional) — 1–100, default 20
  • cursor (optional) — opaque pagination cursor from prior nextCursor response

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 cursor
  • 401 — 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 failed
  • 402{ 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 row
  • 201 Created — created new slug; returns row

Error responses:

  • 400 — malformed body, invalid slug format, or invalid hash format
  • 401 — no auth
  • 409{ 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:

  • 400 invalid slug
  • 401 no auth
  • 403 { error, code: "cross_tenant_forbidden" } — exists but owned elsewhere (explicit permission signal)
  • 404 slug 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 legacy application/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/oversized X-Display-Name, oversized X-Excerpt, or invalid Content-Type
  • 401 — no auth
  • 413 — artifact exceeds 10 MiB
  • 409{ 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 tenant
  • 404 — 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 — Unauthorized
  • 403{ error: "Oracle requires a Pro subscription", upgrade_url: "/pricing" }
  • 404 — flow not found
  • 422 — 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.tsworker_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 — missing service field, empty payload, or invalid JSON
  • 401 — no auth
  • 413 — payload exceeds 64 KB
  • 403{ 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:

  • range1h | 6h | 24h (default) | 7d | 30d
  • worker (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 (default 24h)
  • worker (optional) — filter by worker
  • status (optional) — ok | error
  • limit (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, returns accepted: [...]
  • 401 — Unauthorized
  • 409{ 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):

productKeyPriceMode
ciso-trust-bundle$499 one-timepayment
ciso-trust-page-hosting$149/mosubscription
cto-consultationgated / 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 — Unauthorized
  • 422{ 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 until effectiveAt
  • no_subscription — admin override or comp account; immediate tier flip
  • already_canceled — reconciling a dangling stripe_subscription_id; immediate flip

Error responses:

  • 400"You're already on the free plan." (short-circuit when tier is already free)
  • 401 — Unauthorized
  • 403{ error: "Forbidden: ..." } — ownership/role denial from edge-auth
  • 429 — 5/hr rate limit
  • 502 — 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 or Bearer ea_*, admin-only
  • GET — 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:

  • -32700 parse error | -32600 invalid request | -32601 method not found | -32602 invalid params | -32000 unauthorized

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

  • 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 /decide seam for /trust/*
  • AEGIS wiki trust-bundle-storage-architecture — R2 + D1 flow for /api/trust/bundle/*