Last updated: 2026-10-12
The public HTTP surface of the coordinator, derived from its composed route bindings under coordinator/api/, including the /v1/ catch-all. Every route is listed below with its handler symbol, authentication requirement, and rate-limit bucket; the second half of the page gives the wire shapes, headers, error table, SSE framing, limits, timeouts, and version-gate semantics that those routes share. For why the pipeline is built this way see ../architecture/components/consumer.md; for the crypto model behind sealed transport see ../architecture/security/encryption.md.
Production base URL: https://api.darkbloom.dev. Route composition stays in
coordinator/api; handlers live with the access, inference, provider, catalog,
release, account, billing, reporting and operations owners described in the
code navigation guide. Package boundaries do not
change HTTP paths, middleware ordering, status codes or wire shapes.
Offline caller conformance is exercised through the real handler by
coordinator/tests/internal/conformance/, including concrete-provider-model to
public-alias rewriting. See the conformance test entry points.
The public model catalog optionally includes hugging_face_artifact for direct
provider downloads; the admin registration accepts the same object. See the
registry artifact contract.
Admin request-profile records expose additive prediction decision fields. Public inference responses and error codes are unchanged.
GET /v1/me/providers reports the effective serving model set. Cached
Autopilot planning candidates stay out of its models array while control is
waiting or observing; enrollment alone does not change the operator's selected
models. This uses Provider.ServingModelsLocked in
coordinator/registry/autopilot_inventory.go; see
model Autopilot.
The coordinator accepts top-level user, generic metadata, safety_identifier
and caller prompt_cache_key on the four inference endpoints but removes them from provider-bound bodies before encryption.
The shared parseInferencePrelude (coordinator/api/inference/prelude_parser.go)
runs Parser.Parse, which calls stripProviderCallerIdentity
(coordinator/internal/inference/prelude/request_prelude.go,
coordinator/internal/inference/prelude/provider_body_privacy.go),
so direct dispatch, queueing, retries and endpoint/model rewrites use the same
minimized body. Nested messages, tool arguments, schema properties and media are
unchanged. metadata_details remains the separate coordinator response-metadata
opt-in. Authentication, billing ownership and cache scopes remain context-derived.
This is field minimization, not anonymity; see the
privacy boundary.
Both Earn-page notification buttons register the same per-account interest; a later registration replaces that account's selected hardware. No endpoint sends email.
| Route | Authentication and rate limit | Result | Code |
|---|---|---|---|
POST /v1/interest/small-models |
Privy JWT only; financial mutation bucket | Empty 204 only after the store upsert completes |
coordinator/api/accounts/small_models_interest.go (HandleRegisterSmallModelsInterest), coordinator/api/routes.go (routes) |
GET /v1/interest/small-models |
Privy JWT only | Own record (account_id, hardware, created_at, updated_at), or 404; private/no-store |
coordinator/api/accounts/small_models_interest.go (HandleGetSmallModelsInterest) |
GET /v1/admin/interest/small-models?limit=100&after=<account_id> |
Admin key or verified Privy admin; inference/provider keys rejected | {data: [...], next_cursor?: string}; each record includes the current user email; private/no-store |
coordinator/api/accounts/small_models_interest.go (HandleAdminSmallModelsInterest) |
| Registration field / constraint | Accepted values | Code |
|---|---|---|
mac_type |
MacBook Pro, Mac Mini, Mac Studio, Mac Pro |
coordinator/api/accounts/small_models_interest.go (HandleRegisterSmallModelsInterest) |
chip |
Nonblank string, at most 128 bytes, no control characters | Same handler |
ram_gb |
Integer from 1 through 2048 | Same handler |
| Identity/contact | Derived from the verified session and stored user; caller-supplied identity, email, or other unknown fields reject with 400 |
Same handler |
| Body | One JSON object, no trailing values; maximum 1024 bytes (413 when exceeded) |
Same handler, interestJSONError |
| Contactability | A stored account email is required (422 email_required); 401 invalid/missing session, 403 noninteractive credentials, 429 rate limit, 500 storage failure never acknowledge registration |
Same handler and RequirePrivyAuth in coordinator/api/access/auth.go, RateLimitFinancial in coordinator/api/access/rate_limits.go |
| Export page | limit defaults to 100, valid range 1–100; after is exclusive account ID, at most 256 bytes; follow next_cursor until absent |
coordinator/api/accounts/small_models_interest.go (HandleAdminSmallModelsInterest) |
The console forwards only to this fixed coordinator path. Its pending hardware marker is not proof of registration; success requires 204 or authenticated readback. See sign-in and registration steps.
Lifecycle drains preserve the existing public inference protocol. A reservation that reaches a newly draining provider's final writer is retried as transient 503 capacity, with no sent frame or new usage debit. Already accepted requests continue through their normal streaming/non-streaming terminal and settlement paths. The additive provider WebSocket barrier is connection-scoped and never exposed as an unauthenticated HTTP stop endpoint. The unified local API refuses new admissions with 503 during drain and tracks accepted response bodies until their final write. This applies to local-only CLI replacement as well as coordinator-connected providers; CLI lifecycle control remains private to the local OS user.
The trust_status.authorization readiness diagnostic can use self_route for
an account-owned connection that passes existing self/preferred-owner liveness
and privacy gates below the public trust floor. It does not grant public-fleet
eligibility or bypass per-model dispatch checks. Code:
coordinator/registry/owner_authorization.go (ProviderOwnerServingAuthorized)
and coordinator/api/provider/trust/app_attest.go (providerServingAuthorizationStatus).
X-Provider-Authorization-Method is app_attest, legacy, dual, or none.
X-Provider-Verification is compact JSON with observed_at, app_attest, and
legacy. Each method has state and optional verified_at / expires_at
(exclusive Unix seconds). Server states are verified, pending, expired,
revoked, unsupported, and offline; missing metadata is unknown. Unsupported
means the registered App Attest protocol lacks the qualified protocol 3 path,
not an inference from a reported OS version. Times absent from the response are
unavailable. No certificate, receipt, account, credential, serial or canonical
machine identifier is included. Code: coordinator/registry/verification.go
(ProviderVerification).
For inference these fields are frozen at authorizeInferenceHandoff in
coordinator/registry/inference_authorization.go, after the writer queue and
last authorization check. The winning attempt's snapshot is used for headers
and opt-in chat metadata.verification even if its grant expires or is revoked
before the first response byte. Existing X-Provider-Trust-Level and MDA fields
keep their legacy meaning. The console proxy forwards the verification and
provider-hop encryption headers; it does not construct them from provider output.
The server decides verified with precise time before dispatch; Unix-second
serialization can make a valid final fractional second show equal observed_at
and expires_at. Historical display preserves that frozen server verdict,
while live views still expire grants at the recorded deadline. Public network statistics are explicitly historical source observations: summarizeSnapshotVerification in console-ui/src/lib/verification.ts keeps those method counts stable until the next snapshot, while the stats header exposes age. Directory filters and proof details use that same observation. Owner dashboard/removal controls continue to use live expiry; a stale statistics snapshot never grants permission to serve.
GET /v1/me/providers, GET /v1/providers/attestation, and individual public
GET /v1/stats provider rows include the same verification object evaluated
at snapshot time. Owner records with no live connection are offline. Connected but untrusted owned
providers retain their live verification verdict (including revocation), while
serving authorization remains false. Owner verification,
app_attest_authorized and expiry are checked with current account and status
in one registry/provider-locked observation (ProviderVerificationAndAuthorization in
coordinator/registry/verification.go), so grant changes cannot mix opposing
verdicts in one row. Unknown App Attest protocol versions remain unsupported;
only protocol 3 can show a pending current authorization path. Public attestation rows capture verification,
compatibility authorization flags, and catalog models in one registry/provider
locked walk (ForEachProviderVerification in coordinator/registry/verification.go).
Stats rows and geography aggregates use one locked visitor and detached location
values (aggregateProviderLocations in coordinator/internal/api/reporting/statsview/stats_provider_locations.go),
so a new or replacement connection cannot change the geography between the row
and count observations. Missing
or stale verification metadata is counted as unknown in the UI, with a separate
known-verdict denominator. Owner views retain explicit app_attest_authorized
and expiry guidance during older-coordinator rollout without inventing missing
proof timestamps; connected untrusted records remain in the connected count.
verification_counts in stats and privacy-floored provider geography buckets
contains connections, authorized (union), app_attest, legacy, and
overlap. Both method counts include the overlap. Legacy hardware_attested
counts remain evidence counts. Top-level counts additionally report
known_unique_machines, connections_without_machine_identity,
reported_macos_27_or_later, and connections_with_reported_os. Known machines
are deduplicated privately from verified account/machine inventory; this is not
proof of physical uniqueness. OS counts are app reports, not successful App
Attest counts. Private-only providers are excluded. These aggregates describe
the source snapshot, not a reusable routing grant. Code:
coordinator/internal/api/reporting/statsview/stats_verification.go (AddProvider).
Live console views honor each method's expiry and stop showing cached verified
verdicts after 60 seconds from observed_at, even if polling fails. Revocation
appears on refresh, subject to server/cache/poll delay; no instant push is
promised. Chat history uses dispatch time instead of the live clock. No
telemetry wire enums or authorization gates change.
| Surface | Contract | Code |
|---|---|---|
POST /v1/admin/app-attest/revoke |
Admin authenticated; account/key/reason body, durable idempotent revocation and immediate local dispatch fencing; exact response and errors | coordinator/api/provider/trust/app_attest_revocation.go (HandleAdminAppAttestRevoke) |
GET /v1/providers/attestation |
Public connections only; private-only providers are excluded before shared caching. Additive app_attest_authorized boolean and authorization_expires_at Unix deadline; no account, App Attest credential, canonical machine IDs or raw evidence exposed. The existing persistent legacy se_public_key remains linkable across public sessions. |
coordinator/api/provider/trust/status.go (HandleProviderAttestation) |
GET /v1/me/providers adds account-scoped app_attest_authorized and optional
authorization_expires_at (exclusive Unix seconds), computed from the current
connection's complete registry authorization; stored/offline records never
restore that grant. These fields add no private App Attest IDs or proof bytes
and never change legacy trust_level or mda_verified. Code:
coordinator/internal/api/accounts/fleetview/me_authorization.go (attachMyProviderAuthorization).
Each owner-visible provider may also include os_version, the current or last
app-reported macOS version retained from its signed registration blob in
attestation.VerificationResult.OSVersion. This is upgrade guidance, not
Apple-certified inventory or serving authorization. A live connection without
an OS report clears any older stored version; absent values mean unknown.
Code: coordinator/internal/api/accounts/fleetview/projection.go (buildMyProvider).
Auth column — how the handler chain establishes identity. The only credential header is Authorization: Bearer <token> (ExtractBearerToken); the coordinator never reads x-api-key.
| Label | Mechanism | Symbol |
|---|---|---|
— |
No authentication | — |
key |
Bearer is an API key (shape; legacy eigeninference-… keys are also accepted), a Privy JWT, or the admin key. Missing or invalid → 401 authentication_error |
RequireAuth |
privy |
Bearer must be a Privy JWT. API keys → 403 forbidden |
RequirePrivyAuth |
user |
key or privy plus an in-handler check that a resolved account user is in the context (Privy JWT, or an API key linked to a Privy account). Admin key and unlinked legacy keys → 401 auth_error |
RequirePrivyUser (coordinator/api/access/request.go) |
admin |
In-handler check: Bearer equals the admin key (EIGENINFERENCE_ADMIN_KEY), or the context holds a Privy user whose email is in the admin list. Otherwise 403 forbidden. When the route is registered without RequireAuth no user is ever placed in the context, so only the admin key can pass; those rows say admin-key |
IsAdminAuthorized (coordinator/api/access/authorize.go), RequireAdminKey (coordinator/api/access/authorize.go), IsAdmin (coordinator/api/access/authorize.go) |
admin-session |
RequireAuth verifies the Privy JWT or admin key; the handler requires an allowlisted admin and rejects inference API keys/provider tokens even when owned by an admin. Missing/invalid credentials → 401; authenticated non-admin or non-interactive account credentials → 403 |
isBuildAdminAuthorized (coordinator/api/releases/app_attest_builds.go) |
publishing |
X-Darkbloom-Publishing-Key header or Bearer equal to the bootstrap MODEL_REGISTRY_PUBLISHING_KEY, the admin key, or a publishing key stored in the DB |
RequirePublishingAPIKey (coordinator/api/access/publishing.go) |
release |
Bearer equal to EIGENINFERENCE_RELEASE_KEY; otherwise 401 unauthorized |
HandleRegisterRelease (coordinator/api/releases/release_handlers.go) |
stripe-sig |
Stripe webhook signature | HandleStripeWebhook (coordinator/api/billing/stripe_checkout_webhook.go), HandleStripeConnectWebhook (coordinator/api/billing/payouts/stripe_payouts_webhooks.go) |
mdm-secret |
Webhook secret via X-Webhook-Token header or ?token=; body capped at maxMDMWebhookBodyBytes |
HandleMDMWebhook |
ws |
Provider WebSocket handshake (enrollment credentials + attestation); see protocol-messages.md |
HandleProviderWS (coordinator/api/provider/provider.go) |
Limiter column — the rate-limit middleware in the chain. All limiters share one implementation, rateLimitWithTier, keyed by the authenticated account id (ConsumerKeyFromContext); the admin key bypasses it.
| Label | Behaviour | Symbol |
|---|---|---|
drain |
While draining, new inference requests get 429 rate_limit_exceeded with Retry-After set to coordinatorDrainRetryAfter (written through WriteTokenRateLimited) |
DrainGate (coordinator/api/operations/drain.go) |
rpm |
Consumer tier: first the key's own rpm_limit (applyKeyRPMLimit), then the account limiter; service-role accounts use the elevated service limiter. Rejection → 429 rate_limit_exceeded (code: rate_limit_exceeded) with Retry-After and X-RateLimit-Reset |
RateLimitConsumer |
fin |
Financial tier: the stricter limiter installed by SetFinancialRateLimiter, applied to every account regardless of role; same 429 shape |
RateLimitFinancial |
Both tiers set x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests on allowed and rejected responses (setRequestRateLimitHeaders). Limiter Retry-After values are clamped to [DefaultRetryAfter, maxRetryAfter] (Timeouts and constants).
All four share the chain DrainGate → RequireAuth → RateLimitConsumer → SealedTransport → handler and the pipeline in coordinator/api/inference/consumer.go.
After authentication, shared preprocessing rejects negative or malformed
top-level max_tokens, max_completion_tokens and max_output_tokens with
HTTP400 invalid_request_error naming the field, before budget defaulting or
inference admission (InvalidOutputTokenField,
coordinator/api/inference/request/output_budget_validation.go). Omitted, null and zero retain
their existing default-bound behavior; valid positive integer bounds and alias
precedence are unchanged. Nested tool arguments and schemas are not inspected
as inference budgets.
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| POST | /v1/chat/completions |
HandleChatCompletions (coordinator/api/inference/consumer.go) |
key |
drain, rpm, token limits |
OpenAI Chat Completions, streaming and non-streaming |
| POST | /v1/responses |
HandleChatCompletions — the same handler; it detects input (Responses) versus messages (Chat) |
key |
same | OpenAI Responses; lowered by coordinator/internal/promptcontract/endpoint/endpoint_lower_responses.go, streamed by NewResponsesStreamEmitter (coordinator/api/inference/response/responses_stream.go) |
| POST | /v1/completions |
HandleCompletions (coordinator/api/inference/consumer.go) |
key |
same | Legacy text completions; response built by coordinator/api/inference/response/generic_endpoint_response.go, streamed by NewGenericEndpointStreamEmitter (coordinator/api/inference/response/generic_endpoint_stream.go) |
| POST | /v1/messages |
HandleAnthropicMessages (coordinator/api/inference/consumer.go) |
key |
same | Anthropic Messages; lowered by coordinator/internal/promptcontract/endpoint/endpoint_lower_messages.go, streamed by newMessagesStreamEmitter |
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| GET | /v1/models |
HandleListModels (coordinator/api/catalog/models_endpoints.go) |
key |
— | ModelListResponse: public aliases plus un-aliased builds (?include_builds=1 also lists hidden builds). With X-Darkbloom-Route: self or a self_route_only key it returns the account's own machines' models filtered by the key's allowed_models. Field reference in ../consumer/models.md |
| GET | /v1/models/openrouter |
HandleListModelsOpenRouter (coordinator/api/catalog/openrouter_endpoint.go) |
key |
— | OpenRouterModelsResponse projection |
| GET | /v1/models/{id...} |
HandleGetModel (coordinator/api/catalog/models_endpoints.go) |
key |
— | One ModelEntry; 404 model_not_found when neither a build id nor an alias matches |
| GET | /v1/models/capacity |
HandleModelsCapacity (coordinator/api/catalog/capacity.go) |
— |
— | Per-model provider capacity, cached 2 s |
| GET | /v1/models/catalog |
HandleModelCatalog (coordinator/api/catalog/catalog.go) |
— |
— | Registry catalog; ?type= selects the catalog kind, unknown → 400 |
| GET | /v1/models/catalog/manifest/ |
HandleModelCatalogManifest (coordinator/api/catalog/model_registry_handlers.go) |
— |
— | Per-model manifest by path suffix |
| GET | /v1/models/catalog/ |
HandleModelCatalogItem (coordinator/api/catalog/model_registry_handlers.go) |
— |
— | Single catalog item by path suffix |
| GET | /v1/runtime/manifest |
HandleRuntimeManifest |
— |
— | Hashes the coordinator accepts from provider runtimes: {"configured":false} or {"configured":true,"template_hashes":{"<name>":[<sorted hashes accepted across active releases>]}}; cached 1 min (runtime manifest) |
| GET | /v1/cache/status |
HandleExactCacheStatus (coordinator/api/inference/exact_cache_status.go) |
— |
— | Exact-cache status, cached for exactCacheStatusCacheTTL |
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| POST | /v1/auth/keys |
HandleCreateKey (coordinator/api/access/keys/handlers.go) |
privy |
fin |
Legacy mint: CreateKeyResponse {api_key, account_id}. If every active (not disabled, not expired) key on the account is already self_route_only, the minted key inherits that ceiling (consoleKeyInheritsSelfRouteOnly) so console auto-provision cannot escalate a machine-only account onto the paid public fleet |
| DELETE | /v1/auth/keys |
HandleRevokeKey (coordinator/api/access/keys/handlers.go) |
privy |
— | Body {"key": "<api key>"}; 400 bad_request otherwise; RevokeKeyResponse {status} |
| GET | /v1/keys |
HandleListAPIKeys (coordinator/api/access/keys/handlers.go) |
privy |
— | APIKeyListResponse {object: "list", data: [APIKeyResponse]} |
| POST | /v1/keys |
HandleCreateAPIKey (coordinator/api/access/keys/handlers.go) |
privy |
fin |
CreateAPIKeyResponse {key, data}; key is the plaintext secret (API key shapes) |
| GET | /v1/keys/{id} |
HandleGetAPIKey (coordinator/api/access/keys/handlers.go) |
privy |
— | APIKeyResponse |
| PATCH | /v1/keys/{id} |
HandleUpdateAPIKey (coordinator/api/access/keys/handlers.go) |
privy |
fin |
Partial update, fields under API key shapes |
| DELETE | /v1/keys/{id} |
HandleDeleteAPIKey (coordinator/api/access/keys/handlers.go) |
privy |
fin |
Revoke |
| POST | /v1/keys/{id}/rotate |
HandleRotateAPIKey (coordinator/api/access/keys/handlers.go) |
privy |
fin |
New secret, same settings |
| GET | /v1/key |
HandleGetCallingKey (coordinator/api/access/keys/handlers.go) |
key |
— | The calling key's own APIKeyResponse |
| GET | /v1/encryption-key |
HandleEncryptionKey (coordinator/api/inference/sender_encryption.go) |
— |
— | {kid, public_key, algorithm: "x25519-nacl-box"}, Cache-Control: public, max-age=300; 503 encryption_unavailable when sealing is not configured |
Lifecycle semantics: ../consumer/authentication.md.
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| POST | /v1/device/code |
HandleDeviceCode (coordinator/api/access/device/handlers.go) |
— |
— | 200 {device_code, user_code, verification_uri, expires_in, interval} |
| POST | /v1/device/token |
HandleDeviceToken (coordinator/api/access/device/handlers.go) |
— |
— | Body {"device_code"} (400 invalid_request if missing). 200 {status: "authorization_pending"} until approved; 200 {status: "authorized", token, account_id} once approved; 404 invalid_grant; 410 expired_token |
| POST | /v1/device/approve |
HandleDeviceApprove (coordinator/api/access/device/handlers.go) |
privy |
fin |
Body {"user_code"}. 404 invalid_code, 409 already_used, 410 expired_code |
Constants: DeviceCodeExpiry = 15 min (expires_in: 900), DeviceCodePollInterval = 5 (interval). The token is a provider token (eigeninference-pt- + 64 hex characters, labelled device-<user_code>; only its SHA-256 hash is stored) used by the provider CLI to link a machine to the account; it is not a consumer API key. The small-body cap maxControlPlaneBodyBytes applies to these unauthenticated endpoints.
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| GET | /v1/payments/balance |
HandleBalance (coordinator/api/billing/account.go) |
key |
— | BalanceResponse {balance_micro_usd, balance_usd, withdrawable_micro_usd, withdrawable_usd} |
| GET | /v1/payments/usage |
HandleUsage (coordinator/api/billing/account.go) |
key |
— | UsageResponse {usage: [...]}; each payments.UsageEntry carries cached_tokens (omitted when 0) — the subset of prompt_tokens billed at the cache-read rate; recent history only (retention) |
| GET | /v1/billing/wallet/balance |
HandleWalletBalance (coordinator/api/billing/wallet.go) |
key |
— | Wallet view of the ledger balance |
| GET | /v1/billing/methods |
HandleBillingMethods (coordinator/api/billing/methods.go) |
— |
— | Which top-up methods are enabled |
| GET | /v1/provider/account-earnings |
HandleAccountEarnings (coordinator/api/billing/earnings.go) |
key |
— | Earnings across the account's providers |
| GET | /v1/me/token-promotions |
HandleMyModelTokenPromotions (coordinator/api/inference/model_token_promotions.go) |
privy |
— | Account-scoped grants and eligible offers |
| POST | /v1/me/token-promotions/claim |
HandleMyModelTokenPromotions (coordinator/api/inference/model_token_promotions.go) |
privy |
fin |
Claim a capped grant; campaign procedure |
| GET | /v1/me/summary |
HandleMySummary (coordinator/api/accounts/summary.go) |
user |
— | Console account summary; includes latest_provider_version |
| GET | /v1/me/providers |
HandleMyProviders (coordinator/api/accounts/providers.go) |
user |
— | Machines linked to the account |
| GET | /v1/me/self-route-models |
HandleMySelfRouteModels (coordinator/api/accounts/self_route.go) |
user |
— | Models the account's own machines can serve |
| DELETE | /v1/me/providers/{id} |
HandleDeleteMyProvider (coordinator/api/accounts/delete_provider.go) |
user |
fin |
Unlink a machine; requires the soft-delete mutation gate. After authentication, record existence and ownership checks, a disabled gate returns 503 soft_delete_mutations_disabled without mutation |
| GET | /v1/pricing |
HandleGetPricing (coordinator/api/billing/pricing.go) |
— |
— | Public price table, types.PricingResponse {prices: [{model, input_price, output_price, cache_read_price, input_usd, output_usd, cache_read_usd}], fallback_input_price, fallback_output_price, fallback_cache_read_price, fallback_*_usd}; cache_read_price is the effective rate (derived when the row sets none); see pricing-model.md |
| PUT | /v1/pricing |
HandleSetPricing (coordinator/api/billing/pricing.go) |
user |
— | Provider sets its own prices: {model, input_price, output_price, cache_read_price?} (modelprice.Input, coordinator/api/modelprice/price.go; 0 ≤ cache_read_price ≤ input_price, omitted = derived) → types.PriceUpdateResponse |
| DELETE | /v1/pricing |
HandleDeletePricing (coordinator/api/billing/pricing.go) |
user |
— | Revert to defaults |
All six /v1/me/* routes are wrapped in RequirePrivyAuth, so they are Privy-JWT only.
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| POST | /v1/billing/stripe/create-session |
HandleStripeCreateSession (coordinator/api/billing/checkout.go) |
key |
fin |
502 stripe_error when Stripe rejects |
| POST | /v1/billing/stripe/webhook |
HandleStripeWebhook (coordinator/api/billing/stripe_checkout_webhook.go) |
stripe-sig |
— | Checkout events |
| GET | /v1/billing/stripe/session |
HandleStripeSessionStatus (coordinator/api/billing/checkout.go) |
key |
— | Poll a checkout session |
| POST | /v1/billing/stripe/onboard |
HandleStripeOnboard (coordinator/api/billing/payouts/connect_onboarding.go) |
user (Privy-only wrapper) |
fin |
Country-aware Connect or Global Payouts onboarding link |
| GET | /v1/billing/stripe/status |
HandleStripeStatus (coordinator/api/billing/payouts/connect_status.go) |
user |
— | Payout readiness; additive account_id scopes browser confirmation recovery, plus migration_required (self-service bank setup needed), payout_rail, payout_currency, countries, payouts_available, recipient_limits (currency, exponent, published minimum/maximum minor units) |
| POST | /v1/billing/withdraw/stripe |
HandleStripeWithdraw (coordinator/api/billing/payouts/stripe_withdraw.go) |
user (Privy-only wrapper) |
fin |
Global Payouts confirms a persisted quote_id; low funding accepts a reserved queued withdrawal with HTTP 202 on either rail; 409 stripe_account_gone / stripe_account_recreate_required; 502 stripe_error |
| GET | /v1/billing/stripe/withdrawals |
HandleStripeWithdrawals (coordinator/api/billing/payouts/history.go) |
user |
— | Withdrawal history |
| POST | /v1/billing/stripe/dashboard |
HandleStripeDashboardLink (coordinator/api/billing/payouts/connect_dashboard.go) |
user (Privy-only wrapper) |
fin |
Express dashboard link |
| DELETE | /v1/billing/stripe/account |
HandleStripeUnlink (coordinator/api/billing/payouts/connect_unlink.go) |
user (Privy-only wrapper) |
— | Resets Global Payouts to a fresh empty generation while retaining its routing fence. During cutover, preserves the legacy Connect mapping; before cutover, an unmigrated Connect user can still unlink it. Does not close Stripe accounts or cancel withdrawals. |
| POST | /v1/billing/stripe/connect/accounts/webhook |
HandleStripeConnectAccountsWebhook (coordinator/api/billing/payouts/stripe_payouts_webhooks.go) |
stripe-sig (accounts secret) |
— | Requires a connected-account envelope; account updates and payout events. |
| POST | /v1/billing/stripe/connect/webhook |
HandleStripeConnectWebhook (coordinator/api/billing/payouts/stripe_payouts_webhooks.go) |
stripe-sig |
— | Connect events |
| POST | /v1/billing/stripe/quote |
HandleGlobalPayoutQuote (coordinator/api/billing/payouts/global_payouts_withdraw.go) |
user (Privy-only wrapper) |
fin |
{amount_usd} returns quote ID, local amount/currency/exponent, expiry and fee; no ledger debit. |
| POST | /v1/billing/stripe/global/webhook |
HandleGlobalPayoutWebhook (coordinator/api/billing/payouts/global_payouts_reconcile.go) |
stripe-sig (separate secret) |
— | Reconciles the current outbound-payment state; does not consume Connect sweep events. |
| POST | /v1/mdm/webhook |
HandleMDMWebhook |
mdm-secret |
— | Fleet enrollment webhook |
Ledger semantics, reservations and payouts: ../architecture/billing.md.
| Method | Path | Handler | Auth | Limiter | Notes |
|---|---|---|---|---|---|
| POST | /v1/referral/register |
HandleReferralRegister (coordinator/api/billing/referrals.go) |
user |
fin |
Normalized code; repeated registration returns the existing code; 400 referral_error on invalid input |
| POST | /v1/referral/apply |
HandleReferralApply (coordinator/api/billing/referrals.go) |
user |
fin |
Same-code application is idempotent; self-referral or reassignment returns 400 referral_error |
| GET | /v1/referral/stats |
HandleReferralStats (coordinator/api/billing/referrals.go) |
key |
— | 404 referral_error before registration |
| GET | /v1/referral/info |
HandleReferralInfo (coordinator/api/billing/referrals.go) |
key |
— | 200 before registration; empty code, current referred_by |
| POST | /v1/invite/redeem |
HandleRedeemInviteCode (coordinator/api/accounts/invite_handlers.go) |
key |
fin |
Redeem an invite code |
| GET | /v1/providers/attestation |
HandleProviderAttestation (coordinator/api/provider/trust/status.go) |
— |
— | Public attestation roster; see ../architecture/security/attestation.md |
All routes resolve the caller's account; they do not accept an account ID from the body. Register and apply require Privy authentication. Read routes accept an authenticated API key or Privy user. New registration trims whitespace, uppercases ASCII letters, and enforces the code rules. Applying legacy codes containing Unicode letters remains supported.
| Route | Request JSON | Success JSON | Citation |
|---|---|---|---|
POST /v1/referral/register |
{"code":"MYCODE"} |
code, share_percent, reward_basis, message |
coordinator/api/billing/referrals.go (HandleReferralRegister) |
POST /v1/referral/apply |
{"code":"MYCODE"} |
status: "applied", normalized code, message |
HandleReferralApply |
GET /v1/referral/info |
— | code (empty before registration), share_percent, reward_basis, referred_by (empty without attribution) |
HandleReferralInfo |
GET /v1/referral/stats |
— | Fields below; 404 referral_error before registration |
coordinator/billing/referral.go (ReferralStatsResponse); HandleReferralStats |
| Stats field | Type | Meaning | Citation |
|---|---|---|---|
code |
string | Caller's own registered referral code | coordinator/billing/referral.go (ReferralStatsResponse) |
share_percent |
integer | Fixed rate defined in pricing constants | ReferralService.SharePercent |
reward_basis |
string | "consumer_spend"; reward arithmetic |
ReferralStatsResponse |
total_referred |
integer | Accounts attributed to this code | coordinator/store/postgres/referrals.go (GetReferralStats) |
total_referred_spend_micro_usd, total_referred_spend_usd |
integer, decimal string | Eligible collected spend attributed to this referrer in settlement records; excludes pre-program historical usage. Routing exclusions also apply to paid promotion portions. | GetReferralStats |
total_rewards_micro_usd, total_rewards_usd |
integer, decimal string | Lifetime credited referral rewards, including historical referral ledger entries | GetReferralStats; coordinator/billing/referral.go (ReferralStatsResponse) |
balance_micro_usd, balance_usd |
integer, decimal string | Current spendable account balance, including other funds; not lifetime rewards or the withdrawable subset | ReferralService.Stats |
Get withdrawable_micro_usd from GET /v1/payments/balance or the authenticated
summary when displaying available earned funds. USD strings use six decimal
places. A per-request reward rounds down independently, so lifetime reward
need not equal a percentage of the displayed aggregate spend; old referral
rewards can also predate the settlement-based spend counter.
Malformed JSON or missing code returns 400 invalid_request_error; invalid,
taken, self-referral or conflicting codes return 400 referral_error.
Unexpected store failures return a sanitized 503 referral_error; an unavailable
referral service returns 503 billing_error
(coordinator/api/billing/referrals.go, writeReferralError and referral handlers).
| Method | Path | Handler | Auth | Notes |
|---|---|---|---|---|
| GET | /v1/stats |
HandleStats (coordinator/api/reporting/stats_handler.go) |
— |
Refresh every 30 s; preserve the UTC source observation time in snapshot_at (time.RFC3339Nano). Geography refreshes independently and reports availability per section. Retain a successful core body up to 5 min on core refresh failure; 503 service_unavailable without an unexpired success |
| GET | /v1/leaderboard |
HandleLeaderboard (coordinator/api/reporting/leaderboard.go) |
— |
Successful top-200 rankings, including genuinely empty windows, cached 5 min per metric/canonical window; caller limits and aliases share one fill. Query, scan, or iteration failures return 503 service_unavailable with Retry-After; only a 10 s failure cooldown is retained, never empty/partial data |
| GET | /v1/network/totals |
HandleNetworkTotals (coordinator/api/reporting/totals_handler.go) |
— |
Database-backed totals refreshed every 5 min with a 15 min stale-success ceiling; 503 service_unavailable without an unexpired success; canonical windows 24h, 7d, 30d, all (1d → 24h, empty/lifetime → all) |
| GET | /v1/network/model-demand |
HandleModelDemand (coordinator/api/) |
— |
Recorded public model demand; window=24h (default), 7d, 30d; cached up to 5 min; 400 for other windows; 503 on unavailable aggregation |
| GET | /v1/network/series |
HandleNetworkSeries (coordinator/api/reporting/network_series.go) |
— |
Time series for 30m, 24h, 7d, 30d, successful database results cached 5 min; 503 service_unavailable on a store error after a miss, with no failed result cached |
| GET | /health |
HandleHealth (coordinator/api/operations/health.go) |
— |
HealthResponse {status: "ok", draining, providers, version, build_commit, build_date} |
A successful empty analytics window returns 200 with empty arrays or zero totals.
When configured, the archived snapshot mode
replaces leaderboard, network totals and all four network series reads with
validated local data. updated_at reports the source as_of time. Missing,
unqualified or expired data returns 503 without a SQL fallback or an ordinary
cache hit. Only the totals background queries are disabled; core stats and
geography retain their independent database-backed refreshes.
Core stats query failures retain the unexpired success or return 503; request
geography never blocks core stats. Geography refreshes on its own
statsRefreshInterval loop, using statsGeographyCacheKey. Core snapshots
include the latest completed geography attempt, so availability changes appear
on a subsequent core refresh. Missing or expired geography is unavailable.
Cache behavior is implemented by coordinator/internal/api/reporting/refresher/cache_refresher.go
(computeCachedEntry) and coordinator/api/reporting/refresh_start.go
(StartCacheRefreshers), with geography in
coordinator/internal/api/reporting/statsview/stats_geography.go (cachedStatsGeography, computeStatsGeography).
Leaderboard fills use coordinator/internal/api/reporting/ranking/leaderboard_cache.go (CachedLeaderboard):
one in-flight query per metric/canonical window, leaderboardCacheLimit = 200,
leaderboardCacheTTL = 5 * time.Minute, and
leaderboardFailureBackoff = 10 * time.Second. The response retains the caller's
window spelling and requested limit; shared successful rows are immutable.
Failure cooldowns return a positive Retry-After rounded up to seconds until
the next query is allowed.
| Stats geography field | Contract | Code |
|---|---|---|
request_locations_status, request_flows_status |
available or unavailable, independently; unavailable includes startup before the first geography refresh finishes. Core stats still return 200 |
coordinator/internal/api/reporting/statsview/stats_geography.go (statsGeographyStatus, computeStatsGeography) |
geography_snapshot_at |
RFC 3339 UTC observation start for the geography attempt, separate from core snapshot_at; empty before an attempt or after expiry |
coordinator/internal/api/reporting/statsview/stats_geography.go (statsGeography) |
request_locations, request_regions, unknown_request_location_requests, suppressed_request_city_requests |
null when locations are unavailable; successful empty windows retain arrays and numeric counts. A failed attempt replaces previous geography rather than presenting stale figures as current |
coordinator/internal/api/reporting/statsview/stats_geography.go (computeStatsGeography, addTo) |
request_flows |
null when flows are unavailable, an array (possibly empty) on success; independent of location status |
coordinator/internal/api/reporting/statsview/stats_geography.go (computeStatsGeography) |
provider_locations, provider_regions |
Computed from the same live-fleet walk as core provider rows and verification counts; request-geography failures do not hide provider geography | coordinator/internal/api/reporting/statsview/stats.go (computeStats); coordinator/internal/api/reporting/statsview/stats_provider_locations.go (aggregateProviderLocations) |
The stats, totals, leaderboard, and series handlers emit the 503 service_unavailable error
envelope when their required data is unavailable.
coordinator/api/ (HandleModelDemand) serves a fixed receipt-time
window ending at the preceding UTC hour (at least one hour behind now). The
JSON has window, start_at, end_at, updated_at, collection_started_at,
coverage: "published_hourly_cohorts", bucket_seconds, and models. Each model object has model,
requests, completed, capacity_rejected, latency_rejected, timed_out,
failed, cancelled, unknown, and http_429 (all counts are integers).
Each model also contains time_series: fixed display intervals with timestamp
and counts (the same outcome counters, or null when no hours are publishable).
Intervals are 1 hour for 24h, 6 hours for 7d, and 24 hours for 30d.
Publication eligibility is always evaluated per model and UTC clock hour:
at least 20 non-excluded recorded requests from 3 consumer accounts. Larger
display intervals sum only eligible hours and may cover only part of their
duration. They never restore suppressed hours. Each model's totals equal the
fieldwise sum of its non-null interval counts; no complete-window totals or
suppressed residuals are exposed. Models without an eligible hour are omitted.
Null intervals are gaps, not measured zeros. All counts share one repeatable-read
transaction and the same hourly publication rule across all three windows.
The seven outcome counts sum to requests; http_429 overlaps that partition.
capacity_rejected includes provider or coordinator saturation and a 503
model_too_large supply shortfall. A preflight context_exceeded request is
excluded from requests; dispatch_exhausted is counted as capacity only when
the terminal capacity check supports its HTTP 429 response. Other 429 reasons
are not assumed to be capacity rejections.
No token estimates, identifiers, provider details, raw reasons, or suppressed
counts are exposed. ModelDemandCounts in coordinator/store/model_demand.go
is the response shape.
Only new, explicitly scoped requests reaching public routing admission are counted. Owner-preferred, exclusive self-route and machine-restricted requests are excluded; validation and account failures are outside the denominator. Requests rejected before this point (including early model shedding) are not covered. Admin-key traffic is excluded. Ordinary authenticated load tests cannot be separated from organic traffic. Public aliases retain their requested identity through build fallback. Client retries count separately; internal dispatch attempts do not.
ModelDemandMinRequests = 20 and ModelDemandMinConsumers = 3 suppress sparse
hourly model cohorts. A gateway is one authenticated consumer, not a count of
its downstream users. A successful empty list means no hour qualifies for
publication, not zero traffic. Counts and percentages describe published hours
only, not the complete selected window. Collection may also have partial history;
recording remains best-effort, not an independently reconciled network-wide
denominator. Completion establishes
coordinator-observed provider completion and successful terminal writes, not
client receipt. See incoming request accounting.
| Method | Path | Handler | Auth | Notes |
|---|---|---|---|---|
| GET | /install.sh |
inline closure in routes() rendering installScript with the coordinator URL from resolveBaseURL |
— |
Provider install script, text/plain |
| GET | /api/version |
HandleVersion (coordinator/api/releases/download.go) |
— |
VersionResponse {version, platform, backend, download_url, binary_hash, bundle_hash, metallib_hash, changelog}; uses the newest active release in the store, else LatestProviderVersion |
| POST | /v1/releases |
HandleRegisterRelease (coordinator/api/releases/release_handlers.go) |
release |
Register a release |
| GET | /v1/releases/latest |
HandleLatestRelease (coordinator/api/releases/read_handlers.go) |
— |
Latest release record |
| GET | /readyz |
HandleReadyz (coordinator/api/operations/drain.go) |
— |
200 normally; 503 while draining |
The 0.9.19 prepared candidate sets LatestProviderVersion in
coordinator/api/server.go. A registered active release still takes precedence
for version displays; this fallback change does not publish an updater release.
GET /v1/releases/latest requires a registered release and returns 404 when none
exists (coordinator/api/releases/read_handlers.go, HandleLatestRelease).
The platform's updated POST /v1/releases retains scoped release-key
authentication and the existing code_directory_hash, source_commit,
ci_run_id, and require_app_attest_qualification fields. After artifact
verification it records the complete official native identity before
revocation-fenced publication. Partial identity fails closed; conflicting or
revoked identity returns 409 and unavailable policy returns 503. No separate
hardware approval is required. Deploy the platform change before using the
provider workflow; the retained coordinator copy here is not its implementation
owner. See the platform API contract.
Admin GET/POST /v1/admin/app-attest/builds lists/approves signed builds; admin POST /v1/admin/app-attest/builds/revoke records a permanent withdrawal. See request/response and error contracts. With production App Attest serving enabled, even cached /v1/releases/latest and /api/version responses return 503 when the selected release lacks fresh qualification/catalog readiness; this does not silently select a different release.
Release publishing: ../operations/provider-release.md.
| Method | Path | Handler | Auth | Notes |
|---|---|---|---|---|
| POST | /v1/enroll |
HandleEnroll (coordinator/api/provider/trust/enroll.go) |
Linked provider Bearer token + signed SE-key proof (upcoming policy) | Downloads an MDM profile only for an existing key under its frozen account; proof contract |
| GET | /ws/provider |
HandleProviderWS (coordinator/api/provider/provider.go) |
ws |
Provider WebSocket; message catalogue in protocol-messages.md |
| POST | /v1/provider/log-report |
HandleUploadLogReport (coordinator/api/operations/log_reports.go) |
key |
Body capped at maxLogReportBodySize; 409 account_deleted if scrub finishes before persistence; 426 upgrade_required when ?serial= names a provider below the minimum version |
During provider WebSocket registration, a transient early token-store lookup
failure closes the connection with 1013 (StatusTryAgainLater) before registry
registration or frozen-cohort classification. Reconnecting retries the lookup;
missing or revoked tokens retain the fail-closed authentication behavior and
cannot gain authorization through a storage failure (coordinator/api/provider/session.go,
Owner.providerReadLoop, local resolveAccount closure).
Under the upcoming frozen legacy policy, POST /v1/enroll downloads a profile
for authenticated reenrollment, not provider credentials (coordinator/api/provider/trust/enroll.go,
HandleEnroll). Cohort and copied-profile limits are defined in
MDM enrollment.
On older macOS, EnrollmentService.enroll in
provider-swift/Sources/ProviderCore/Auth/Enrollment.swift uses this same
authenticated endpoint before returning
"Already enrolled" for an existing local Darkbloom profile. A successful response
in that case is not saved or installed again, and a failed eligibility check
cannot become success merely because a profile exists. This is an eligibility
check, not a current serving grant. New providers require macOS 27 or later and
current qualified App Attest; an OS version alone grants neither serving nor enrollment.
| Input or outcome | Contract | Enforcement |
|---|---|---|
Authorization |
Exact Bearer <token> prefix with an active linked provider token; its account must be the frozen account for this key |
coordinator/internal/provider/legacymdm/policy.go (Policy.AuthorizeEnrollment) |
se_public_key |
Existing frozen key's exact standard-base64 string; decoded P-256 point is 65 bytes (0x04 + X + Y), or accepted 64-byte X + Y |
coordinator/internal/provider/legacymdm/policy.go (Policy.AuthorizeEnrollment); coordinator/attestation/attestation.go (ParseP256PublicKey) |
timestamp |
JSON integer Unix seconds; at most five minutes behind or ahead of the coordinator clock | coordinator/internal/provider/legacymdm/policy.go (Policy.AuthorizeEnrollment) |
signature |
Standard-base64 ASN.1 DER ECDSA P-256 signature over SHA-256 of the canonical transcript below | coordinator/attestation/attestation.go (VerifyChallengeSignature) |
| Success | 200 MDM .mobileconfig response, not a trust grant or provider token |
coordinator/api/provider/trust/enroll.go (HandleEnroll) |
| Credential failure | 401 for missing, invalid, inactive or unlinked credentials; a backend lookup error is not an invalid credential |
coordinator/internal/provider/legacymdm/policy.go (Policy.AuthorizeEnrollment) |
| Credential-store unavailable | 503 on token lookup I/O failure; retry later without replacing credentials. The response does not expose the underlying store error |
Same |
| Proof or membership failure | 403 for a nonmember, out-of-window timestamp or invalid signature. Nonmembers receive macOS 27+/qualified App Attest guidance and the legacy base-reward restriction; a frozen account/key with an out-of-window proof receives clock/retry guidance instead |
coordinator/internal/provider/legacymdm/policy.go (Policy.AuthorizeEnrollment) |
The JSON body contains se_public_key, timestamp and signature. The canonical
transcript uses LF separators and no trailing newline; \n below denotes one
LF byte, not a literal backslash followed by n:
darkbloom-mdm-enroll-v1\n<lowerhex SHA256 token>\n<se_public_key>\n<unix timestamp>
<lowerhex SHA256 token> is the lowercase hexadecimal SHA-256 digest of the
Bearer token itself, excluding the Bearer header prefix. <se_public_key> is
the exact JSON string value and <unix timestamp> is the integer formatted in
base 10. The proof binds the request to both token and key; possession of either
alone is insufficient. The timestamp bound is proof freshness, not a cohort
grace period or expiry; frozen membership remains a separate prerequisite.
| Method | Path | Handler | Auth | Notes |
|---|---|---|---|---|
| PUT | /v1/admin/pricing |
HandleAdminPricing (coordinator/api/billing/pricing.go) |
admin |
Platform default price table; same body and response as PUT /v1/pricing (modelprice.Input → types.PriceUpdateResponse) |
| PUT | /v1/admin/users/role |
HandleAdminSetUserRole (coordinator/api/accounts/admin_users.go) |
admin |
Role selects the consumer or service limiter |
| POST | /v1/admin/accounts/{account_id}/erasure/plan |
HandlePlan (coordinator/api/accounts/erasure/handlers.go) |
admin |
Dry run plus a 15-minute confirm token; changes no account data. Erasure plan |
| POST | /v1/admin/accounts/{account_id}/erasure |
HandleRequest (coordinator/api/accounts/erasure/handlers.go) |
admin |
Soft delete, revoke keys and provider tokens, disconnect providers; force scrubs at once. Erasure confirm |
| GET | /v1/admin/accounts/{account_id}/erasure |
HandleStatus (coordinator/api/accounts/erasure/handlers.go) |
admin |
Newest request, its outbox rows and refused credits. Erasure status |
| POST | /v1/admin/accounts/{account_id}/erasure/cancel |
HandleCancel (coordinator/api/accounts/erasure/handlers.go) |
admin |
Grace period only; keys and tokens stay revoked. Erasure cancel |
| PUT | /v1/admin/users/platform-fee |
HandleAdminSetUserPlatformFee (coordinator/api/accounts/admin_users.go) |
admin |
Per-user fee override; fee policy in ../architecture/billing.md#invariants |
| POST | /v1/admin/models/register |
HandleRegisterModel (coordinator/api/catalog/model_registry_handlers.go) |
publishing |
Publish a model build; optional cache_read_price beside input_price/output_price (modelprice.Input); the response (registerModelResponse) quotes the effective platform rates |
| POST | /v1/admin/models/ |
HandleAdminModelRegistryAction (coordinator/api/catalog/registry_actions.go) |
publishing |
Registry actions selected by path suffix, including publish-revision (version plus optional pinned hugging_face_artifact) and retire-revision (version); publication returns 503 if its committed promotion has not reached live policy or desired-state delivery to a provider fails; revision contracts |
| GET / POST | /v1/admin/models/aliases |
HandleModelAliasList, HandleModelAliasUpsert (coordinator/api/catalog/model_alias_handlers.go) |
publishing |
Two registrations; upserts fan out desired_models (see Version gating) |
| DELETE | /v1/admin/models/aliases/{aliasID} |
HandleModelAliasDelete (coordinator/api/catalog/model_alias_handlers.go) |
publishing |
|
| GET / POST | /v1/admin/models/openrouter-aliases |
HandleOpenRouterAliasList, HandleOpenRouterAliasUpsert (coordinator/api/catalog/openrouter_alias_handlers.go) |
publishing |
Two registrations |
| DELETE | /v1/admin/models/openrouter-aliases/{aliasID} |
HandleOpenRouterAliasDelete (coordinator/api/catalog/openrouter_alias_handlers.go) |
publishing |
|
| GET / PUT | /v1/admin/token-promotions |
HandleAdminModelTokenPromotions (coordinator/api/inference/model_token_promotions.go) |
admin-key |
Two registrations; inspect/configure token campaigns |
| POST | /v1/admin/app-attest/revoke |
HandleAdminAppAttestRevoke (coordinator/api/provider/trust/app_attest_revocation.go) |
admin-key |
Revoke an account-owned credential; contract |
| GET | /v1/admin/app-attest/builds |
HandleAdminAppAttestBuilds (coordinator/api/releases/app_attest_builds.go) |
admin-session |
List exact signed build qualifications and audits |
| POST | /v1/admin/app-attest/builds |
HandleAdminAppAttestBuilds (coordinator/api/releases/app_attest_builds.go) |
admin-session |
Independently approve signed bytes and record test evidence; contract |
| POST | /v1/admin/app-attest/builds/revoke |
HandleAdminAppAttestBuildRevoke (coordinator/api/releases/app_attest_builds.go) |
admin-session |
Permanently withdraw build approval and fence local grants; contract |
| GET / DELETE | /v1/admin/releases |
HandleAdminListReleases, HandleAdminDeleteRelease (coordinator/api/releases/read_handlers.go) |
admin-key |
Two registrations |
| GET | /v1/admin/state-export |
HandleAdminStateExport (coordinator/api/operations/state_export.go) |
admin-key |
404 unless EIGENINFERENCE_STATE_EXPORT_ENABLED=true; 412 precondition_failed without an encryption recipient. See ../operations/state-export.md |
| POST | /v1/admin/auth/init |
HandleAdminAuthInit (coordinator/api/access/otp.go) |
— |
Body {"email"}; starts a Privy email OTP for an admin email. 503 not_configured when Privy is not configured; 500 otp_error when sending fails |
| POST | /v1/admin/auth/verify |
HandleAdminAuthVerify (coordinator/api/access/otp.go) |
— |
Verifies the OTP and returns a session token for the admin console |
| POST | /v1/admin/invite-codes |
HandleAdminCreateInviteCode (coordinator/api/accounts/invite_handlers.go) |
admin (fin) |
409 conflict on code collision |
| GET / DELETE | /v1/admin/invite-codes |
HandleAdminListInviteCodes, HandleAdminDeactivateInviteCode (coordinator/api/accounts/invite_handlers.go) |
admin |
Two registrations |
| POST | /v1/admin/credit |
HandleAdminCredit (coordinator/api/billing/admin_balance_adjustment.go) |
admin |
Manual ledger credit |
| POST | /v1/admin/reward |
HandleAdminReward (coordinator/api/billing/admin_balance_adjustment.go) |
admin |
Manual provider reward |
| GET | /v1/admin/log-reports/{id} |
HandleGetLogReport (coordinator/api/operations/log_reports.go) |
admin |
Fetch an uploaded provider log bundle |
| GET | /v1/admin/metrics |
HandleAdminMetrics |
admin-key |
Telemetry counters |
| GET | /v1/admin/base-rewards |
HandleAdminBaseRewards (coordinator/api/billing/base_rewards_handlers.go) |
admin-key |
|
| GET | /v1/admin/utilization |
HandleAdminUtilization (coordinator/api/reporting/admin_utilization.go) |
admin-key |
|
| GET / POST | /v1/admin/autopilot |
handleAdminAutopilot (coordinator/api/autopilot_handlers.go) |
admin |
Two registrations; controller status and runtime pause, not shadow/live promotion |
| GET | /v1/admin/autopilot/inventory |
handleAdminAutopilotInventory (coordinator/api/autopilot_handlers.go) |
admin |
Connected saved-approval aggregates, independent of the ledger |
| GET | /v1/admin/autopilot/machines |
handleAdminAutopilotMachines (coordinator/api/autopilot_handlers.go) |
admin |
Persisted desired machine modes and current verified sessions |
| PATCH | /v1/admin/autopilot/machines/{machine_id} |
handleAdminAutopilotMachines (coordinator/api/autopilot_handlers.go) |
admin |
Persist an exact machine's desired mode; no restart or implicit activation |
| GET | /v1/admin/autopilot/rewards |
handleAdminAutopilotRewards (coordinator/api/autopilot_handlers.go) |
admin |
Durable reward pool and paged enrollments, including offline/history-required machines |
| PATCH | /v1/admin/autopilot/rewards/pool |
same | admin, financial |
Set the separate cumulative absolute cap; never below spent funds |
| POST | /v1/admin/autopilot/rewards/machines/{machine_id}/baseline |
same | admin, financial |
Restore a missing first-ever opt-in baseline with evidence; no consent or cohort mutation |
| POST | /v1/admin/drain |
HandleAdminDrain (coordinator/api/operations/drain.go) |
admin |
Start a drain; default grace DefaultDrainGrace |
| GET | /v1/admin/routes, /v1/admin/routes/export |
HandleAdminRoutes, HandleAdminRoutesExport (coordinator/api/observation/admin_telemetry.go) |
admin-key |
Route records |
| GET | /v1/admin/rejections, /v1/admin/rejections/export |
HandleAdminRejections, HandleAdminRejectionsExport (coordinator/api/observation/admin_telemetry.go) |
admin-key |
Admission rejections; could_have_served is nullable: null means not evaluated. CSV uses an empty cell; `could_have_served=true |
| GET | /v1/admin/request-outcomes |
HandleAdminRequestOutcomes (coordinator/api/observation/request_outcome_admin.go) |
admin-key |
Bounded received cohort with versioned request/attempt evidence and current-process sink health; see accounting. |
| GET | /v1/admin/profiles, /v1/admin/profiles/export |
HandleAdminProfiles, HandleAdminProfilesExport (coordinator/api/observation/profiler_admin.go) |
admin-key |
Request profiles; see ../architecture/system-profiler.md |
| GET | /v1/admin/snapshots, /v1/admin/snapshots/export |
HandleAdminSnapshots, HandleAdminSnapshotsExport (coordinator/api/observation/profiler_admin.go) |
admin-key |
| Pattern | Handler | Behaviour |
|---|---|---|
/v1/ |
handleUnimplementedEndpoint |
Any /v1/* request matching no registered method+path — including a wrong method on a real path — gets 404 invalid_request_error with message endpoint <METHOD> <path> is not implemented |
Total: 4 + 9 + 10 + 3 + 15 + 13 + 6 + 6 + 5 + 3 + 1 + 45 + 1 = 121 registrations, matching routes().
Typed audio content (input_audio / audio_url), including malformed audio
parts and audio-bearing tool output, is ineligible for the text-only prefix
planner. Ordinary text mentioning audio and opaque tool argument strings are
not audio-presence signals. This exclusion does not enable audio routing or a
provider capability, and does not classify audio as vision for billing.
Sources: coordinator/api/inference/request/audio_cache_eligibility.go,
coordinator/promptcontract/endpoint_lower.go and
coordinator/promptsidecar/src/endpoint.rs.
GET /v1/cache/status returns aggregate operational state, with no provider,
model, tenant, prompt, token, hash, scope, or epoch identifiers
(ExactCacheStatus, coordinator/api/inference/exact_cache_status.go). Readiness counts
are advertised provider/model pairs, not unique models or guaranteed cache hits.
| JSON field | Meaning | Code |
|---|---|---|
artifact_allowlist.configured |
Whether the optional exact-artifact list is configured; false is unrestricted, true plus zero count denies all participation |
coordinator/api/inference/exact_cache_status.go (ExactCacheArtifactAllowlistStatus) |
artifact_allowlist.count |
Number of configured exact tuples; never returns their model IDs or hashes | Same |
artifact_allowlist.stale_models |
While routing is on, the number of catalog models that the list names only under a superseded weight hash or prompt contract. Each stays out of cache routing until its live tuple is appended; the coordinator log names that tuple once |
coordinator/api/inference/exact_cache_allowlist_staleness.go (missingAllowlistEntries); coordinator/internal/registry/cachepolicy/artifacts.go (ArtifactAllowlist.StaleFor) |
providers.v2_ready_models |
Ready durable SSD capabilities; preserves the existing meaning | coordinator/registry/cache_status.go (PrefixCacheProtocolStatus) |
providers.memory_ready_models |
Ready resident capabilities, counted separately from SSD readiness | coordinator/registry/cache_status.go (PrefixCacheProtocolStatus) |
lifecycle.fences_applied |
Proof-fence windows opened or escalated | coordinator/registry/cache_routing.go (CacheRoutingLifecycleStatus); coordinator/registry/cache_proof_fence.go (rejectCapability) |
lifecycle.fences_expired |
Windows that lifted by time, each counted once | Same; coordinator/registry/cache_proof_fence.go (countLapseLocked) |
lifecycle.persistence.enabled / .ready |
Persistence is started / restore has established the key generation. Holder and demand writes wait for ready; failed restores retry each flush tick |
coordinator/internal/registry/cachepersist/status.go (Status); coordinator/registry/cache_persistence_registry.go (runCacheRoutingPersistence) |
lifecycle.persistence.restored_holders / .restored_demand |
Holder rows accepted into the parked set during boot or retry, and restored demand entries accepted by the index. Loads enforce TTL, caps and clock-skew bounds | coordinator/internal/registry/cachepersist/restore.go (Restore, SeedDemandPersisted) |
lifecycle.persistence.pending_holders / .bound_holders / .dropped_pending |
Currently parked holders (not pending writes), cumulative bindings, and parked/restored rows dropped for expiry, capacity, identity mismatch, abandoned epoch or invalidation fences | coordinator/internal/registry/cachepersist/pending.go (Park, AddBound, prunePendingChunked); coordinator/registry/cache_persistence.go (bindRowsLocked, settleParkedChunk); coordinator/internal/registry/cachepersist/restore.go (Restore) |
lifecycle.persistence.key_rotated |
This boot reset a different recorded cache-key generation, covering the master key and all derivation versions. Excludes first initialization and completion of an in-progress reset | coordinator/internal/registry/cachepersist/restore.go (Restore) |
lifecycle.persistence.flushes / .flush_errors |
Nonempty or reset-only flush attempts, and their failures. Unwritten mutations stay pending; successful chunks clear only matching revisions | coordinator/internal/registry/cachepersist/flush.go (Flush, recordFlush); coordinator/internal/registry/cachepersist/mutations.go (acknowledgeHolders, acknowledgeDemand) |
lifecycle.persistence.rows_written / .rows_deleted |
Holder/demand rows successfully submitted in upsert chunks / holder keys successfully submitted in delete chunks. Deletes can name missing rows; neither is a current row count or a count of rows actually changed. Reset/prune deletions are excluded | coordinator/internal/registry/cachepersist/mutations.go (acknowledgeHolders, acknowledgeDemand) |
lifecycle.persistence.dropped_dirty |
Upsert/demand marks dropped at their pending cap, plus pending holder upserts discarded by delete-backlog overflow | coordinator/internal/registry/cachepersist/marks.go (MarkHolderUpsert, MarkDemand); coordinator/internal/registry/cachepersist/reset.go (requireResetLocked) |
lifecycle.persistence.overflow_resets / .stale_upserts |
Delete-backlog overflows requesting a durable reset (not completed resets) / holder upserts rejected because evidence is at or before a per-key invalidation or the process-lifetime overflow cutoff | coordinator/internal/registry/cachepersist/reset.go (requireResetLocked); coordinator/internal/registry/cachepersist/marks.go (MarkHolderUpsert); coordinator/internal/registry/cachepersist/delete_fences.go (tombstonedLocked) |
lifecycle.persistence.last_flush_ms / .last_flush_at |
Duration and completion time of the last counted flush, successful or failed, including a mid-flush reset. Time is UTC RFC 3339 and omitted before any counted flush | coordinator/internal/registry/cachepersist/flush.go (recordFlush); coordinator/internal/registry/cachepersist/status.go (Status) |
lifecycle.fenced_capabilities |
Currently fenced provider/model/tier capabilities | coordinator/registry/cache_proof_fence.go (sweepFencesLocked) |
lifecycle.demand_entries |
Entries currently in the observed-demand index | coordinator/registry/cache_demand.go (stats) |
lifecycle.demand_cap_evictions |
Demand entries evicted by the cap inside their TTL; a growing count means repeated prefixes are being reported as novel | Same |
lifecycle.attempt_bytes |
Logical bytes of retained cache-attempt records, against the 64 MiB attempt budget | coordinator/registry/cache_routing.go (CacheRoutingLifecycleStatus); coordinator/internal/registry/cachetracker/attempt_pressure.go (AttemptLifecycle) |
lifecycle.attempt_budget_refused |
Attempts dispatched without a cache scope because the byte budget refused their record: live records fill it, reclaiming at most 64 of the earliest-expiring completed attempts' grace records would not make room, or the record alone exceeds the budget. Monotonic within the current tracker generation | coordinator/internal/registry/cachetracker/cache_receipts_kernel.go (StoreAttemptLocked) |
lifecycle.attempt_grace_reclaimed |
Completed attempts' records reclaimed inside their two-minute terminal grace to admit another; each forfeits only a late write-behind READY. Monotonic within the current tracker generation | coordinator/internal/registry/cachetracker/attempt_pressure.go (reclaimTerminalGraceLocked, terminalBudgetVictimsLocked) |
The persistence field names are unchanged. Routing reads remain in memory; write-behind, restore and overflow guarantees are defined in cache persistence.
The artifact-list fields have Prometheus gauges
exact_cache_artifact_allowlist_configured, exact_cache_artifact_allowlist_count,
exact_cache_artifact_allowlist_stale_models and Datadog gauges
exact_cache.artifact_allowlist.configured, exact_cache.artifact_allowlist.count,
exact_cache.artifact_allowlist.stale_models; mode off remains authoritative
(coordinator/api/inference/exact_cache_metrics.go).
The additive resident count has Prometheus gauge
exact_cache_memory_ready_models and Datadog gauge
exact_cache.memory_ready_models (coordinator/api/inference/exact_cache_metrics.go).
The fence fields have Prometheus gauges exact_cache_fence{event}
(event ∈ applied, expired) and exact_cache_fenced_capabilities, and
Datadog gauges exact_cache.fence tagged event:applied|expired and
exact_cache.fenced_capabilities (same file).
The attempt fields have Prometheus gauges exact_cache_attempt_bytes,
exact_cache_attempt_budget_refused and exact_cache_attempt_grace_reclaimed,
and Datadog gauges exact_cache.attempt_bytes, exact_cache.attempt_budget_refused
and exact_cache.attempt_grace_reclaimed (same file).
The existing prefix_cache_statuses state/reason aggregates retain their SSD
meaning; resident routing uses the separate memory capability and bounded holder
receipts described in cache-aware routing.
The exact-cache lifecycle holder_removed map includes proof_mismatch, separate
from capability_change, and shorter_hit, separate from miss_invalidation: a
provider that proves a hit below a boundary it was recorded at loses its deeper
holders for that prompt in that tier, without a fence. proof_mismatch counts plan-scoped drops (anchor
mismatches, invalidateProviderPlan) and whole provider/model drops (identity
mismatches, invalidateProviderModel); the fence windows themselves are
defined in cache-aware routing.
Updating one model preserves unchanged models' holders,
pending receipts and proof fences. See coordinator/registry/cache_model_changes.go
and coordinator/registry/cache_receipt_result.go.
Per-model cache usage, accepted lookup and selection metrics are available only
through the existing authenticated GET /v1/admin/metrics endpoint and Datadog.
They add no model identifiers or fields to GET /v1/cache/status. See the
internal cache metric inventory
for cache_model_* labels and populations (coordinator/api/observation/cache_model_telemetry.go).
GET /v1/me/providers returns a reputation object on each machine with
total_jobs, successful_jobs, failed_jobs, total_uptime_seconds,
avg_response_time_ms, challenges_passed, and challenges_failed
(coordinator/internal/api/accounts/fleetview/provider_types.go, myReputation). The legacy object name is
retained for the raw metrics; its former score field has been removed.
Live and stored/offline snapshots have the same shape. The console displays
job counts, tokens, uptime, and response timing without a reputation rating.
Deploy the updated console before the coordinator field removal: older console
bundles dereference reputation.score and cannot consume the new response.
Existing tabs running an older bundle must reload. The updated console also
accepts older responses containing the extra field.
Inference planning may obtain exact input work from the verified model/template
tokenizer before dispatch. The internal numeric provenance is not a client
request field. Before preflight and dispatch, a verified exact count matching
the candidate provider's advertised artifact and renderer may correct the SLA's
input-token term upward or downward, measured from the original request
arrival and bounded by any earlier caller deadline. Calibrated uncertainty and
provider recount do not extend that duration. Planning, retries and hedges keep
the original arrival time and never start a fresh clock. Each provider's
feasibility and dispatched budget use its own qualified or fallback cutoff;
another provider's matching renderer cannot supply that qualification. An
unsent exact-bound attempt is rejected if its renderer changes before handoff.
When every otherwise-fitting provider's own cutoff has expired, admission returns
429 with Retry-After, retains the deadline_unreachable rejection reason and
does not spill to a cold provider. A physically fitting expired peer prevents a
too-small peer from turning that request into a permanent model-size refusal.
An eligible previous-build alias can still serve within its own cutoff.
Unsupported shapes keep
conservative fallback, and billing continues to settle actual provider usage
(planPromptRoute, coordinator/api/inference/prompt_work.go; Owner.PromptWorkDeadline,
coordinator/api/inference/first_content_prompt_deadline.go).
Public inference uses first-content routing
by default across chat completions, Responses, completions and Anthropic messages.
Internal retries, cache planning, quotes, queue waits and hedges consume the same
ingress-anchored budget. Predictive provider refusals do not count as node
health failures; after two, another attempt needs fresh feasible evidence.
A request can launch at most one speculative backup. Current error JSON and
Retry-After contracts remain; unavailable deadline-bound capacity can produce
an earlier overload response instead of waiting the queue maximum. Explicit
owner routing, deadline exemptions and valid empty completions keep their
existing contracts (coordinator/api/inference/first_content_retry.go,
coordinator/api/inference/first_content_preflight.go).
GET /v1/me/providers exposes the accepted backend slot snapshot through
backend_capacity.slots (HandleMyProviders, coordinator/api/accounts/providers.go). The
owner-only response also carries optional backend_capacity.load_usable_gb
(live no-eviction load memory before serving headroom),
backend_capacity.load_headroom_gb (activation plus minimum-KV reserve for
the current serving set), each model's estimated_memory_gb, and
capacity_model_ids: the catalog/capability-accepted subset to which the
canonicalized heartbeat slots and memory sample apply. The response also carries
capacity_accepted_at, the coordinator time of the last applied capacity
snapshot. A repeated or out-of-order capacity sequence can advance
last_heartbeat for liveness without refreshing this timestamp. The owner
load verdict uses capacity_accepted_at and withholds stale or absent samples.
models_replace clears this owner load evidence until an accepted heartbeat
for the replacement inventory arrives, including when a model ID is reused.
It also carries optional backend_capacity.load_transition_active, which marks
an in-flight model load or load-gate update before a slot exists. The provider
emits a capacity heartbeat when this transition changes. My Macs and the
coordinator attention count defer memory failures while it is active.
My Macs shows estimated_memory_gb + load_headroom_gb against load_usable_gb for
cold accepted models. A crashed slot retains weights and gets a separate
backend warning, not a cold-load verdict. Owner-only/off-catalog models remain
in models but are not assigned a load verdict from a different canonical inventory.
Missing fields from older providers mean unknown, never zero or
"fits"; a stale capacity sample, active or queued request, in-flight load or
reloading slot also withholds a definitive cold-load failure. The coordinator does not route
from these owner diagnostics.
The existing free_for_load_gb remains the routing input and may credit
eviction of idle slots, so it is not interchangeable with the no-eviction
preload budget. The
optional paged_storage object carries bounded allocator observations; omitted
fields mean uninstrumented. Its exact fields and sample-age rules live in the
wire reference. The coordinator
consumer is implemented; provider emission is pending. These observations do
not grant admission or assert cache readiness.
_darkbloom_prompt_date is reserved internal body context. The coordinator
overwrites caller input once with the request's UTC Gregorian YYYY-MM-DD
before lowering, cache planning, fallback and retries (parseInferencePrelude,
coordinator/api/inference/inference_preprocess.go; SetRequestDate,
coordinator/promptcontract/request_date.go). Local provider HTTP captures its
own date; callers cannot override it (LocalChatRequest,
provider-swift/Sources/ProviderCore/Server/LocalChatRequest.swift). It is not a
new envelope or canonical signature field. The renderer semantic version and
contract behavior are defined in prompt-contract sidecar.
| Header | Where | Meaning |
|---|---|---|
Authorization: Bearer <token> |
ExtractBearerToken |
The only credential header; scheme match is case-insensitive |
X-Request-ID |
loggingMiddleware |
Honoured if present, otherwise generated (newRequestID); echoed back and logged, never persisted |
Content-Type: application/eigeninference-sealed+json |
SealedTransport (coordinator/api/inference/sender_encryption.go) |
Switches the inference endpoint into sealed mode (SealedContentType) |
X-Darkbloom-Metadata-Details |
ApplyMetadataDetailsRequest (coordinator/api/inference/request/metadata_flags.go) |
Requests the extended metadata object (timing, location) on chat completions; ?metadata=details does the same |
X-Darkbloom-Route: self / prefer |
resolveSelfRoutePolicy (coordinator/api/inference/self_route.go) |
self restricts dispatch to the account's own machines; prefer tries them first and falls back to the fleet; see ../provider/self-route.md |
X-Darkbloom-Publishing-Key |
RequirePublishingAPIKey |
Publishing credential for /v1/admin/models/* |
Stripe-Signature |
HandleStripeWebhook, HandleStripeConnectWebhook |
Stripe webhook signature |
X-Webhook-Token |
HandleMDMWebhook |
MDM webhook secret (or ?token=) |
Origin |
corsMiddleware |
Allowed origins default to https://console.darkbloom.dev plus localhost dev ports unless EIGENINFERENCE_CONSOLE_URL overrides |
| Header | Where | When |
|---|---|---|
X-Request-ID |
loggingMiddleware |
Every response |
Retry-After |
rateLimitWithTier, applyKeyRPMLimit, WriteTokenRateLimited, DrainGate, shedIfModelRejected, writeTTFTTooSlow, writeServiceUnavailable, HandleLeaderboard (coordinator/api/reporting/leaderboard.go), Admission.Run, selfRouteUnavailable, preContentTerminal, the exhausted branch of dispatchState.run (coordinator/api/inference/dispatch.go) |
Every 429 (including the drain 429, coordinatorDrainRetryAfter); 503 service_unavailable, machine_offline (30 s), model_not_loaded (15 s), and 503 provider_error from dispatch exhaustion. Not set on 503 model_unavailable, 502, or 504. Leaderboard failures use the remaining leaderboardFailureBackoff rounded up to whole seconds (coordinator/internal/api/reporting/ranking/leaderboard_cache.go, LeaderboardRetryAfter). Admission values come from estimateRetryAfter (coordinator/api/inference/consumer.go), capped at maxDistressRetryAfter; a provider-forecast feasible_after_ms overrides it, clamped to 2–30 s |
X-RateLimit-Reset, x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests |
rateLimitWithTier, setRequestRateLimitHeaders |
Request-rate limited routes (rpm, fin); the first only on rejection |
x-ratelimit-limit-input-tokens, x-ratelimit-remaining-input-tokens, x-ratelimit-reset-input-tokens, and the -output-tokens triple |
setTokenRateLimitHeaders |
Inference responses when token limits are configured |
X-Timing |
writeTimingHeaderWithProfile (coordinator/api/inference/profiler_dispatch.go) |
Committed inference responses. A JSON object with the RequestTimingDetails fields (coordinator/api/types/types.go): parse_us, reserve_us, media_fetch_us, route_us, queue_us, encrypt_us, dispatch_us, provider_us, plus profiler-only additive keys (pre_handler_us, preflight_us, route_reserve_us, queue_pure_us, writer_us, socket_us, provider_ack_us, timing_anomaly) |
X-Inference-Job-ID |
WriteInferenceJobIDHeader (coordinator/api/inference/response/sse_response.go) |
Committed inference responses; the coordinator job id, which can differ from X-Request-ID across retries |
X-Provider-Id, X-Provider-Attested (true/false), X-Provider-Trust-Level, X-Provider-Chip, X-Provider-Model, X-Provider-Encrypted (only when true), X-Provider-Secure-Enclave (when known), X-Provider-Mda-Verified (only when true) |
WriteCommittedProviderHeaders (coordinator/api/inference/response/response_metadata.go) |
Committed inference responses; the same facts as the metadata object |
X-Attestation-Se-Public-Key |
WriteCommittedProviderHeaders |
When the provider attested with a Secure Enclave key; see ../consumer/verification.md |
X-Eigen-Sealed: true, X-Eigen-Sealed-Kid |
sealingResponseWriter (coordinator/api/inference/sender_encryption.go) |
Sealed-mode responses |
Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive |
WriteSSEResponseHeader (coordinator/api/inference/response/sse_response.go) |
Streaming responses, written at commit |
Cache-Control: public, max-age=300 |
HandleEncryptionKey |
/v1/encryption-key |
Access-Control-Allow-* |
corsMiddleware |
Methods GET, POST, PUT, PATCH, DELETE, OPTIONS; allowed request headers include Authorization, Content-Type, X-Darkbloom-Metadata-Details |
Every error body has one shape (errorResponse, writeJSON, WithCode in coordinator/api/httpx/json.go):
{
"error": {
"message": "human-readable text",
"type": "rate_limit_exceeded",
"code": "rate_limit_exceeded",
"param": "model"
}
}code mirrors type unless a handler overrides it (WithCode, e.g. payload_too_large, model_capability_unsupported); param is present only when a handler names the offending field (WithParam, e.g. "model" on model_not_found). Chat errors raised after a stream has committed cannot change the status line; they surface as a terminal SSE error event, without a normal-completion [DONE] (WriteChatStreamTerminalError, coordinator/api/inference/response/chat_metadata_stream.go; writeChatStreamProviderError, coordinator/internal/inference/relay/consumer_stream.go).
| Status | type values |
Raised by |
|---|---|---|
| 400 | invalid_request_error, invalid_sealed_envelope, kid_mismatch, decryption_failed, invalid_request, bad_request, referral_error, email_mismatch, wallet_mismatch |
Body/JSON validation, n > 1, tool-choice and vision rules, native media tools unsupported by a model's serving fleet (param: model), sealed-envelope faults, device-code and key-management input, unknown catalog ?type= |
| 401 | authentication_error, auth_error, unauthorized |
Missing/invalid bearer (RequireAuth, RequirePrivyAuth), no account user (RequirePrivyUser), release key |
| 402 | insufficient_funds (balance below the reservation), insufficient_quota (per-key spend cap); code is insufficient_quota for both |
reserveInferenceBalance (coordinator/api/inference/inference_balance.go); the per-cause table, including the provider-price 402, is Payment-required responses |
| 403 | forbidden, model_not_allowed, account_pending_deletion, invalid_confirm_token |
API key on a privy route; non-admin on an admin route; model outside the key's allowed_models (keyModelAllowed, coordinator/api/inference/key_policy.go); Privy login of an account that waits for erasure (writePrivyUserError, coordinator/api/access/auth.go); wrong or expired erasure confirm token |
| 404 | model_not_found, not_found, invalid_grant, invalid_code, referral_error, invalid_request_error |
Model or alias not in the catalog; unknown key id; device codes; /v1/ catch-all; state export when disabled |
| 409 | no_linked_machine, already_used, conflict, stripe_account_gone, stripe_account_recreate_required, open_withdrawal, erasure_conflict |
Self-route without a linked machine; device-approve replay; invite-code collision; Stripe Connect state; account erasure with a withdrawal in flight or in the wrong state |
| 410 | expired_token, expired_code |
expired device codes |
| 412 | precondition_failed |
State export without an encryption recipient |
| 413 | invalid_request_error (plain, or with code: payload_too_large) |
Inference body over maxInferenceBodyBytes (Limits and validation; parseInferencePrelude); admission rejects a prompt no provider can accept (Admission.Run) |
| 422 | invalid_request_error |
Tool-constraint schema the parser cannot compile (ValidateResolvedToolConstraintParser, coordinator/api/inference/request/tool_parser.go) |
| 426 | upgrade_required |
Log report from a provider below the minimum version |
| 429 | rate_limit_exceeded, machine_busy |
machine_busy: self-route (X-Darkbloom-Route: self) when the owned machine is at capacity, with Retry-After (Recorder.PreContentTerminal, coordinator/internal/inference/rejection/terminal.go). rate_limit_exceeded: key RPM, account RPM, input/output tokens per minute, coordinator drain (Retry-After = coordinatorDrainRetryAfter), admission shedding, model-rejection shedding, fleet TTFT too slow, and dispatch exhausted on capacity: every attempt refused for capacity, no provider produced first content within the deadline (the coordinator's own pre-content timeout is reclassified from 504 by retry.ClassifyExhaustedStatus, coordinator/internal/inference/retry/terminal_classification.go), or the request fits no provider; always with Retry-After |
| 500 | internal_error, server_error, auth_error, otp_error |
Store failures, token generation, account lookup, admin OTP delivery |
| 502 | provider_error, stripe_error |
Provider returned an error or no usable output; Stripe API failures |
| 503 | model_unavailable (no Retry-After; may carry code: model_capability_unsupported), service_unavailable, encryption_unavailable, machine_offline, model_not_loaded, billing_error, not_configured, provider_error |
No routable provider for the resolved model; no serving capacity (writeServiceUnavailable); sealing not configured; self-route machine states; ledger or Stripe not configured; Privy not configured for admin OTP; /readyz while draining; dispatch exhausted on a genuine provider 503; public stats/totals/series store failure with no usable cached body (see public stats) |
| 504 | timeout, provider_error |
timeout: non-streaming only, inferenceTimeout elapsed after commit while waiting for the response or its usage. provider_error: dispatch exhausted on a typed provider 504 (terminalCauseSafetyDeadline, terminalCauseBackpressureTimeout; isTypedTimeout504Cause, coordinator/api/inference/terminal_cause.go) |
When every dispatched provider rejects a request with the same deterministic client error (for example a chat template that cannot render the messages, or a body the provider caps), the provider's own 4xx status is passed through once as invalid_request_error with code: model_capability (or payload_too_large) rather than being retried or reclassified (terminalClientError handling in the exhausted branch of dispatchState.run, coordinator/api/inference/dispatch.go).
A client that disconnects before commit receives nothing; the coordinator records status 499 internally and cancels the provider job (sendProviderCancel, coordinator/api/inference/consumer.go).
Requests are decoded into a generic JSON object with json.Number preserved (parseInferencePrelude, coordinator/api/inference/inference_preprocess.go), so fields the coordinator does not interpret pass through to the provider. Fields it does interpret:
| Field | Handling |
|---|---|
model |
Required. Alias or build id, resolved by resolveRequestedModel (coordinator/api/inference/consumer.go); see ../consumer/models.md |
messages (Chat) / input (Responses) |
Required; missing → 400 |
stream |
SSE when true; the provider's usage chunk, when it sends one, is held and emitted at the end of the stream |
n |
Values above 1 → 400 invalid_request_error |
max_tokens, max_completion_tokens |
max_completion_tokens is mapped to max_tokens. An explicit value is passed through unchanged (not clamped); when none is set the coordinator fills in the output bound from pricing-model.md → Formulas (ensureMaxTokensBound, coordinator/api/inference/consumer.go) |
stop |
A single string is normalised to a one-element array in parseInferencePrelude |
tools, tool_choice, parallel_tool_calls |
Schemas normalised by NormalizeToolSchemas (coordinator/api/inference/request/toolschema.go); constraints validated by ValidateToolConstraintPolicy (coordinator/api/inference/request/tool_constraints.go) |
response_format |
Passed through to the provider without coordinator validation |
reasoning, reasoning_effort |
Applied per model policy by ApplyResolvedModelReasoningPolicy (coordinator/api/inference/request/reasoning_request_policy.go) |
provider and other routing hints |
Removed by StripProviderRoutingFields (coordinator/api/inference/request/request_introspection.go) |
user, metadata, safety_identifier, prompt_cache_key (top level) |
Accepted, then removed before the body is forwarded to a provider (the coordinator may add its own protocol-0 prompt_cache_key afterwards); see provider-bound caller fields |
image_url parts with http(s) URLs |
Fetched by the coordinator before dispatch (Bridge.Resolve, coordinator/internal/inference/media/media_resolve.go) |
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1725000000,
"model": "<the model string you sent>",
"choices": [
{ "index": 0,
"message": { "role": "assistant", "content": "…",
"reasoning": "…", "reasoning_content": "…", "reasoning_details": [ … ],
"tool_calls": [ … ] },
"finish_reason": "stop" }
],
"usage": {
"prompt_tokens": 12, "completion_tokens": 34, "total_tokens": 46,
"prompt_tokens_details": { "cached_tokens": 0 },
"completion_tokens_details": { "reasoning_tokens": 0 }
},
"se_signature": "…",
"response_hash": "…",
"metadata": { … }
}In Chat streams, usage may accompany the finish event or arrive in a separate
usage-only event. Validated cache and reasoning details are added to the
combined event only when no separate usage event follows; otherwise the
dedicated event owns those details. No usage object is invented when the
provider sends none (handleStreamingResponseWithFirstChunkAndError,
coordinator/internal/inference/relay/consumer_stream.go; FinalizeUsageChunk,
coordinator/api/inference/response/chat_stream_terminal.go).
model echoes the requested string, alias included (BuildNonStreamingResponse, coordinator/api/inference/response/chat_response.go). se_signature and response_hash are present when the provider signed the response; verification is described in ../consumer/verification.md. metadata is ChatCompletionMetadata:
| Field | Type | Meaning |
|---|---|---|
provider_id |
string | Provider that served the request |
provider_attested |
bool | Attestation passed |
provider_trust_level |
string | Trust tier from attestation |
provider_encrypted |
bool | Coordinator→provider payload was end-to-end encrypted |
provider_chip, provider_machine_model, provider_secure_enclave, provider_mda_verified |
string / bool | Hardware facts from attestation |
attestation_se_public_key |
string | Secure Enclave public key used to sign |
job_id |
string | Coordinator job id (also X-Inference-Job-ID) |
timing |
RequestTimingDetails |
Only with metadata details; the same fields as X-Timing |
location |
ProviderApproxLocation |
Only with metadata details: region, region_code, country, country_code, timezone |
Non-empty string instructions becomes a leading system message before input,
preserving whitespace, existing system/developer messages and tool history.
Missing, null or empty instructions add no message; other types return400.
Both serving and text-cache preparation use lowerResponsesWithContent
(coordinator/internal/promptcontract/endpoint/endpoint_lower_responses.go); Rust mirrors the
text-cache contract in lower_responses (coordinator/promptsidecar/src/endpoint.rs).
Routing and billing reservation estimates include the new system message before
lowering (routingShape, billingBytes, coordinator/api/inference/request/request_introspection.go).
Serving uses LowerResponsesInferenceBody
(coordinator/internal/promptcontract/endpoint/endpoint_responses_inference.go) to preserve
ordered inline media alongside text and function history. input_image accepts
a string image_url; canonical image_url and video_url parts accept their
{"url": ...} objects. These references must be inline data: URIs. Uploaded
file IDs, files, audio, unknown parts and the unsupported input_video alias
return400 rather than being omitted or fetched. The existing Chat remote-media
resolver policy is unchanged. Media-bearing function_call_output.output
arrays participate in vision routing and media-aware token estimates.
This serving path is distinct from LowerProviderBody, the text-only Go/Rust
cache-planning contract. That contract rejects media, including media in tool
outputs; accepting a Responses image for inference does not establish exact
coordinator cache-routing eligibility. Native model codec, media-size, context
and tool-capability checks still apply.
A Responses, Completions or Messages body that cannot be lowered for cache
planning is still served on its own endpoint, without a cache plan. The
coordinator counts that decision as lowering_unsupported in the cache-planning
decision telemetry (CachePlanner.EmitDecision, called from
handleGenericInference in coordinator/api/inference/consumer.go). The
response shape, status and error codes do not change.
Bodies are lowered into the chat pipeline (coordinator/internal/promptcontract/endpoint/endpoint_lower_responses.go) and the provider's chat output is raised back into ResponsesResponse (coordinator/api/types/types.go): id (resp_…), object, created_at, status, error, incomplete_details.reason, instructions, max_output_tokens, model, output[], parallel_tool_calls, temperature, tool_choice, tools, top_p, metadata, usage (input_tokens, input_tokens_details.cached_tokens, output_tokens, output_tokens_details.reasoning_tokens), se_signature, response_hash. Streams use event:-typed frames from response.created / response.in_progress through the item deltas to response.completed (or response.incomplete when truncated) and carry no data: [DONE] (NewResponsesStreamEmitter, coordinator/api/inference/response/responses_stream.go).
usage.total_tokens is always emitted as input_tokens + output_tokens, including
zero. Cached and reasoning token details are subsets, not additional tokens;
the same buildResponsesUsage constructor supplies direct, converted and
streamed responses (coordinator/api/inference/response/responses_response.go). This field changes
neither the underlying usage counts nor billing.
Final non-streaming reasoning output items carry status: completed, matching
the streaming emitter's closed reasoning items (appendResponsesOutputItems,
coordinator/api/inference/response/responses_response.go; ResponsesStreamEmitter.closeReasoning,
coordinator/api/inference/response/responses_stream.go). This item status does not override a
root response marked incomplete because generation reached its output limit.
/v1/completions and /v1/messages are lowered to the chat contract (coordinator/promptcontract/endpoint_lower.go, coordinator/internal/promptcontract/endpoint/endpoint_lower_messages.go); responses are re-shaped by coordinator/api/inference/response/generic_endpoint_response.go and streams by coordinator/internal/inference/relay/generic_endpoint_stream.go, which terminates with data: [DONE]. Usage reports a validated cache hit in each endpoint's own schema (completionsUsage, messagesUsage): /v1/completions adds usage.prompt_tokens_details.cached_tokens (a subset of prompt_tokens); /v1/messages reports cache_read_input_tokens and excludes those tokens from input_tokens, as Anthropic does. Streams carry the same object on their terminal event: the final text_completion chunk's usage for completions, and the message_delta usage for messages (its message_start still reports input_tokens: 0, because usage is known only at the end).
Built by handleStreamingResponseWithFirstChunkAndError (coordinator/internal/inference/relay/consumer_stream.go), coordinator/api/inference/response/sse_response.go, and coordinator/api/inference/response/chat_metadata_stream.go; ordering guarantees come from the dispatch state machine in coordinator/api/inference/dispatch.go.
-
Deferred commit. No status line, headers, or bytes are written until the first content chunk arrives from a provider (
commitFirstContent). Until then the coordinator can still fail over to another provider or return a JSON error with a real status code (Recorder.PreContentTerminal,coordinator/internal/inference/rejection/terminal.go). Clients see a delayed 200, never a 200 that turns into an error mid-preamble. -
Headers at commit:
Content-Type: text/event-stream,Cache-Control: no-cache,Connection: keep-alive,X-Inference-Job-ID(WriteSSEResponseHeader), plusX-Timingand theX-Provider-*headers. -
Each provider chunk is forwarded as one
data: <json>\n\nevent afterNormalizeSSEChunk(coordinator/internal/inference/sse/sse_normalize.go); the coordinator does not re-tokenise or coalesce content. Chunks that arrive before commit are buffered (chunkBufferSize= 256). -
Usage and finish chunks are held. A chunk that only carries
usage(parseUsageOnlyStreamChunk) is held so the reasoning-token breakdown can be spliced in; the chunk carrying the terminalfinish_reason(parseFinishStreamChunk) is held so it can be corrected tolengthagainst the authoritative token counts. Both are written after every content delta.se_signature,response_hashand opt-inmetadataride on the held usage chunk; when there is none they are emitted as one additional fully-shapedchat.completion.chunk(NewChatCompletionExtrasEvent) immediately before termination. Every chunk'smodelis rewritten to the alias you sent (rewriteChunkModel).Coordinator-authored extras, including metadata before an in-band error, reuse the first observed Chat response
idand its validcreatedtimestamp (ChatStreamIdentity.observe,coordinator/api/inference/response/chat_stream_identity.go). Provider frames are not rewritten for this purpose; signature/hash values are preserved. The coordinator's job identity stays inX-Inference-Job-IDandmetadata.job_id, not in a new response ID. If no valid provider ID has been observed, the existingchatcmpl-<job-id>fallback is used. -
Termination: exactly one
data: [DONE]\n\n, written by the coordinator after every coordinator-appended event. Any[DONE]from the provider is stripped first (stripSSEDoneEvents). Responses streams end withresponse.completed/response.incompleteinstead. -
No keepalives. The coordinator never writes comment frames or pings; a silent stream means the provider has not produced a token. Before commit a first-content deadline bounds the silence only for accounts selected by
EIGENINFERENCE_FIRST_CONTENT_SLA_ACCOUNTS(a miss is answered with 429 +Retry-After, see the status table). Other accounts have no first-content timeout and remain subject to client cancellation and provider-disconnect cleanup; after commitinferenceTimeoutbounds it (a terminalerrorevent of typetimeout). -
Chat errors after commit are one terminal
data: {"error": {...}}event, without[DONE]; optional authoritative metadata precedes it. -
Sealed mode seals each SSE event individually (see below).
| Rule | Value / behaviour | Symbol |
|---|---|---|
| Non-streaming provider output | Default 64 MiB of decrypted chunk data and 262,144 frames per attempt, inclusive. All four inference endpoints share the limits, including first/preamble chunks, reasoning, tool calls and usage JSON. Overflow cancels the attempt and returns 502 provider_error; no partial success. Streaming consumers keep the existing backpressure policy. Limits are deployment-configurable; see configuration. |
NewBudget (coordinator/internal/inference/responselimit/nonstream_response_limit.go), HandleChunk (coordinator/api/inference/provider_inference.go), NonStream (coordinator/internal/inference/relay/consumer_response.go) |
| Global request body | 64 MiB ceiling on every request (maxRequestBodyBytes, bodyLimitMiddleware) |
coordinator/api/server.go, coordinator/internal/api/middleware/middleware.go |
| Inference body | 16 MiB (maxInferenceBodyBytes) → 413 invalid_request_error; sealed bodies are read with the same cap (400 invalid_request_error when exceeded) |
parseInferencePrelude (coordinator/api/inference/inference_preprocess.go), SealedTransport (coordinator/api/inference/sender_encryption.go) |
| Console chat preflight | MAX_CHAT_REQUEST_BYTES = 16 * 1024 * 1024; send and retry measure the complete serialized UTF-8 body and, when encryption is enabled, the actual sealed envelope before POST /api/chat. Exactly at the cap is allowed. Oversized requests produce a local error and retain the user message/images in current chat state. Uploads still allow four images, each up to 10 MiB; base64, history and envelope overhead can make an accepted upload too large to send. Coordinator rewrite and final-forwarding checks remain authoritative |
assertChatRequestBudget (console-ui/src/lib/chat/request-budget.ts), streamChat (console-ui/src/lib/chat/stream.ts), useChatStream (console-ui/src/hooks/useChatStream.ts) |
| Control-plane bodies | 64 KiB (maxControlPlaneBodyBytes) for enroll, device token, admin auth |
coordinator/api/server.go |
| MDM webhook body | 1 MiB (maxMDMWebhookBodyBytes) |
HandleMDMWebhook (coordinator/api/provider/trust/settings.go) |
n |
Must be 1 | HandleChatCompletions |
max_tokens |
max_completion_tokens → max_tokens; an explicit value is not clamped, a missing one is filled from the output bound |
ensureMaxTokensBound |
| Prompt size at admission | 413 payload_too_large when the estimated prompt exceeds what the model's providers can accept |
Admission.Run (coordinator/api/inference/inference_admission.go) |
| Catalog membership | Model resolved but absent from the routable catalog → 404 model_not_found, after the balance reservation is released |
HandleChatCompletions |
| Key allow-list | model not in the key's allowed_models → 403 model_not_allowed |
keyModelAllowed |
tool_choice |
"none", "auto", "required", or {"type": "function", "function": {"name": …}}; a named function must exist in tools; required or a named choice with no tools → 400 |
ValidateToolConstraintPolicy |
| Tool schemas | Normalised to strict JSON Schema before dispatch; schemas the constraint parser cannot compile → 422 | NormalizeToolSchemas, ValidateResolvedToolConstraintParser |
| Vision | Image parts require a vision-capable model, otherwise 400; a vision model with no vision-capable provider online → 503 model_unavailable |
DetectMediaRequirement (coordinator/api/inference/request/request_introspection.go), visionToolsFailFast (coordinator/api/inference/inference_preprocess.go) |
| Remote images | http(s) image_url parts are gated before dispatch and fetched by the coordinator; the fetch is billed as media |
Bridge.Gate, Bridge.Resolve (coordinator/internal/inference/media/media_resolve.go); callbacks bound in coordinator/api/inference/media_bridge.go |
| Forced media tools / media tool results | Requires explicit per-model native media-tool capability. A public model served only by providers lacking it → 400, param: model; no currently eligible capable provider → 503. Applies to required/named tools with media and media-bearing tool results even with tool_choice: none. Other vision/tool checks remain; response_format is not validated by the coordinator |
nativeMediaToolsFailFast, coordinator/api/inference/native_media_tools.go |
| Token rate limits | Per-account input and output tokens per minute → 429 with Retry-After |
applyTokenRateLimitWithAdmission, WriteTokenRateLimited |
| Model shedding | A model currently rejecting → 429 with Retry-After from estimateRetryAfter |
shedIfModelRejected |
| Media preparation memory | Provider reason media_memory_unavailable permits bounded failover and eventual 429 when no candidate serves it. It does not invalidate a provider's text capacity or mark its engine unhealthy |
IsProviderHealthNeutralErrorReason, Classify; coordinator/internal/inference/failure/failure_reasons.go, coordinator/internal/inference/rejection/inference_failure_class.go |
| Constant | Value | Where | Effect |
|---|---|---|---|
inferenceTimeout |
600 s | coordinator/api/inference/consumer.go |
Streaming: maximum silence between chunks (the timer resets on every chunk) → terminal SSE error event, type timeout. Non-streaming: remaining response wait after first-content commit → 504 timeout |
defaultFirstContentDeadlineBase |
the compiled default of EIGENINFERENCE_TTFT_LIVE_DEADLINE_BASE_MS |
coordinator/api/inference/consumer.go |
Fallback base of the request-absolute first-content deadline for selected accounts when the variable is unset. Other accounts have no SLA deadline or provider budget. Deadline = CoordinatorFirstContentDeadline(model, promptTokens, base) = base + 1 ms per estimated prompt token, tightened per model by exact-model overrides (coordinator/modelpolicy/first_content_deadline.go, replaceable via EIGENINFERENCE_MODEL_FIRST_CONTENT_BASES). Expiry before any content → 429 rate_limit_exceeded + Retry-After (the pre-content 504 is reclassified by classifyExhaustedStatus) |
preambleContentTimeout |
90 s | coordinator/api/inference/consumer.go |
For SLA-selected accounts, cap from a provider's first preamble chunk (role delta / Responses lifecycle event, nothing written to the client yet) to its first content chunk; a provider that stalls after preamble fails over instead of holding the request for inferenceTimeout. Never exceeds the remaining first-content budget |
maxDispatchAttempts |
64 | coordinator/api/inference/consumer.go |
Upper bound on provider attempts per request |
chunkBufferSize |
256 | coordinator/api/inference/consumer.go |
Pre-commit chunk buffer per attempt |
apiKeyCacheTTL |
60 s | coordinator/api/access/cache.go |
API-key lookups are cached; a revocation takes effect within one TTL |
coordinatorDrainRetryAfter / DefaultDrainGrace |
3 s / 600 s | coordinator/api/operations/drain.go |
Retry-After on the drain 429; default drain window |
DeviceCodeExpiry / DeviceCodePollInterval |
see Device-code flow | coordinator/api/access/device/handlers.go |
Device-code lifetime and poll interval |
maxLogReportBodySize |
10 MB | coordinator/api/operations/log_reports.go |
Provider log upload cap |
degradedRouteEWMAThresholdMs / maxDistressRetryAfter |
1000 ms / 60 s | coordinator/api/inference/consumer.go |
Input threshold and cap for estimateRetryAfter |
DefaultRetryAfter / maxRetryAfter |
1 s / 60 s | coordinator/ratelimit/ratelimit.go |
Clamp for limiter Retry-After |
exactCacheStatusCacheTTL |
1 s | coordinator/api/inference/exact_cache_status.go |
/v1/cache/status cache |
Two distinct version values govern providers:
LatestProviderVersion(coordinator/api/server.go) is the source's provider-version display fallback.HandleVersion(/api/version) and/v1/me/summaryreport the highest active release in the store and fall back to this constant when none is registered. With production App Attest serving enabled,/api/versionreturns 503 instead of a download fallback when release authorization is unavailable. Preparing a source bump does not create a release row or alter/v1/releases/latest.EIGENINFERENCE_MIN_PROVIDER_VERSION(MinProviderVersion,coordinator/api/provider/trust/owner.go;SetMinProviderVersion) is the routing floor: a provider that registers or re-attests below it stays connected but is marked not runtime-verified and excluded from routing (BelowMinProviderVersionincoordinator/api/provider/trust/owner.go, applied at registration, inapplyChallengeMinVersionPolicyand in manifest sync). While a floor is set, a provider that reports no version counts as below it.- Request-shape gates exclude providers from specific request traits rather than the whole model, and they key on advertised capabilities, not versions: inference-enforced
tool_choice(required/named) needs the model's tool-constraint advertisement (providerSupportsToolConstraintLocked,coordinator/registry/tool_constraints.go), and a build reportingtemplate_render_ok=falseserves no request for that model (providerEligibleForTraitsLocked,coordinator/registry/request_traits.go). Servability and pooled admission assume the routed fleet is past the routing floor and carry no version branches. When no provider clears a gate for a request, the client sees 503model_unavailable(or 400param: tool_choicewhen the fleet serves the model but no provider advertises the tool-constraint protocol).
A consumer never sees a version error directly; an under-served model surfaces as 503 model_unavailable.
The exact Flash-Next registry ID also has an all-request version floor
(qwen4RegistryMinimumProviderVersion, coordinator/registry/qwen4_model_policy.go). See the
native identity routing gate;
a catalog listing alone does not grant an older provider the matching policy.
Sealed mode hides request and response bodies from TLS-terminating intermediaries in front of the coordinator. It is opt-in per request and implemented in coordinator/api/inference/sender_encryption.go (HandleEncryptionKey, SealedTransport, sealingResponseWriter). The cryptographic construction and threat model are in ../architecture/security/encryption.md; this section is only the wire contract.
-
GET /v1/encryption-key(no auth) returns{ "kid": "…", "public_key": "<base64 32-byte X25519>", "algorithm": "x25519-nacl-box" }. -
Send the inference request with
Content-Type: application/eigeninference-sealed+jsonand body{ "kid": "<kid from step 1>", "ephemeral_public_key": "<base64 32-byte X25519>", "ciphertext": "<base64: 24-byte nonce || NaCl box>" }ciphertextseals the ordinary JSON request body to the coordinator key with your ephemeral key.kidis optional but, when present, must match → otherwise 400kid_mismatch; a malformed field → 400invalid_sealed_envelope; authentication failure → 400decryption_failed; sealing not configured → 503encryption_unavailable. The decrypted body is then handled exactly like a plaintext request. -
Responses carry
X-Eigen-Sealed: trueandX-Eigen-Sealed-Kid: <kid>. A non-streaming response hasContent-Type: application/eigeninference-sealed+jsonand body{ "kid": "…", "ciphertext": "…" }whose plaintext is the normal JSON response. A streaming response keepsContent-Type: text/event-stream; every complete SSE event (split at\n\n, including thedata: [DONE]frame) is sealed as a whole and re-emitted asdata: <base64 nonce || box>\n\n, so the client decrypts each frame to recover the original event text. -
Errors produced before the sealed layer runs (401, 429 including drain) are plain JSON.
APIKeyResponse (coordinator/api/types/types.go): id (key_<hex>, GenerateKeyID), name, label, disabled, limit_usd, limit_reset, usage_usd, remaining_usd, rpm_limit, itpm_limit, otpm_limit, allowed_models, self_route_only, expires_at, created_at, last_used_at. The secret is KeyPrefix (sk-db-) + 64 hex characters (GenerateRawKey, coordinator/store/apikey.go), is returned only by create and rotate, and is stored only as its SHA-256 hash (hashKey, coordinator/store/postgres/). POST /v1/keys and PATCH /v1/keys/{id} accept name, limit_usd, limit_reset, rpm_limit, itpm_limit, otpm_limit, allowed_models, expires_at, self_route_only; PATCH also accepts disabled (coordinator/api/types/types.go).
See the Device-code flow table for the three bodies. verification_uri is <console>/link when EIGENINFERENCE_CONSOLE_URL is set, else <scheme>://<request host>/link (HandleDeviceCode).
For payout_rail=global, submit {amount_usd, method:"standard", quote_id} to the existing withdrawal endpoint. A confirmed quote returns its original withdrawal on retry. The response/history include payout_rail, destination_amount, payout_currency and refunded. Confirmed withdrawal states are queued, pending, processing, posted, failed, canceled and returned; posted does not establish bank receipt. Quotes expire before first confirmation; an already-submitted withdrawal can still be checked with the same ID (coordinator/api/billing/payouts/global_payouts_withdraw.go, maybeGlobalWithdraw).
DELETE /v1/billing/stripe/account removes a Global Payouts recipient mapping first, when present, and preserves any stored Connect destination; that older destination may become visible again. Otherwise it clears the Connect mapping. Responses are {unlinked:true} when a mapping was removed and {unlinked:false} when neither exists. Stripe accounts remain open and submitted withdrawals keep their recorded destination (coordinator/api/billing/payouts/connect_unlink.go, HandleStripeUnlink).
An unsubmitted confirmation invalidated by paused admissions returns 409 quote_paused; changed payout settings return 409 payout_changed. The browser releases that saved confirmation. Invalidation is atomic with BeginGlobalPayout; if another confirmation has already debited, the endpoint returns/reconciles the existing withdrawal instead. A recipient minimum/maximum violation returns 400 recipient_amount_limit with the threshold in local currency (coordinator/api/billing/payouts/global_payouts_withdraw.go, maybeGlobalWithdraw, HandleGlobalPayoutQuote).
An unknown payout outcome held for manual reconciliation remains status=pending and exposes failure_reason=manual_reconciliation_required. History displays Needs review; the debit remains reserved, and automatic scans and repeated confirmations do not resubmit or refund it (coordinator/store/global_payouts.go, GlobalPayout.RequiresManualReconciliation; coordinator/api/billing/payouts/global_payouts_history.go, globalWithdrawalView).
Four admin routes erase one account's personal data: plan, confirm, status
and cancel (coordinator/api/accounts/erasure/handlers.go; routes in
coordinator/api/routes.go). How erasure works is in
account erasure; the procedure is the
runbook; the per-column rules are in
personal-data rules.
Each route is wrapped in RequireAuth and then checks IsAdminAuthorized
(coordinator/api/access/authorize.go). Send one of:
Authorization: Bearer <EIGENINFERENCE_ADMIN_KEY>, recorded as actoradmin_key;Authorization: Bearer <Privy JWT>of a user whose email is inEIGENINFERENCE_ADMIN_EMAILS, recorded as actoraccount:<account_id>(adminActor). The actor never holds an email.
| Status | type (= code) |
When |
|---|---|---|
| 401 | authentication_error |
No bearer token, an invalid Privy JWT, or an unknown API key (RequireAuth) |
| 403 | forbidden |
A valid credential that is not an admin (IsAdminAuthorized) |
| 400 | invalid_request_error |
Body is not JSON (invalid JSON) |
| 413 | invalid_request_error |
Body over maxControlPlaneBodyBytes (64 KiB) |
| 500 | internal_error |
Any other store error (account erasure failed) |
Errors use the error envelope;
writeErasureError maps the store errors:
| Store error | Status | type |
param |
|---|---|---|---|
store.ErrNotFound |
404 | not_found |
— |
store.ErrErasureConfirmToken |
403 | invalid_confirm_token |
— |
store.ErrErasureWalletMismatch |
400 | wallet_mismatch |
wallet_addresses |
store.ErrErasureEmailMismatch |
400 | email_mismatch |
email |
store.ErrErasureOpenWithdrawal |
409 | open_withdrawal |
— |
store.ErrErasureConflict |
409 | erasure_conflict |
— |
POST /v1/admin/accounts/{account_id}/erasure/plan. A dry run. It changes no account data. Handler HandlePlan.
Request body (optional; an empty body is allowed):
| Field | Type | Required | Meaning |
|---|---|---|---|
wallet_addresses |
array of string | no | Wallet addresses of the retired on-chain payments to replace in payments and provider_payouts. Trimmed, de-duplicated and sorted (erasure.NormalizeWallets); give each one exactly as stored |
Response 200 (erasurePlanResponse, embeds store.ErasurePlan):
| Field | Type | Meaning |
|---|---|---|
account_id |
string | The account |
email |
string | The account email, for the admin to check; not stored |
stripe_objects |
array of {target, id} |
Every Express account, Global Payouts recipient and Checkout Session the account used; not stored |
wallets |
array of ErasureWalletCount |
Rows that hold each named address; empty when none was named ([] from Postgres, null from the memory store) |
rows, retained, stripe_object_counts, balance_micro_usd, withdrawable_micro_usd, open_withdrawals |
ErasureCounts fields |
What the scrub would change now |
request_id |
string | The planned request |
confirm_token |
string | 64 hex characters; the only copy (the store keeps its SHA-256) |
confirm_expires_at |
RFC 3339 time | Now + erasureConfirmTTL (15 minutes) |
grace_seconds |
integer | The configured grace period in seconds |
| Status | type |
When |
|---|---|---|
| 404 | not_found |
No user with this account ID |
| 409 | erasure_conflict |
A request is pending, or the account is already erased (the user row is not live) |
Side effects: stores or replaces the account's planned request with the
actor, the counts, the token hash, the wallet-list hash and the expiry
(SaveErasurePlan). A new plan makes the earlier token invalid.
Example (seedErasureHTTPAccount, coordinator/tests/api/accounts/contracts/erasure_test.go;
rows trimmed from 32 entries to 2):
POST /v1/admin/accounts/acct-erase-http/erasure/plan
Authorization: Bearer admin-key{
"account_id": "acct-erase-http",
"email": "person@example.com",
"stripe_objects": [],
"wallets": null,
"rows": [
{"rule": "users", "table": "users", "columns": ["email", "privy_user_id", "stripe_account_id", "stripe_account_status", "stripe_account_country", "stripe_destination_type", "stripe_destination_last4"], "action": "update", "rows": 1},
{"rule": "device_codes", "table": "device_codes", "action": "delete_row", "rows": 0}
],
"balance_micro_usd": 4000000,
"withdrawable_micro_usd": 0,
"open_withdrawals": 0,
"request_id": "04f52b57-c8ff-4d94-9e83-dec049a2e82a",
"confirm_token": "73809a8a07d09c19c7679467262bca4d435277d16de101865c8414e881fb947b",
"confirm_expires_at": "2026-10-04T17:40:47.306196Z",
"grace_seconds": 2592000
}POST /v1/admin/accounts/{account_id}/erasure. Soft deletes the account and starts the grace period. Handler
HandleRequest; store RequestAccountErasure.
Requires EIGENINFERENCE_SOFT_DELETE_MUTATIONS_ENABLED=true (default false).
After admin authorization, a disabled gate returns 503 with error type and
code soft_delete_mutations_disabled, before body parsing or mutation,
including when force is requested. Plan, status and cancel remain available;
already-pending scrub and outbox delivery continue. Disabling the gate does not
undo deletion or remove read/credit fences.
| Field | Type | Required | Meaning |
|---|---|---|---|
account_id |
string | yes | Must equal the path's account ID |
confirm_token |
string | yes | The token from the latest plan |
email |
string | yes when the account has an email | Must equal the account email, ignoring case and outer spaces |
wallet_addresses |
array of string | yes when the plan named any | Must be the plan's list (compared by hash after normalizing) |
reason |
string | no | Free text kept in erasure_requests.reason; write no personal data |
force |
bool | no | true scrubs at once instead of waiting for scrub_after |
Response 200: {"request": ErasureRequest}, the request in pending, or in
erased when force succeeded.
| Status | type |
When |
|---|---|---|
| 400 | invalid_request_error, param: confirm_token |
confirm_token is empty |
| 400 | invalid_request_error, param: account_id |
account_id differs from the path |
| 404 | not_found |
No user with this account ID |
| 403 | invalid_confirm_token |
No planned request, a wrong token, or an expired token |
| 400 | email_mismatch |
Email differs |
| 400 | wallet_mismatch |
Wallet list differs from the plan |
| 409 | open_withdrawal |
A withdrawal is open (open withdrawals) |
| 409 | erasure_conflict |
The request is already pending, or the user is already soft deleted |
| 409 or 500 | — (not the error envelope) | force only: the soft delete committed but the scrub failed. Body {"request": ErasureRequest, "scrub_error": "<error text>"}; 409 for an open withdrawal, 500 otherwise. The request stays pending and the loop retries |
The checks run in this order: admin authorization, mutation gate, body, token present, account ID, then the store checks (user, request state, token, email, wallets, withdrawals).
Side effects, in one transaction: deleted_at on the user and its providers;
API keys and provider tokens get active = false and deleted_at; the
request becomes pending with requested_at, scrub_after = now + grace,
reason, the wallet list, and a cleared token. After the commit the handler
clears the API key cache, disconnects the account's providers
(registry.DisconnectAccount) and logs account erasure requested
(request_id, account_id, actor, providers_disconnected, force).
With force, the scrub (Owner.scrub, coordinator/api/accounts/erasure/loop.go) runs next.
Example (TestAdminErasureHTTPFlow; summary.planned.rows trimmed to the users rule):
POST /v1/admin/accounts/acct-erase-http/erasure
Authorization: Bearer admin-key
Content-Type: application/json
{"account_id": "acct-erase-http", "confirm_token": "73809a8a…947b", "email": "person@example.com", "reason": "ticket 42"}{
"request": {
"id": "5aeaf3af-e434-4fe0-b3de-c1b3950ab8e3",
"account_id": "acct-erase-http",
"actor": "admin_key",
"reason": "ticket 42",
"state": "pending",
"summary": {
"planned": {"rows": [{"rule": "users", "table": "users", "columns": ["email", "privy_user_id", "stripe_account_id", "stripe_account_status", "stripe_account_country", "stripe_destination_type", "stripe_destination_last4"], "action": "update", "rows": 1}], "balance_micro_usd": 4000000, "withdrawable_micro_usd": 0, "open_withdrawals": 0}
},
"wallet_address_count": 0,
"requested_at": "2026-10-04T17:25:55.365729Z",
"scrub_after": "2026-11-03T17:25:55.365729Z",
"created_at": "2026-10-04T12:25:55.364872-05:00"
}
}A wrong email answers:
{"error": {"code": "email_mismatch", "message": "email does not match the account email shown in the plan", "param": "email", "type": "email_mismatch"}}GET /v1/admin/accounts/{account_id}/erasure. The newest request of the account, its outbox rows and its refused credits.
Handler HandleStatus. No side effects.
| Field | Type | Meaning |
|---|---|---|
request |
ErasureRequest |
The newest request (GetLatestErasureRequest, by created_at) |
outbox |
array of ErasureOutboxItem |
Outbox rows of that request; late external results can be staged during the grace period, and the worker delivers only after scrub |
refused_credits |
array of ErasureRefusedCredit |
Credits refused after the erasure, oldest first, at most 500 |
| Status | type |
When |
|---|---|---|
| 404 | not_found |
The account has no erasure request |
Example after a forced scrub and one late 9 µUSD refund
(TestAdminErasureHTTPFlow account, then a store.LedgerRefund credit as in TestAdminErasureConfirmBindsAccountAndWallets; rows trimmed to the users rule):
{
"request": {
"id": "df248e6b-78ff-49b8-93b2-847891c6cebf",
"account_id": "acct-erase-http",
"actor": "admin_key",
"state": "erased",
"summary": {
"planned": {"rows": [{"rule": "users", "table": "users", "columns": ["email", "privy_user_id", "stripe_account_id", "stripe_account_status", "stripe_account_country", "stripe_destination_type", "stripe_destination_last4"], "action": "update", "rows": 1}], "balance_micro_usd": 4000000, "withdrawable_micro_usd": 0, "open_withdrawals": 0},
"applied": {"rows": [{"rule": "users", "table": "users", "columns": ["email", "privy_user_id", "stripe_account_id", "stripe_account_status", "stripe_account_country", "stripe_destination_type", "stripe_destination_last4"], "action": "update", "rows": 1}], "balance_micro_usd": 4000000, "withdrawable_micro_usd": 0, "open_withdrawals": 0}
},
"wallet_address_count": 0,
"requested_at": "2026-10-04T17:26:07.978892Z",
"scrub_after": "2026-11-03T17:26:07.978892Z",
"erased_at": "2026-10-04T17:26:07.978894Z",
"created_at": "2026-10-04T12:26:07.978735-05:00"
},
"outbox": [
{"id": "100279eb-6122-429b-bc0f-d412ccbeac1f", "request_id": "df248e6b-78ff-49b8-93b2-847891c6cebf", "target": "erasure_log", "state": "pending", "attempts": 0, "next_at": "2026-10-04T17:26:07.978894Z", "has_external_id": false, "created_at": "2026-10-04T17:26:07.978894Z"}
],
"refused_credits": [
{"id": 1, "account_id": "acct-erase-http", "entry_type": "refund", "amount_micro_usd": 9, "reference": "late-refund", "created_at": "2026-10-04T12:26:07.979046-05:00"}
]
}summary.applied.balance_micro_usd is the balance the scrub forfeited, not
the balance after it. In this example the account had no Stripe objects and
the outbox worker had not run yet.
After the outbox worker
runs, rows move to done or manual_action. Outbox rows of an account with
an Express account and one Checkout Session, after one worker pass with the
Stripe fake answering the delete and the job create
(TestErasureOutboxLoopDeliversScrubRows, coordinator/tests/api/accounts/contracts/erasure_outbox_test.go):
[
{"id": "07c25b2b-041a-4498-bcd2-a99d918d8680", "request_id": "5ce002e2-1f7e-4cb8-b7d7-ee81386e5e70", "target": "stripe_account", "state": "done", "attempts": 0, "next_at": "2026-10-04T17:50:49.453053Z", "done_at": "2026-10-04T17:50:49.453053Z", "has_external_id": false, "has_stripe_job": false, "created_at": "2026-10-04T17:50:49.452161Z"},
{"id": "102b9fb9-4aaa-4550-9c6b-55c5c700d249", "request_id": "5ce002e2-1f7e-4cb8-b7d7-ee81386e5e70", "target": "checkout_sessions", "state": "pending", "attempts": 0, "next_at": "2026-10-04T17:55:49.453053Z", "has_external_id": true, "has_stripe_job": true, "created_at": "2026-10-04T17:50:49.452161Z"},
{"id": "28f5c1ab-ee86-4434-98bb-91600ec7da0d", "request_id": "5ce002e2-1f7e-4cb8-b7d7-ee81386e5e70", "target": "erasure_log", "state": "done", "attempts": 0, "next_at": "2026-10-04T17:50:49.453373Z", "done_at": "2026-10-04T17:50:49.453373Z", "has_external_id": false, "has_stripe_job": false, "created_at": "2026-10-04T17:50:49.452161Z"}
]The checkout_sessions row waits for its redaction job; its next_at is
the next poll, erasureRedactionPoll later.
POST /v1/admin/accounts/{account_id}/erasure/cancel. Ends a pending request before scrub_after. No body. Handler
HandleCancel; store CancelAccountErasure.
Response 200: {"request": ErasureRequest} in canceled, with
canceled_by and canceled_at.
| Status | type |
When |
|---|---|---|
| 404 | not_found |
No user, or no planned or pending request (also after the scrub) |
| 409 | erasure_conflict |
The open request is planned, or scrub_after has passed |
Side effects: deleted_at cleared on the user and its providers; the
request's wallet list cleared; log account erasure canceled. API keys and
provider tokens stay revoked, and disconnected providers stay disconnected
until they link again.
All in coordinator/store/erasure_types.go. Times are RFC 3339. Fields marked
"omitted" are left out when empty.
| Type | Field | Type | Meaning |
|---|---|---|---|
ErasureRequest |
id, account_id |
string | Request and account |
actor |
string | admin_key or account:<id> of the last planner or confirmer |
|
canceled_by |
string, omitted | Actor of the cancel | |
reason |
string, omitted | From the confirm call | |
state |
string | planned, pending, erased, canceled |
|
summary |
{planned, applied} |
Each an ErasureCounts, omitted until set |
|
confirm_expires_at |
time, omitted | Only while planned |
|
wallet_address_count |
integer | Wallet addresses stored at confirm; 0 after the scrub or a cancel | |
requested_at, scrub_after, erased_at, canceled_at |
time, omitted | Step times | |
last_error |
string, omitted | Last scrub failure while pending |
|
created_at |
time | Insert time | |
ErasureCounts |
rows |
array of ErasureRowCount |
One per rule, in rule order |
retained |
array of {table, rows, reason}, omitted |
Rows kept because another account shares them (retained data) | |
stripe_object_counts |
map of target to integer, omitted | Stripe objects per outbox target | |
balance_micro_usd, withdrawable_micro_usd |
integer | Balance at plan time, or the forfeited balance in applied |
|
open_withdrawals |
integer | Open withdrawals at plan time | |
ErasureRowCount |
rule, table |
string | Rule name and table |
columns |
array of string, omitted | Columns the rule changes; omitted for delete_row |
|
action |
string | update or delete_row |
|
rows |
integer | Rows counted (plan) or changed (applied) | |
ErasureWalletCount |
address |
string | A named wallet address |
payments_consumer_rows, payments_provider_rows, provider_payouts_rows |
integer | Rows that hold it; all 0 means the address is wrong | |
ErasureOutboxItem |
id, request_id |
string | Row and request |
target |
string | stripe_account, global_recipient, checkout_sessions, erasure_log, resend_contact (manual cleanup; contact email is not exposed) |
|
state |
string | pending, done, manual_action |
|
attempts, next_at, last_error, done_at, created_at |
Delivery bookkeeping; last_error and done_at omitted when empty |
||
has_external_id |
bool | The row still holds an external cleanup identifier (Stripe ID or Resend contact email; false once done); the identifier itself is never returned |
|
has_stripe_job |
bool | A Stripe redaction job is in progress for a checkout_sessions row; the job ID is never returned |
|
ErasureRefusedCredit |
id, account_id, entry_type, amount_micro_usd, reference, created_at |
A credit kept out of an erased account (schema) |
- While a request is
pending, a Privy login of the account (anyRequireAuthorRequirePrivyAuthroute) answers 403account_pending_deletionwith messagethis account is scheduled for deletion; contact support to cancelinstead of creating a second account (auth.ErrAccountPendingDeletion;writePrivyUserError,coordinator/api/access/auth.go). After the scrub the stored Privy ID is random, and the same login creates a new, empty account. - The account's API keys answer 401
authentication_errorfrom the confirm on; they stay revoked after a cancel. POST /v1/billing/stripe/webhook: acheckout.session.completedevent for an erased account's session, or a replay for a session the scrub cleared, answers 200 and credits nothing (store.ErrCheckoutErased).
| Concern | Files |
|---|---|
| Route registration, middleware, request-id, CORS, admin/version constants | coordinator/api/routes.go, coordinator/internal/api/middleware/middleware.go, coordinator/api/server.go, coordinator/api/server_config.go; credential policy in coordinator/api/access/ |
| Inference pipeline | coordinator/api/inference/consumer.go, coordinator/api/inference/inference_preprocess.go, coordinator/api/inference/inference_admission.go, coordinator/api/inference/request_introspection.go, coordinator/api/inference/request/reasoning_request_policy.go, coordinator/api/inference/dispatch.go; coordinator/internal/inference/rejection/terminal.go (Recorder.PreContentTerminal) wired by Owner.NewRejectionRecorder in coordinator/api/inference/rejection_telemetry.go; coordinator/internal/inference/rejection/inference_failure_class.go |
| Endpoint lowering (Responses, Completions, Messages) | coordinator/promptcontract/endpoint_lower.go, coordinator/internal/promptcontract/endpoint/endpoint_lower_responses.go, coordinator/internal/promptcontract/endpoint/endpoint_lower_messages.go, coordinator/api/inference/response/generic_endpoint_response.go, coordinator/internal/inference/relay/generic_endpoint_stream.go, coordinator/api/inference/response/responses_stream.go |
| SSE, timing and provider metadata | coordinator/api/inference/response/sse_response.go, coordinator/api/inference/response/chat_metadata_stream.go, coordinator/api/inference/response/response_metadata.go, coordinator/api/inference/profiler_dispatch.go |
| Tools, media, constraints | coordinator/api/inference/request/toolschema.go, coordinator/api/inference/tool_constraints.go, coordinator/internal/inference/media/media_resolve.go |
| Sealed transport | coordinator/api/inference/sender_encryption.go |
| Models and catalog | coordinator/api/catalog/models_endpoints.go, coordinator/api/catalog/concrete_model_entries.go, coordinator/api/catalog/openrouter_endpoint.go, coordinator/api/catalog/, coordinator/api/catalog/model_alias_handlers.go, coordinator/api/catalog/openrouter_alias_handlers.go, coordinator/api/catalog/capacity.go, coordinator/api/inference/exact_cache_status.go |
| Keys, device code, accounts | coordinator/api/access/keys/handlers.go, coordinator/store/apikey.go, coordinator/api/access/device/handlers.go, coordinator/api/accounts/ |
| Billing, Stripe, referral, invites | coordinator/api/billing/, coordinator/api/billing/referrals.go, coordinator/api/billing/payouts/, coordinator/api/billing/payouts/stripe_withdraw.go, coordinator/api/billing/payouts/stripe_payouts_webhooks.go, coordinator/api/accounts/invite_handlers.go, coordinator/api/billing/base_rewards_handlers.go |
| Stats | coordinator/api/reporting/stats_handler.go, coordinator/api/reporting/refresh_start.go, coordinator/api/reporting/totals_handler.go, coordinator/api/reporting/leaderboard.go, coordinator/api/reporting/network_series.go |
| Release, enrollment, provider WS, log reports | coordinator/api/releases/release_handlers.go, coordinator/api/provider/trust/enroll.go, coordinator/api/provider/, coordinator/api/operations/log_reports.go |
| Account erasure | coordinator/api/accounts/erasure/ (handlers, loop, outbox worker), coordinator/api/routes.go, coordinator/store/erasure_types.go; tests coordinator/tests/api/accounts/contracts/erasure_test.go, coordinator/tests/api/accounts/contracts/erasure_outbox_test.go |
| Drain, admin telemetry, profiler, state export | coordinator/api/operations/drain.go, coordinator/api/observation/admin_telemetry.go, coordinator/api/reporting/admin_utilization.go, coordinator/api/observation/profiler_admin.go, coordinator/api/operations/state_export.go |
| Rate-limit bucket consumption | coordinator/ratelimit/ratelimit.go (allowBucket, debitBucket): fixed and per-key rate paths share token consumption and retry calculation while keeping their own admission and clamp rules |
| Shared types and helpers | coordinator/api/types/types.go, coordinator/api/httpx/json.go, coordinator/ratelimit/ratelimit.go, coordinator/modelpolicy/first_content_deadline.go |
| Method/path | Authorization | Behavior | Code |
|---|---|---|---|
PUT /v1/admin/token-promotions |
Admin | Create immutable terms or toggle enabled for an exact model ID, even before registration |
coordinator/api/inference/model_token_promotions.go (HandleAdminModelTokenPromotions) |
GET /v1/admin/token-promotions |
Admin | List configured promotions | coordinator/api/inference/model_token_promotions.go (HandleAdminModelTokenPromotions) |
POST /v1/me/token-promotions/claim |
Privy only | Explicitly claim the requested model_id for an eligible individual account; atomically enforce signup cutoff and campaign capacity |
coordinator/api/inference/model_token_promotions.go (HandleMyModelTokenPromotions) |
GET /v1/me/token-promotions |
Privy only | Return account grants and available offers without issuing any | coordinator/api/inference/model_token_promotions.go (HandleMyModelTokenPromotions) |
Promotion input is { "model_id": "...", "tokens": 150000000, "claim_starts_at": "RFC3339", "claim_ends_at": "RFC3339 or null", "signup_cutoff_at": "RFC3339", "max_claims": 250, "enabled": true }. Signup eligibility is strictly before signup_cutoff_at using the persisted account creation timestamp. max_claims accepts integers in [1, 1000000]. A null claim end is supported, but the Bonsai launch draft has an explicit end. Only enabled is mutable; conflicting terms return 409 promotion_conflict. Tokens are integers in [1, 1000000000000]. Grant responses have a grants array containing model_id, total_tokens, used_tokens, reserved_tokens, remaining_tokens (available after reservations), and claimed_at. There is no expiry field. Claiming requires {"model_id":"..."}, uses the server clock and does not require catalog registration. Repeated successful claims return the existing grant without consuming another slot, including after the window or cap closes. Service accounts receive no grant. Responses also include offers with model_id, tokens, max_claims, remaining_claims, signup_cutoff_at, claim_ends_at and status (available, claimed, sold_out, ineligible, or unavailable). Claim failures return 403 promotion_ineligible, 409 promotion_sold_out, 409 promotion_unavailable, or 404 promotion_not_found.
Inference returns 402 free_tokens_exhausted when the claimed allowance is exhausted or held by active requests and paid balance is insufficient. 402 promotion_balance_required means remaining free tokens plus paid balance cannot cover the request's upper bound. Both carry an OpenAI-compatible error.code and user-facing message. Paid fallback succeeds when funded. See operations/model-token-promotions.md.
Source: coordinator/api/autopilot/handler.go (Handler.ServeHTTP),
authenticated adapter coordinator/api/autopilot_handlers.go (handleAdminAutopilot),
coordinator/api/accounts/providers.go (HandleMyProviders).
| Endpoint | Authorization | Result |
|---|---|---|
GET /v1/admin/autopilot |
Admin key or authenticated admin | Controller summary and up to 200 durable events in the last 24 hours; ledger read failure returns 503 |
GET /v1/admin/autopilot/inventory |
Admin key or authenticated admin | Read-only connected-session saved-approval aggregates; 200 even without a configured controller or available ledger; Cache-Control: no-store |
GET /v1/admin/autopilot/machines |
Admin key or authenticated admin | Bounded canonical-machine list with desired modes and separate current-session status; machine contract |
PATCH /v1/admin/autopilot/machines/{machine_id} |
Admin key or authenticated admin | Persist desired_mode as shadow or live; synchronize local authority before success, without changing global overrides or asserting active control |
POST /v1/admin/autopilot |
Admin key or authenticated admin | Required JSON { "paused": true } stops new reservations; false resumes in the configured mode, never promotes shadow to live. Existing operations continue reconciliation. Missing/invalid input or unknown fields (including observe_only) return 400; unavailable controller returns 409; successful mutation returns the summary independently of ledger availability |
GET /v1/me/providers |
Provider owner | Optional model_autopilot live snapshot with consent, exact approved cached network inventory (selected_models), active, observe_only, paused state and last operation; a valid shadow lease reports active=false, observe_only=true |
Each model summary separates terminal logical observations (logical_requests)
from current qualified public queue occupancy (queued_requests) and in-flight
reservations (public_inflight_requests). Live snapshots do not increment arrival
history. Private/local or unattributed slot work creates no public placement demand.
Per-model ready/pending capacity and warm coverage use actual serving evidence.
eligible_idle remains a coarse count and uses hypothetical post-activation fits
for shadow recipients; it does not establish an absent target, donor safety,
load headroom or positive benefit. Shadow planning's permission projection is
therefore not a second measurement of currently routable capacity.
The controller summary's observe_only is the global shadow override, not the
mode of every enrolled provider. false permits live control only for verified
machines with persisted live intent. Without live settings, every provider stays
shadow. Startup enrollment is consent, not activation. Global mode changes still
require configuration and restart; machine-mode edits use the separate PATCH
endpoint. The pause-only POST rejects a live_machine_ids field.
Source: coordinator/registry/autopilot/types.go (Summary) and
coordinator/registry/autopilot/summary.go (Summarize). The live_cohort,
live_active and shadow fields count connected, consenting, nonprivate sessions,
not distinct people or machines. opted_in retains its separate planner definition.
| Summary field | Meaning |
|---|---|
live_cohort |
Sessions assigned live mode by the global switch and synchronized verified-machine settings; includes locally paused or stale sessions and is not an actionability count |
live_active |
Subset with a matching, unexpired, acknowledged live grant; still not proof of fresh idle capacity or feasible placement |
shadow |
Consenting public sessions assigned shadow mode, including paused/stale sessions and selected machines while global shadow is enabled |
opted_in |
Existing planner-managed or hypothetical-shadow population; not an alias for live membership or approved ready capacity |
live_proposed |
Actions selected by this tick's live pass, including actions later refused by reservation or delivery preparation |
shadow_proposed |
Hypothetical actions selected by this tick's detached shadow pass; never residency commands or live capacity |
proposed |
Sum of live_proposed and shadow_proposed; each pass has its own per-tick bound |
issued |
Live actions advanced to command send, not confirmed delivery or terminal success; zero while global shadow is enabled or no selected machine is actionable |
No machine UUIDs or account identifiers are added to this summary. The separate inventory endpoint remains a controller-independent saved-consent projection.
The events array is a recent decision/operation ledger, not a per-tick time
series. Unchanged shadow decisions retain their first timestamp and can age out
of the 24-hour window even while proposed again; the current controller summary
still reports the latest tick. See ledger semantics.
The operator pause lasts for the current coordinator process. Live intent is persisted before dispatch. Ledger read/write errors are not success or rollback evidence. Snapshots and operation records contain model/control metadata, never prompts.
Source: coordinator/api/autopilot/machines.go (MachineHandler.ServeHTTP),
coordinator/registry/autopilot_machine_policy.go
(SetMachineAutopilotDesiredMode), and
coordinator/registry/autopilot_machine_status.go (ListMachineAutopilot).
Both routes require authentication and admin authorization before parsing input:
missing/invalid credentials return 401 and authenticated non-admins return 403.
The authorized machine adapter sets Cache-Control: no-store.
| Request | Contract |
|---|---|
GET /v1/admin/autopilot/machines |
Returns {"machines":[...]} in ascending canonical UUID order, including offline machines. limit defaults to 100 and accepts integers 1 through 200. Optional after is an exclusive canonical UUID cursor. A full page includes next_after; following it may return an empty last page. Duplicate or invalid limit/after values return 400. |
PATCH /v1/admin/autopilot/machines/{machine_id} |
Exactly one JSON object with exactly one desired_mode key, value "shadow" or "live"; body limit 1024 bytes. Missing/null/invalid modes, duplicate keys, unknown fields, case-altered field names and trailing data return 400. Returns {"machine":{...}} on 200. |
| Machine ID or cursor | Nonzero, hyphenated UUID; letter case normalizes to lowercase. Nil UUIDs, whitespace and alternate UUID encodings return 400. Unknown or merged-away mutation targets return 404, never create a machine or redirect to a survivor. |
| Store failure | 503 with a sanitized error. An interrupted write may have committed; read back or repeat the same desired mode after recovery rather than infer rollback. A missing persistence capability is not an ephemeral fallback. |
| Machine field | Meaning |
|---|---|
machine_id |
Exact existing, unmerged inventory UUID, not a connection ID or evidence of current live verification |
desired_mode |
Persisted intent, shadow by default or explicitly live; does not override consent, identity, global shadow/pause or safety gates |
revision |
Nonnegative persisted integer, default 0; increases only on a mode change. Repeating the current mode is idempotent. Not the provider consent revision or a wire grant epoch. |
sessions |
Current sessions on this coordinator with a verified machine binding matching the authenticated account, sorted by provider_id; [] when no such connection exists. Historical, unverified and disconnected associations are not attached. |
| Session field | Meaning |
|---|---|
provider_id |
Current provider connection identifier, usable for correlation but not as a machine-edit target |
machine_model, chip_name, memory_gb |
Hardware labels and installed memory reported by this connection; not independent hardware verification or available load headroom |
effective_mode |
disabled when the controller is absent/disabled; then, in precedence order, paused, private, unconsented, shadow, awaiting_ack, or live according to effective control. awaiting_ack includes expired or missing grants on selected sessions. |
control_active |
Effective acknowledged, unexpired live authority after global and session gates; not proof of idle capacity, feasible placement or an issued operation |
consented |
Current supported cached-only consent with a valid saved selection/revision |
paused |
Provider pause or current coordinator pause; does not alter durable desired mode |
private_only |
Session excludes public participation |
capacity_fresh |
Accepted capacity exists within the applicable control/ordinary freshness window; independent of consent and control activation |
last_heartbeat |
Coordinator connection-liveness timestamp, or null when unavailable; rejected capacity sequences can advance it without refreshing the reported model state |
capacity_accepted_at |
Coordinator timestamp of the last applied non-null capacity frame, or null; an accepted empty frame without a usable sample can still have capacity_fresh=false |
idle_unload_mins |
Last valid heartbeat-reported ordinary idle timeout in minutes: 0 disables timeout unloading, a positive value enables it, and null means unreported. Retained within this connection when later heartbeats omit the field. |
always_ready_configured |
true exactly when reported idle_unload_mins is 0, false for a positive timeout, and null when unknown. Describes the running daemon's startup-loaded baseline, not current residency, routability or a later on-disk edit. |
pinned_models |
Sorted exact IDs from the bounded protocol-3 cached-only Autopilot report, including the provider's combined explicit pins and configured backend.model; [] means reported none and null means no usable report. Does not require active control or imply that protection is currently enforced. |
resident_models |
Sorted IDs from that same report's resident_models, with the same []/null distinction. Reported local residency, not selected/advertised models, a fresh disk scan, or catalog-filtered ready routing capacity. |
Model lists are unavailable for missing, unsupported or sanitized malformed
Autopilot reports. A stale report remains visible alongside its freshness
metadata; it is not converted into an empty list. An accepted heartbeat omitting
Autopilot state clears the previous lists. Registration can supply model lists
before any accepted capacity frame or idle-policy report. Offline machines have
sessions: []; neither pins nor idle policy are recovered from historical rows.
The diagnostic fields also appear in the successful PATCH response, but PATCH
still accepts only desired_mode. Reads send no commands and change no policy.
Always ready is not an Autopilot activation requirement or a pin-all setting.
Live Autopilot suspends the ordinary idle timer and may replace or unload
unpinned models subject to its own safeguards. The combined paused field does
not distinguish a global pause from an explicit provider pause; do not use it
alone to infer provider-side pin protection. See
idle-policy ownership.
The database read and current-session projection are separate snapshots, not an atomic cross-process view. A successful local edit publishes authority and revokes changed grants before returning; other coordinators apply it on their next successful policy refresh. Provider reports can lag delivery or expiry. Demotion, revision changes and policy-store uncertainty preserve accepted operation ownership. Promotion requires a fresh grant and a later accepted matching acknowledgement. The APIs work while the controller is unconfigured, disabled, paused or globally shadow, without changing those safety settings. See persistence and operator procedure.
Provider WebSocket capture records consent without calculating a baseline. This admin listing may materialize a previously journaled enrollment using its original opt-in timestamp; the billing mechanism describes that separation.
coordinator/api/autopilot/rewards.go (RewardsHandler) owns the payloads;
rewards_decode.go in that directory rejects ambiguous JSON. The adapter
coordinator/api/autopilot_handlers.go (handleAdminAutopilotRewards) requires
admin authorization after RequireAuth; admin bearer keys or authenticated
admin Privy users qualify. Missing/invalid credentials return 401; authenticated
non-admins return 403. Mutations use the financial limiter (429 on exhaustion).
The adapter sets Cache-Control: no-store. These routes do not activate
Autopilot, change provider consent/cohort, deploy code or publish a provider release.
| Request | Contract |
|---|---|
GET /v1/admin/autopilot/rewards |
{"enabled":bool,"pool":{...},"enrollments":[...]}, ascending canonical machine UUIDs, including offline and unknown-baseline enrollments. limit defaults to 100, accepts 1 through 200; optional exclusive after is a canonical UUID. Full pages include next_after; following one may return an empty final page. Unknown/duplicate query keys or invalid values return 400. |
PATCH /v1/admin/autopilot/rewards/pool |
Exactly {"cap_micro_usd":<nonnegative int64>}; body limit 1024 bytes. Sets an absolute cumulative cap, not an increment, monthly budget or balance deposit. Returns {"pool":{...}}. |
POST /v1/admin/autopilot/rewards/machines/{machine_id}/baseline |
Exactly {"first_opt_in_at":<RFC3339 string>,"seven_day_earnings_micro_usd":<nonnegative int64>,"evidence":<string>}; body limit 8192 bytes. Evidence must be nonblank and at most 1024 UTF-8 bytes. Time must be no later than the first positive tracked observation. Returns {"enrollment":{...}}. |
| Input identity and decoding | Nonzero hyphenated UUIDs, normalized to lowercase; whitespace/alternate encodings fail. Mutations require exactly one object with all named fields; missing/null values, duplicate/unknown/case-altered keys, invalid numbers/times or trailing JSON return 400. |
| Domain conflicts | 409 conflict for a frozen baseline (including exact resubmission), cap below spending, unresolved history/ownership or account-erasure admission. No baseline rewrite or partial funding occurs. A known machine without reward enrollment returns 404 not_found; invalid/unverified ownership fails closed. |
| Store failure | 503 server_error with sanitized message Autopilot rewards unavailable; no raw store errors/evidence in the error. Read back after an uncertain mutation; do not infer rollback or add funds by blindly incrementing the requested cap. |
Response records are defined in coordinator/store/earningsfloor/types.go:
| Field | Meaning |
|---|---|
enabled |
Payment worker is wired; not proof of funding, known baselines or live Autopilot control |
pool.cap_micro_usd, pool.spent_micro_usd |
Separate cumulative allowance and committed spending; monetary policy |
pool.tracking_started_at |
Persisted start of the new tracker, not a historical first opt-in |
enrollments[].machine_id, account_id |
Current canonical machine UUID and authenticated payout account; financial records may retain an original pre-merge ID |
first_opt_in_at, first_observed_at |
Frozen first-ever anchor (null while unknown), and the first positive used to initialize enrollment respectively. Both remain unchanged when later evidence sets history_conflict; backfill does not move accrual to an older date |
seven_day_earnings_micro_usd, daily_floor_micro_usd, baseline_known, baseline_evidence |
Frozen baseline values and evidence. Zero values with baseline_known=false are unknown, not a measured zero floor |
baseline_source |
Closed enum: "" while unknown, "tracked" for an automatically frozen personal-history baseline, "cohort" for an automatically frozen comparable-machine baseline, "verified_history" for an evidenced admin import. Assigned by the store, not a baseline-request field; independent of the evidence string. All known sources stay frozen through off/on |
Cohort baseline_evidence |
JSON string containing chip_class, memory_gb, peer_count, peer_fingerprint_sha256, window_start, window_end and statistic="mean_seven_day_micro_usd_floor". The fingerprint hashes sorted canonical peer IDs; it does not expose the peer IDs. coordinator/internal/payments/floorpolicy/cohort.go (CohortBaselineValue) |
history_conflict |
Linked history contradicts the frozen anchor or invalidates an automatic baseline's creation-history proof; history rules. May be true with baseline_known=true; frozen fields stay unchanged, but unfinalized days are held without payment or cursor advance. Ordinary baseline import still returns 409 for the frozen value |
opted_in, observed_at |
Whether the latest durable declaration qualifies as saved opt-in, and that declaration's observation watermark; not a connection/readiness indicator or the consent snapshot for every prior day |
next_day |
Next chronological UTC settlement day; pending funding/history does not advance it |
next_after |
Optional exclusive cursor for the next page; absent on a short page |
Listing can lazily bind already-journaled declarations and materialize enrollment
after trusted inventory resolution. The GET can therefore write tracking records,
but cannot pay a day or invent first-ever history. Unverified or
ambiguous ownership and erased accounts are not admitted to the list. Pool and
enrollment reads are separate snapshots, not an atomic financial export.
There is no receipt-list or manual settlement endpoint here. Use the
operator runbook for restricted financial
verification and backfill evidence requirements.
Daily receipts distinguish final unpaid ineligible days from pending history
or funding under the status reference.
The enrollment's current opted_in field alone does not prove a day's OS/model
qualification or uptime.
Source: coordinator/registry/autopilot_inventory_report.go
(AutopilotInventory, AutopilotInventoryReport), exposed by
coordinator/api/autopilot_handlers.go (handleAdminAutopilotInventory).
No credentials returns 401; an authenticated non-admin returns 403.
The population is connected registry sessions with supported, enabled, cached-only
consent and a valid nonempty saved selection/revision, excluding private-only
providers (coordinator/internal/registry/autopilotstate/state.go, Consented).
Waiting and shadow sessions need no active lease. All model counts include paused
and stale sessions. Disconnected/offline selections are unavailable, not persisted.
No provider identity, account details, keys or request content are returned.
| Field | Definition (AutopilotInventory) |
|---|---|
generated_at |
UTC report-generation time; provider states are read under their individual locks during the registry scan, not one simultaneous fleet heartbeat |
stale_after_seconds |
Normal serving freshness window, DefaultProviderHeartbeatTimeout (90 seconds), not the stricter live-command budget |
enrolled_providers |
Total included connected sessions, not distinct physical machines or accounts |
participating_providers |
Included sessions reporting paused=false; does not imply a live lease, current controller activity or fresh capacity |
paused_providers |
Included sessions reporting paused=true; participating plus paused equals enrolled |
stale_providers |
Overlapping subset with no accepted capacity heartbeat or an accepted capacity timestamp older than the freshness window; registration alone is stale and rejected sequence frames cannot refresh it |
distinct_models |
Number of unique exact saved model IDs across the population |
total_approvals |
Sum of distinct saved models per included session; repeated IDs within a session count once |
models_per_provider |
Ascending model_count buckets with provider_count; covers all enrolled sessions |
models |
Sorted by exact model_id; each row has approved_providers, participating_providers, paused_providers, stale_providers using the same population/subset definitions |
Empty reports use zero counts and [] arrays. Model IDs are not alias-folded or
filtered against the current catalog. Saved approvals describe the last-reported
selection of approved downloaded/cached builds; they are not a fresh disk scan,
current hash verification, resident inventory or routing-eligibility claim. Reads
send no provider commands and change no enrollment, pause, lease or routing state.
See MiMo prompt fixture reproduction for the independent pinned corpus and metadata required by the Rust parity gate.
With EIGENINFERENCE_STRIPE_GLOBAL_PAYOUTS_ONLY=true, legacy clients attempting
an amount-only withdrawal receive 409 bank_setup_required before any debit.
All returned countries use rail=global; users enter their own bank details via
/v1/billing/stripe/onboard. A missing country returns 400 country_required,
an unsupported destination returns 400 country_unavailable, and paused bank
setup returns 503 payouts_paused. No error falls through to Connect.
Confirmation reserves the gross amount once. Low funding (including estimated
platform-paid fees) returns HTTP 202 with status="queued", the same
withdrawal_id, and the remaining balance. Withdrawal history includes queued
with failure_reason="awaiting_funding"; the minute reconciliation loop retries
automatically. Temporarily unavailable Connect destinations back off for five
minutes without consuming a send or blocking newer eligible withdrawals. A bank-arrival eta is returned only after an external payout
is processing or posted; an unsent pending response explains that earnings
remain reserved while confirmation is in progress. If persisting the Connect queue fails, the response and history
remain pending with funding_queue_persistence_failed and the UI shows Needs
review while retaining the debit. A Global Payouts exchange estimate may be refreshed after a funding
wait, while USD principal and bank destination stay fixed. Already-confirmed quote retries
remain available while paused. Bank reset never re-enables Connect or mutates
historical payouts (coordinator/api/billing/payouts/global_payouts_withdraw.go,
maybeGlobalWithdraw; coordinator/api/billing/payouts/global_payouts_status.go, maybeGlobalStatus).
Checkout amounts require at most two decimal places and the supported integer
cent range (coordinator/api/billing/stripe_checkout_webhook.go, checkoutUSDCents).
Current and legacy Checkout signatures share exact local-session validation and
atomic, non-withdrawable credit (HandleStripeWebhook, CompleteStripeCheckout).
POST /v1/billing/stripe/create-session returns HTTP 409 with error code
account_deleted when the request's Stripe result reaches persistence after
its account was deleted. The response omits the session URL; durable erasure
cleanup retains the newly created Stripe identifier
(coordinator/api/billing/checkout.go, HandleStripeCreateSession).