Twilight MINIS server API

How a MINIS Slot server gets user data out of search-router: verify enrollment tokens against our JWKS, exchange client credentials for an access token, and resolve request ids into user_id, status, and cost.

The user_id, the one identity

A bare lowercase UUID, minted at first sign-in, stable across all of a user's organizations, projects, and API keys. It reaches your systems in exactly two places, and they are the same string, guaranteed:

Where user_id appears
FieldWhere
subon the enrollment token — once, at enrollment; you bind the participant to it
user_idon SERVED / FAILED verification results — continuously; you compare against the binding

A request we cannot attribute to a person comes back with user_id omitted: your comparison fails safe, nobody is credited, nothing errors. That is the case for operator tenants, for multi-owner shared organizations, and for a self-registered agent that no human has claimed yet: its searches are served on the unclaimed tier and verify as SERVED without a user_id until the claim. Issuer https://platform.nyks.dev, algorithm EdDSA (pin it), scope verification:read.

1 · GET /.well-known/jwks.json — our public keys

GET https://platform.nyks.dev/.well-known/jwks.json

{"keys":[{"kty":"OKP","crv":"Ed25519",
  "x":"JJ156dBdNd1bgzBASJtE9xJs0jxjDlhAhBMEt1bQOi0",
  "kid":"747eb17c7478febe","alg":"EdDSA","use":"sig"}]}

Cache per Cache-Control: max-age=300. During rotation the old and new keys are published together; fetch-on-unknown-kid is safe.

2 · The enrollment token — where user_id comes from

{"alg":"EdDSA","typ":"enroll+jwt","kid":"747eb17c7478febe"}
{
  "iss": "https://platform.nyks.dev",
  "sub": "0321a11e-1a31-4d98-b51f-3faf6f838d3e",
  "aud": "twilight-slot-3",
  "exp": 1787662213,
  "iat": 1787661313,
  "jti": "a6327a8c8352c0d25b1f0b02e9a448e2"
}

Users generate this on the Mining page and paste it into tokendrop-proxy enroll. Validate: pin EdDSA at parse time, resolve kid via JWKS, check iss / aud / exp (15-minute lifetime), enforce single-use jti, read sub. No email, name, or profile claim will ever appear. The typ is enroll+jwt; access tokens are at+jwt, and the verification endpoint refuses anything else, so a leaked enrollment token cannot call the API below.

2a · A second issuance path: the claimed agent

Since 2026-09-09 the same token has two senders. The session path is unchanged: the Mining page mints from a signed-in browser, sub is the session's user id. The agent path is new: a headless agent that registered itself and was then claimed by a person calls, with its own bearer key and no browser,

curl -X POST https://agents-v1.nyks.dev/v1/agents/enroll \
  -H "Authorization: Bearer sr-…" -H "Content-Type: application/json" \
  -d '{"slot": "twilight-slot-3"}'

{"token": "eyJhbGciOiJFZERTQSIs…", "slot": "twilight-slot-3", "expires_at": "…"}

sub is the claiming user, never a value the request supplies (a body naming one is rejected). The route refuses unless the agent is claimed, the key carries the mining scope granted at the claim, and the claimer resolves to a real user. Both paths call one signer with one claim-construction function, so what you receive and verify is byte-for-byte the same token shape from the same key: same issuer, typ, claim set, 15-minute life and JWKS. A note about a second sender, not a change to the message.

3 · POST /oauth/token — get an access token

curl -X POST https://platform.nyks.dev/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=minis-slot-3" \
  -d "client_secret=$MINIS_CLIENT_SECRET"

{"access_token":"eyJhbGciOiJFZERTQSIs…","token_type":"Bearer",
 "expires_in":3600,"scope":"verification:read"}

One hour lifetime; refresh = the same exchange (HTTP Basic also accepted). A 401 anywhere means "refresh and retry", never an answer about a request. Revocation of a client takes effect on its next request, not at token expiry. The exchange is paced: 429 slow_down with Retry-After past 30/min per address — one exchange an hour is all a slot needs, so only a loop ever sees it.

4 · POST /v1/verifications — resolve request ids to user data

Batched (1–500 ids), read-only, safe to retry. The ids are the X-Request-Id values your proxy captured from POST /v1/search responses.

curl -X POST https://platform.nyks.dev/v1/verifications \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"client_batch_id":"vfy_01J8XM4Q7B",
       "request_ids":["01a038ea-2356-7fd5-983c-ffa6745559d5",
                      "req_that_never_existed"]}'

{"client_batch_id":"vfy_01J8XM4Q7B","results":[
  {"request_id":"01a038ea-2356-7fd5-983c-ffa6745559d5",
   "status":"SERVED",
   "user_id":"0321a11e-1a31-4d98-b51f-3faf6f838d3e",
   "served_at":"2026-08-25T12:34:29.000Z",
   "usage":{"cost_micros":5000},
   "decision":{"tier":"fast","providers":["openai","exa"]},
   "candidates":[{"provider":"openai","status":"error"},
                 {"provider":"exa","status":"ok"}],
   "chosen":1},
  {"request_id":"req_that_never_existed","status":"NOT_FOUND"}
]}

Always 200 when the batch was processed: exactly one result per requested id, positionally. Statuses form a closed set; unknown values are retryable.

Verification statuses
StatusKindMeaning
SERVEDconclusiveexisted and was served — check user_id
FAILEDconclusiveexisted, attributed, not servable (failure_reason says why)
NOT_FOUNDconclusiveno such id within retention — permanently rejects the work
NOT_YET_DURABLEretryid issued < 15 min ago, not yet queryable
EXPIREDretry, then age outolder than the 30-day retention window
UNAVAILABLEretrywe could not answer for this id right now
  • chosen is always present on SERVED and FAILED: an integer index into candidates, -1 = none.
  • decision.providers is the normative array; provider_set does not exist in this schema.
  • usage.cost_micros is summed from the billing ledger at query time. Slow-lane spend is included, so a later re-verify can show more.
  • No content-bearing field exists: no answer text, citations, query text, URLs, or snippets.
  • No HTTP status is ever a verdict. 400 fix and retry · 401 refresh · 429 pacing (honor Retry-After) · 5xx retry.
  • Rate limit 60/min per client, burst 120, with X-RateLimit-Remaining and X-RateLimit-Reset on every response. Measured p99 for a full 500-id batch on production: 196 ms.

Fetch this reference without a login

The same reference as a plain-markdown file your engineers can curl, no session required.

Download minis-api.mdcurl https://platform.nyks.dev/minis-api.md