Skip to content

API

The Archive's data as JSON, under /api/v1/. It is read from the same database, by the same queries, as the pages, so it never says something the pages do not.

Reading the API is free and needs an account: every request comes with a token made on your account page, or from a browser signed in on the site, and the Archive records which account asked what, and when. The API sends Access-Control-Allow-Origin: *, so a page on any site can read it with a token. Version 1 keeps its fields: a field may be added, but none is renamed or removed without a new version. The only requests that write come from the tooling, described under “Proof of when a run was played” below.

A list comes a page at a time. Its answer carries next_cursor; pass that back as cursor, with the same other parameters, for the next page. A cursor is opaque and belongs to the query it came from. Every answer says in RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset how much of the limit is left.

A token, and where to try it

In your browser, signed in on the site, the API answers you as your account. From a program, make a token under Settings on your account page: give it a name, copy it once, and send it with every request as Authorization: Bearer <token>. A request from nobody is answered 401 and told where to make one. You can have several tokens and revoke any of them there; your account page also shows your latest requests. A token is handed only to an account whose e-mail address is confirmed.

The same API is described as OpenAPI, generated from the same description as this page, and can be tried at /api/docs/ (Swagger UI, served by the Archive itself). Signed in on the site, you need no token there: requests from that page count as your account's. From elsewhere, press Authorize and paste a token.

Endpoints

EndpointReturns
GET /api/v1/the list of endpoints, and how to get a token
GET /api/v1/gamesevery game with runs
GET /api/v1/games/{game}one game and its categories
GET /api/v1/categoriesevery category that holds runs
GET /api/v1/runsruns, newest first; with a game and a goal, grouped by category and ranked within it
GET /api/v1/runs/{run}one run whole
GET /api/v1/runs/{run}/logthe run's log, record by record, in order
GET /api/v1/searchthe runs whose log matched, best first
GET /api/v1/statswhat the Archive holds in numbers
POST /api/v1/ticketsa ticket for a run about to be recorded
GET /api/v1/tickets/{ticket}the state of a ticket
DELETE /api/v1/tickets/{ticket}delete a ticket no run was submitted with
POST /api/v1/tickets/{ticket}/headsa hash of the logs as they stand, signed by the Archive with the time
POST /api/v1/tickets/{ticket}/extendstart the ticket's period again
POST /api/v1/tickets/{ticket}/revokethe ticket takes no more heads
POST /api/v1/bundlesthe upload zip of a run

GET /api/v1/

The list of endpoints, and how to get a token.

No parameters.

Example: GET /api/v1/

Field in the answerTypeMeaning
versioninteger
documentationstring
openapistring
authenticationstring
endpointsobject[]
endpoints[].pathstring
endpoints[].returnsstring
endpoints[].paramsobject[]
endpoints[].params[].namestring
endpoints[].params[].aboutstring
StatusWhen
200the list of endpoints, and how to get a token
400A parameter or cursor that cannot be read.
404No such game, run or endpoint.
405Anything but GET: the API only reads.

GET /api/v1/games

Every game with runs. Needs: an API token.

No parameters.

Example: GET /api/v1/games

Field in the answerTypeMeaning
dataobject[]
data[].slugstring
data[].idstring
data[].namestring
data[].runsinteger
data[].goalsobject[]
data[].goals[].goalstring
data[].goals[].labelstring
data[].platformsstring[]
data[].versionsstring[]
data[].modelsstring[]
data[].lateststring
data[].urlstring
StatusWhen
200every game with runs
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/games/{game}

One game and its categories. Needs: an API token.

ParameterInMeaning
game requiredpath

Example: GET /api/v1/games/slay-the-spire

Field in the answerTypeMeaning
dataobject
data.slugstring
data.idstring
data.namestring
data.runsinteger
data.goalsobject[]
data.goals[].goalstring
data.goals[].labelstring
data.platformsstring[]
data.versionsstring[]
data.modelsstring[]
data.lateststring
data.urlstring
data.categoriesobject[]
data.categories[].goalstring
data.categories[].goalLabelstring
data.categories[].buildstring
data.categories[].observationstring
data.categories[].inputstring
data.categories[].timingstring
data.categories[].humanstring
data.categories[].gamestring
data.categories[].runsinteger
data.categories[].reachedinteger
StatusWhen
200one game and its categories
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/categories

Every category that holds runs. Needs: an API token.

ParameterInMeaning
gamequerya game's slug, such as slay-the-spire
goalquerya goal id, such as act3

Example: GET /api/v1/categories?game=slay-the-spire

Field in the answerTypeMeaning
dataobject[]
data[].goalstring
data[].goalLabelstring
data[].buildstring
data[].observationstring
data[].inputstring
data[].timingstring
data[].humanstring
data[].gamestring
data[].runsinteger
data[].reachedinteger
StatusWhen
200every category that holds runs
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/runs

Runs, newest first; with a game and a goal, grouped by category and ranked within it. Needs: an API token.

ParameterInMeaning
gamequerya game's slug
goalquerya goal id
observationqueryan axis value (specification §3)
inputqueryan axis value (specification §3)
timingqueryan axis value (specification §3)
humanqueryan axis value (specification §3)
modelquerya full model id
namequerya part of the model id
variantquerya part of the model id
model_versionquerya part of the model id
snapshotquerya part of the model id
effortqueryas the runtime reported it
contextquerythe context window, as the runtime reported it
providerqueryas the runtime reported it
runtimequerya runtime plugin id
cliquerya runtime CLI version
versionquerya game version
limitqueryruns per page, at most 100; 50 when left out
cursorquerynext_cursor from the previous page, with the same other parameters

Example: GET /api/v1/runs?game=slay-the-spire&goal=act3&limit=10

Field in the answerTypeMeaning
dataobject[]the runs of this page
data[].idstringthe address the run is published under; may change between revisions
data[].uidstringthe run's identity, written once when the run was configured
data[].revisionintegerwhich publication of the run this is
data[].urlstringthe run's page on this site
data[].gameobject
data[].game.slugstringthe game's address on this site
data[].game.idstringits id in the tooling
data[].game.namestring
data[].game.versionstring
data[].game.platformstring
data[].categoryobjectThe seven axes of the specification's §3, game apart.
data[].category.goalstring
data[].category.goalLabelstring
data[].category.buildstring
data[].category.observationstring
data[].category.inputstring
data[].category.timingstring
data[].category.humanstring
data[].agentobject
data[].agent.modelsobject[]the models that answered
data[].agent.models[].modelstringthe full model id
data[].agent.models[].partsobjectname, variant, version, snapshot when the id has them
data[].agent.models[].effortstring
data[].agent.models[].contextWindowinteger
data[].agent.models[].maxOutputTokensinteger
data[].agent.models[].providerstring
data[].agent.requestedobjectthe model that was asked for, where it differs
data[].agent.requested.modelstringthe full model id
data[].agent.requested.partsobjectname, variant, version, snapshot when the id has them
data[].agent.requested.effortstring
data[].agent.requested.contextWindowinteger
data[].agent.requested.maxOutputTokensinteger
data[].agent.requested.providerstring
data[].agent.runtimeobjectthe runtime plugin, with ai: whether a model played
data[].agent.runtime.idstring
data[].agent.runtime.namestring
data[].agent.runtime.versionstring
data[].agent.runtime.aibooleanfor the runtime: whether a model played (the plugin's hash is one the Archive trusts)
data[].agent.runtime.cliVersionsstring[]
data[].agent.harnessobjectthe harness name and version
data[].agent.tokensobjecttokens total, input and output, as the runtime reported them
data[].modsobject[]
data[].mods[].namestring
data[].mods[].versionstring
data[].timesobjectSeconds, from the bundle's own timeline; never recalculated here.
data[].times.igtnumberin-game time
data[].times.rtanumberreal time
data[].times.thinkingnumberreal time minus playback time
data[].times.toolCallsinteger
data[].times.playbacksinteger
data[].times.segmentsinteger
data[].times.toGoalnumber
data[].reachedGoalbooleanwhether the run reached its declared goal; a run that did not is not ranked
data[].placeintegerits place in its category, when the list is ranked
data[].examplebooleanpublished to show what an entry looks like, not as a submitted run
data[].titlestringthe Archive's own title for the run, where a reviewer gave one
data[].operatorstringthe name on the account that submitted it
data[].datesobject
data[].dates.startedstring
data[].dates.completedstring
data[].dates.endedstring
data[].dates.submittedstring
data[].dates.publishedstring
data[].dates.zonestring
data[].versionsobjectbundle and schema versions
totalintegerhow many runs match
next_cursorstringpass it as cursor for the next page; null on the last page
StatusWhen
200runs, newest first; with a game and a goal, grouped by category and ranked within it
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/runs/{run}

One run whole. Needs: an API token.

ParameterInMeaning
run requiredpath

Example: GET /api/v1/runs/sts-claude-code-01

Field in the answerTypeMeaning
dataobject
data.idstringthe address the run is published under; may change between revisions
data.uidstringthe run's identity, written once when the run was configured
data.revisionintegerwhich publication of the run this is
data.urlstringthe run's page on this site
data.gameobject
data.game.slugstringthe game's address on this site
data.game.idstringits id in the tooling
data.game.namestring
data.game.versionstring
data.game.platformstring
data.game.settingsobject
data.game.modsobject[]
data.game.mods[].namestring
data.game.mods[].versionstring
data.game.mods[].sourcestring
data.game.mods[].sha256string
data.categoryobjectThe seven axes of the specification's §3, game apart.
data.category.goalstring
data.category.goalLabelstring
data.category.buildstring
data.category.observationstring
data.category.inputstring
data.category.timingstring
data.category.humanstring
data.category.goalFinalbooleanwhether the goal is the game's own end
data.category.humanNotesstring
data.agentobject
data.agent.modelsobject[]the models that answered
data.agent.models[].modelstringthe full model id
data.agent.models[].partsobjectname, variant, version, snapshot when the id has them
data.agent.models[].effortstring
data.agent.models[].contextWindowinteger
data.agent.models[].maxOutputTokensinteger
data.agent.models[].providerstring
data.agent.requestedobjectthe model that was asked for, where it differs
data.agent.requested.modelstringthe full model id
data.agent.requested.partsobjectname, variant, version, snapshot when the id has them
data.agent.requested.effortstring
data.agent.requested.contextWindowinteger
data.agent.requested.maxOutputTokensinteger
data.agent.requested.providerstring
data.agent.runtimeobjectthe runtime plugin, with ai: whether a model played
data.agent.runtime.idstring
data.agent.runtime.namestring
data.agent.runtime.versionstring
data.agent.runtime.aibooleanfor the runtime: whether a model played (the plugin's hash is one the Archive trusts)
data.agent.runtime.cliVersionsstring[]
data.agent.harnessobjectthe harness name and version
data.agent.tokensobjecttokens total, input and output, as the runtime reported them
data.agent.pluginsobject
data.agent.toolsobject[]
data.agent.tools[].namestring
data.agent.tools[].descriptionstring
data.modsobject[]
data.mods[].namestring
data.mods[].versionstring
data.timesobjectSeconds, from the bundle's own timeline; never recalculated here.
data.times.igtnumberin-game time
data.times.rtanumberreal time
data.times.thinkingnumberreal time minus playback time
data.times.toolCallsinteger
data.times.playbacksinteger
data.times.segmentsinteger
data.times.toGoalnumber
data.reachedGoalbooleanwhether the run reached its declared goal; a run that did not is not ranked
data.placeintegerits place in its category, when the list is ranked
data.examplebooleanpublished to show what an entry looks like, not as a submitted run
data.titlestringthe Archive's own title for the run, where a reviewer gave one
data.operatorstringthe name on the account that submitted it
data.datesobject
data.dates.startedstring
data.dates.completedstring
data.dates.endedstring
data.dates.submittedstring
data.dates.publishedstring
data.dates.zonestring
data.versionsobjectbundle and schema versions
data.seedstring
data.fingerprintstringsha256 of the run's own log; binds a recording
data.recordingobjectthe recording as the bundle states it (SPEC §6)
data.recording.secondsnumber
data.recording.segmentSecondsnumber[]
data.recording.cutVideonumber
data.recording.playbackWallnumber
data.recording.completedRtanumber
data.recording.videosobject[]each video with its length and the code or line that binds it
data.recordingsobject[]
data.recordings[].urlstring
data.recordings[].platformstring
data.recordings[].hoststring
data.recordings[].kindstring
data.recordings[].confirmedAtstring
data.acceptanceobjecton whose word the run is in the Archive
data.acceptance.atstring
data.acceptance.bystring
data.acceptance.overrideboolean
data.acceptance.reasonstring
data.acceptance.missingstring[]
data.notestring
data.recordCountsobject
data.forumobject
data.forum.topicstring
data.goalsany[]
data.endsany[]
data.sectionsany[]
data.attemptsany[]
data.chaptersany[]
data.eventsobject[]
data.events[].sequenceinteger
data.events[].atnumber
data.events[].timestring
data.events[].eventstring
data.events[].dataobject
data.checksany[]the tooling's check, point by point
data.signatureobject
data.signature.signedboolean
data.signature.validboolean
data.signature.fingerprintstring
data.signature.accountstring
data.filesany[]
data.revisionsobject[]
data.revisions[].revisioninteger
data.revisions[].publishedstring
data.revisions[].currentboolean
data.bundlestringthe zip under /bundles/
data.logstring
StatusWhen
200one run whole
301The run is published under another address now; error.location names it.
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/runs/{run}/log

The run's log, record by record, in order. Needs: an API token.

ParameterInMeaning
run requiredpath
limitqueryrecords per page, at most 100; 100 when left out
cursorquerynext_cursor from the previous page, with the same other parameters

Example: GET /api/v1/runs/sts-claude-code-01/log?limit=100

Field in the answerTypeMeaning
runstring
dataobject[]
data[].sequenceintegerthe record's place in the log
data[].atnumberseconds from the start of the recording
data[].kindstring
data[].rolestringwho spoke
data[].toolstringfor a call and its result: the tool
data[].callstringthe call a result belongs to
data[].eventstringfor an event: its name
data[].dataobjectfor an event: what the harness attached
data[].bodystringwhat was said, the code that ran, or what came back, as the published log has it
next_cursorstringpass it as cursor for the next page; null on the last page
StatusWhen
200the run's log, record by record, in order
301The run is published under another address now; error.location names it.
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/stats

What the Archive holds in numbers. Needs: an API token.

ParameterInMeaning
gamequerya game's slug; tool calls per section need one

Example: GET /api/v1/stats?game=portal

Field in the answerTypeMeaning
gamestring
dataobject
data.totalsobjectruns, games, reached, models, runtimes, rta, igt, thinking, toolCalls, tokens
data.gamesobject[]
data.modelsobject[]
data.runtimesobject[]
data.sectionsobject[]
StatusWhen
200what the Archive holds in numbers
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
429Over the limit for this token; Retry-After says when to try again.

GET /api/v1/tickets/{ticket}

The state of a ticket. Needs: the tooling's sign-in, or the ticket's own secret.

ParameterInMeaning
ticket requiredpath
StatusWhen
200the ticket's heads and receipts
404no such ticket for you

Errors and limits

An error is an answer with the status and an object error holding status and message, the message in words. The statuses each endpoint can answer are in its own table above and in the OpenAPI document.

StatusWhen
301The run is published under another address now; error.location names it.
400A parameter or cursor that cannot be read.
401No token, or a revoked one: make one on your account page.
404No such game, run or endpoint.
405Anything but GET: the API only reads.
409the ticket has lapsed
422refused, with `code` and `reasons`
429Over the limit for this token; Retry-After says when to try again.

The limit is 300 requests per 5 minutes per token; every answer says in its RateLimit headers how much is left.

The shape of things

A run has three identifiers, and they are not interchangeable: uid is the identity, written once when the run was configured and never changed; id is the address it was published under and may change; revision counts publications of that same run. A series may start above 1 and may have gaps.

Timings come from the bundle's own timeline.json and are never recalculated here: rta is real time, igt the time the game actually ran, thinking the difference between real time and the time playbacks took.

No video is served. Where a recording is published is archive data added by the submitter. A recording is tied to a bundle by its verification code, aas and the first 32 hex digits of the fingerprint, which is the sha256 of the run's own session.sanitized.jsonl; the code stands in the video's description, or in its title.

The bundles themselves

Under /bundles/<run>.r<revision>.zip every revision of a bundle is served as it passed the Archive's checks when it was uploaded, without the private part of the upload. That is the evidence: anyone can recalculate the hashes against manifest.json without trusting anything this site says, on their own machine or in the browser.

Proof of when a run was played

  • The tooling asks the Archive to confirm when a run was played only when the runner is signed in with aas login, or has said yes to it for that run. It then makes three kinds of request to this site: one ticket before the run; a hash of its own logs at the start and end of every segment and once an hour, which the Archive signs with the time it received it; and, when the runner submits, the bundle. Nothing about the runner or the run goes with the ticket or the hashes.
  • The signatures are made with the Archive's proof key, published at /.well-known/aas-proof.txt, and they travel in the bundle's proof.json, so anyone can check them against that key. When a run is submitted the Archive holds them against what it received.

POST /api/v1/tickets

A ticket for a run about to be recorded. Needs: the tooling's sign-in.

No parameters.

StatusWhen
200the ticket, when it was issued, when it lapses, and the Archive's signature
429Over the limit for this token; Retry-After says when to try again.

DELETE /api/v1/tickets/{ticket}

Delete a ticket no run was submitted with. Needs: the tooling's sign-in, or the ticket's own secret.

ParameterInMeaning
ticket requiredpath
StatusWhen
200deleted
404no such ticket for you

POST /api/v1/tickets/{ticket}/heads

A hash of the logs as they stand, signed by the Archive with the time. Needs: the tooling's sign-in, or the ticket's own secret.

ParameterInMeaning
ticket requiredpath
StatusWhen
200done; for heads, the receipt
404no such ticket for you
409the ticket has lapsed

POST /api/v1/tickets/{ticket}/extend

Start the ticket's period again. Needs: the tooling's sign-in, or the ticket's own secret.

ParameterInMeaning
ticket requiredpath
StatusWhen
200done; for heads, the receipt
404no such ticket for you
409the ticket has lapsed

POST /api/v1/tickets/{ticket}/revoke

The ticket takes no more heads. Needs: the tooling's sign-in, or the ticket's own secret.

ParameterInMeaning
ticket requiredpath
StatusWhen
200done; for heads, the receipt
404no such ticket for you
409the ticket has lapsed

POST /api/v1/bundles

The upload zip of a run. Needs: the tooling's sign-in.

No parameters.

StatusWhen
200received: `status` review, the submission's number, the proof's status and the reasons a reviewer will see
401sign in with aas login
422refused, with `code` and `reasons`