DocsReference

HTTP API

The JSON API the site reads: every route with its parameters and its response field by field, the live streams, the transaction routes and the RPC relay.

Conventions

The API is the service the site reads its runs, models and totals from. It answers from an index of the program’s transactions kept in PostgreSQL, joined with live account reads for runs that are still turning. It asks for no key and no login: reads are open, and every action is authorized by a wallet’s signature on a Solana transaction.

Paths on this page are relative to the API’s base URL, written $API in the examples. The service listens on port 8312 unless PORT says otherwise.

BodiesJSON
Every response is JSON, except the event streams. Every POST body is JSON and is sent with Content-Type: application/json.
Amountsstring
Lamports and slots are unsigned integers written as decimal strings, so a 64-bit value arrives whole. Do arithmetic on them as big integers.
Countsnumber
Turn counts, token counts, byte sizes and compute units are JSON numbers.
Addressesstring
Addresses and transaction signatures are base58.
Timesstring
Times are ISO 8601 in UTC.
X-Contract-Versionresponse header
The version of the API’s contract, 0.3.1, on every response.
X-Request-Idresponse header
An id for the request, on every response.
Cache-Controlresponse header
no-store on every JSON response.

A browser calls the API from an origin the service lists in WEB_ORIGINS. POST routes under /v1 share an allowance of 60 requests a minute for each client; GET routes are not counted.

bash
curl "$API/v1/runs?state=running&limit=10"
curl "$API/v1/models?sealed=true"
curl -N "$API/v1/runs/stream"

All routes

MethodPathReturnsWhat it is
GET/healthHealthWhether the program is deployed and the index is caught up.
GET/v1/configConfigThe program’s address, fee account, turn reward and protocol share.
GET/v1/statsStatsTotals across every run and model.
GET/v1/runspage of RunSummaryRuns, newest first, filtered by state, model or opener.
GET/v1/runs/recordedRunDetailThe most recently opened complete run.
GET/v1/runs/:addressRunDetailOne run with its turns, budget, settlement and close.
GET/v1/runs/:address/turnspage of TurnOne run’s turns, from the first.
GET/v1/runs/streamevent streamEvery run as it changes.
GET/v1/runs/:address/streamevent streamOne run as it changes, turn by turn.
GET/v1/modelspage of ModelSummaryModels, newest first, filtered by publisher or sealed.
GET/v1/models/:addressModelDetailOne model with its weight accounts, vault and recent runs.
GET/v1/crank/queuepage of QueueItemRunning runs with the turns each has left.
GET/v1/crankerspage of CrankerTotalsCrankers by turns landed.
GET/v1/crankers/:addressCrankerTotalsOne wallet’s turns landed and rewards.
GET/v1/publishers/:addressPublisherTotalsOne wallet’s models and royalties.
GET/v1/tokenTokenInfoThe fee account and the protocol share behind CURTA.
POST/v1/runs/buildOpenRunBuildBuilds the transaction that opens a run.
POST/v1/runs/:address/turn/buildTxBuildBuilds one turn.
POST/v1/runs/:address/fund/buildTxBuildBuilds a deposit into a run’s budget.
POST/v1/runs/:address/cancel/buildTxBuildBuilds the opener’s cancel.
POST/v1/runs/:address/settle/buildTxBuildBuilds a settlement.
POST/v1/runs/:address/close/buildTxBuildBuilds the opener’s close.
POST/v1/models/:address/seal/buildSealModelBuildBuilds the next sealing step.
POST/v1/models/:address/royalty/buildTxBuildBuilds a royalty change.
POST/v1/models/:address/claim/buildTxBuildBuilds a claim of everything in the model’s vault.
POST/v1/transactions/submitTxStatusTakes a signed build and sends it.
GET/v1/transactions/:idTxStatusFollows a submitted build to its end.
POST/rpcJSON-RPCRelays one Solana RPC request.

:address is a base58 address of 32 bytes. Anything else answers VALIDATION_ERROR.

Pagination

Every list route takes the same two query parameters and answers in the same envelope.

limitinteger1 to 100, default 24
The most items in the page.
cursorstringoptional
The nextCursor of the page before. Leave it out for the first page.
itemsarray
The page’s items.
page.nextCursorstring | null
The cursor of the next page, or null on the last.
page.hasMoreboolean
Whether a next page exists.
asOfstring
When the page was read.
GET /v1/crankers?limit=1
{
  "items": [
    {
      "address": "24NhH6Rkb9ZtvcE9PaSu1ZXWPieLgk6MUYoDtNRXK4nc",
      "turnsLanded": 27,
      "earnedLamports": "270000",
      "lastTurnAt": "2026-11-14T10:07:05.000Z"
    }
  ],
  "page": { "nextCursor": "1", "hasMore": true },
  "asOf": "2026-11-14T10:10:00.000Z"
}

A query parameter the route does not name answers VALIDATION_ERROR.

Errors

Every failure, on every route but /rpc, is one envelope with the HTTP status of its code.

error.codestring
One of the codes below. Branch on this.
error.messagestring
A sentence for a person.
error.retryableboolean
Whether the same request can succeed later.
error.stagestring
Where it failed: validation, auth, read, upload, build, wallet, co_sign, simulation, send or confirmation.
error.fieldErrorsobjectoptional
For VALIDATION_ERROR: each field’s name with its messages.
error.programErrorobjectoptional
The program error behind a refusal: programId, name and number.
error.retryAfterSecondsintegeroptional
For RATE_LIMITED: the seconds until the allowance resets.
error.requestIdstringoptional
For INTERNAL_ERROR: the request’s id.
error.txFailureTxFailureoptional
For a failed build or submission: the transaction failure, described under Transactions.
422 CHAIN_REFUSED
{
  "error": {
    "code": "CHAIN_REFUSED",
    "message": "The run has insufficient budget for another turn.",
    "retryable": false,
    "stage": "confirmation",
    "programError": {
      "programId": "9bE6gq6FBeH9rXRfaHFyQkkfJ7DxSKbRSPPUxL2cc3tA",
      "name": "InsufficientBudget",
      "number": 6014
    }
  }
}
400 VALIDATION_ERROR
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Check the highlighted fields.",
    "retryable": false,
    "stage": "validation",
    "fieldErrors": {
      "state": ["Choose running, starved, complete or cancelled."]
    }
  }
}
CodeStatusWhen
VALIDATION_ERROR400A query, a path address or a body does not match the route. fieldErrors names each field.
UNAUTHORIZED401The wallet did not authorize the action.
ORIGIN_NOT_ALLOWED403A browser request from an origin the API does not list.
NOT_FOUND404No such run, model, transaction build or route.
PROGRAM_UNAVAILABLE409The program is not deployed on the API’s cluster.
CONFIGURATION_REQUIRED409The program’s Config account does not exist.
IDEMPOTENCY_CONFLICT409The same action was submitted with different parameters.
TRANSACTION_EXPIRED409The transaction’s blockhash ran out before it confirmed.
PAYLOAD_TOO_LARGE413A body over 1 MiB, or over 64 KiB on /rpc.
UNSUPPORTED_MEDIA_TYPE415A POST whose Content-Type is not application/json.
SIMULATION_FAILED422The transaction failed in simulation. txFailure says why.
CHAIN_REFUSED422The program refused the action. programError or txFailure names the error.
RATE_LIMITED429Over the request allowance. retryAfterSeconds and the Retry-After header say when to return.
INTERNAL_ERROR500The request could not be completed. requestId identifies it.
UPSTREAM_UNAVAILABLE502The cluster RPC could not be reached, or a transaction could not be sent.

Program error names and numbers are the program’s own, listed on Errors.

Health, config and stats

GET /health

Answers 200 in both states, so read status.

statusstring
live when the program is deployed and the index has caught up with the chain, waiting until then.
modestring
chain when the API reads the chain, fixture when it serves recorded data.
programDeployedboolean
Whether the program and its Config account exist on the cluster.
indexStatusstring
live or waiting: the index alone.
clusterstring
localnet, devnet or mainnet.
programIdstring | null
The address of the curta program.

GET /v1/config

sourcestring
chain or fixture, as mode above.
clusterstring
The cluster.
programIdstring | null
The program’s address.
feeVaultstring | null
The fee account, where the protocol share arrives.
turnRewardLamportsstring | null
The turn reward a run opened now pays per turn.
protocolShareBpsinteger | null
The protocol share in basis points, 0 to 10,000.

The last three are read from the Config account and are null while the program is not deployed.

GET /v1/config
{
  "source": "fixture",
  "cluster": "localnet",
  "programId": "9bE6gq6FBeH9rXRfaHFyQkkfJ7DxSKbRSPPUxL2cc3tA",
  "feeVault": "22P3dBWc667q6kiQBwFRoQDuy7vujt2hm4XWWUfAFzhP",
  "turnRewardLamports": "10000",
  "protocolShareBps": 500
}

GET /v1/stats

runsnumber
The runs ever opened.
tokensProducednumber
The tokens every run has produced.
turnsLandednumber
The turns ever landed.
modelsSealednumber
The sealed models.
paidToCrankersLamportsstring
The turn rewards paid to crankers.
asOfstring
When the totals were read.
GET /v1/stats
{
  "runs": 7,
  "tokensProduced": 18,
  "turnsLanded": 52,
  "modelsSealed": 1,
  "paidToCrankersLamports": "520000",
  "asOf": "2026-11-14T10:10:00.000Z"
}

Runs

GET /v1/runs

A page of RunSummary, ordered by opening time, newest first.

statestringoptional
running, starved, complete or cancelled.
modeladdressoptional
Runs on this model.
openeraddressoptional
Runs this address opened: a wallet, or the PDA a program opens runs from.
cursor, limitpagination
As under Pagination.

The fields of a RunSummary:

addressstring
The run account.
modelobject
The model’s address and name.
openerobject
For a wallet, kind: "wallet" and its address. For a program, kind: "program", the opening PDA as address, the program as programId, and programName, a string or null.
statestring
The run’s state. A closed run keeps the state it ended in.
outputTokensnumber
The tokens produced so far.
turnsLandednumber
The turns landed so far.
estimatedTotalTurnsnumber
The turns the run takes to its last token, from the quote for its prompt and parameters.
turnRewardLamportsstring
The turn reward the run pays per turn.
openedAtstring
When the run opened.
updatedAtstring
When it last changed.
closedAtstring | null
When it was closed.
one RunSummary
{
  "address": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb",
  "model": { "address": "J3msA9j85AE2eigDHxsDqGBmx1ZCvL8kxDafpWzwQX8J", "name": "stories260K" },
  "opener": { "kind": "wallet", "address": "A6LtbV456KWdL9XfNBN12UPN88wpangxkoMWWC2QeYDw" },
  "state": "running",
  "outputTokens": 1,
  "turnsLanded": 5,
  "estimatedTotalTurns": 10,
  "turnRewardLamports": "10000",
  "openedAt": "2026-11-14T10:07:00.000Z",
  "updatedAt": "2026-11-14T10:07:05.000Z",
  "closedAt": null
}

A run opened by a program carries this opener:

opener of a program-opened run
{
  "kind": "program",
  "address": "EtduiA17QrN2xvyHCuWr6UdNTiPTW3GHzwn9aEMwZdKE",
  "programId": "BEbThRbMRxvENEex1DK6LvbyDuZ8k4xpSonLDx8SMjtW",
  "programName": "oracle-of-stories"
}

GET /v1/runs/:address

A RunDetail: every field of the summary, and these. For a run that is still open the output, the progress and the budget come from the run account itself, so they are as fresh as the chain.

promptstring
The prompt.
promptTokenCountnumber | null
The prompt’s length in tokens. null until the run has tokenized it.
outputTextstring
The output so far.
outputTokenIdsnumber[]
The ids of the tokens produced.
outputPiecesstring[]
The text of each token, one for one with outputTokenIds. Joined, they are outputText.
parametersobject
maxTokens, temperature as a number from 0 to 16, and seed as a decimal string.
progress.phasestring
tokenizing, prompt, generation or finished.
progress.promptTokensProcessednumber
The prompt tokens the model has read.
progress.generatedTokensnumber
The tokens produced.
progress.currentTokenTurnnumber
The turns landed toward the token in progress.
progress.turnsPerCurrentTokennumber
The turns that token takes.
budget.depositedLamportsstring
The budget deposited over the run’s life.
budget.spentLamportsstring
What has left the run account: turn rewards, and after settlement the royalties and protocol share.
budget.remainingLamportsstring
The rest. Before settlement it includes the accrued royalties and protocol share.
accruedRoyaltyLamportsstring
The royalties the run owes the model’s vault.
accruedProtocolShareLamportsstring
The protocol share the run owes the fee account.
turnsTurn[]
Every indexed turn, in order. Turns has the fields.
fundEventsobject[]
Each deposit after opening: signature, funder, lamports, slot and blockTime.
settlementobject | null
Once settled: signature, royaltyPaidLamports, protocolSharePaidLamports, slot and blockTime.
closeobject | null
Once closed: signature, refundLamports, rentReturnedLamports, slot and blockTime.
GET /v1/runs/CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb
{
  "address": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb",
  "model": { "address": "J3msA9j85AE2eigDHxsDqGBmx1ZCvL8kxDafpWzwQX8J", "name": "stories260K" },
  "opener": { "kind": "wallet", "address": "A6LtbV456KWdL9XfNBN12UPN88wpangxkoMWWC2QeYDw" },
  "state": "running",
  "outputTokens": 1,
  "turnsLanded": 5,
  "estimatedTotalTurns": 10,
  "turnRewardLamports": "10000",
  "openedAt": "2026-11-14T10:07:00.000Z",
  "updatedAt": "2026-11-14T10:07:05.000Z",
  "closedAt": null,
  "prompt": "Tell a tiny story.",
  "promptTokenCount": 2,
  "outputText": "Once",
  "outputTokenIds": [71],
  "outputPieces": ["Once"],
  "parameters": { "maxTokens": 4, "temperature": 0.8, "seed": "42" },
  "progress": {
    "phase": "generation",
    "promptTokensProcessed": 2,
    "generatedTokens": 1,
    "currentTokenTurn": 1,
    "turnsPerCurrentToken": 2
  },
  "budget": { "depositedLamports": "150000", "spentLamports": "50000", "remainingLamports": "100000" },
  "accruedRoyaltyLamports": "200",
  "accruedProtocolShareLamports": "2510",
  "turns": [
    { "runAddress": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb", "index": 0, "signature": "MF8GG3GRyQVw7SK87gZKJ94qwQSXmbvVTb7oi5fGUpji2Ws3qbfE79t4xvSxP5CeNvxBseStBsnVcFU4zdRGihv", "cranker": "24NhH6Rkb9ZtvcE9PaSu1ZXWPieLgk6MUYoDtNRXK4nc", "slot": "100420", "blockTime": "2026-11-14T10:07:01.000Z", "computeUnits": 790000, "rewardLamports": "10000", "completedTokenIds": [], "completedPieces": [], "textDelta": "" },
    { "runAddress": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb", "index": 1, "signature": "5QPUFbxu6ed5btcS6mM9UTt4MLk8czNBU8wMp3yAszDoEUNAagm4Ups5KH2jUyEVZxvo723SPiChLMvXXktpTbmo", "cranker": "8YWiZFj2ygKtX9GZ5KAY2C48jStkKUuL3ZaSujEvLYdD", "slot": "100421", "blockTime": "2026-11-14T10:07:02.000Z", "computeUnits": 790000, "rewardLamports": "10000", "completedTokenIds": [], "completedPieces": [], "textDelta": "" },
    { "runAddress": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb", "index": 2, "signature": "4MhNPJ1sw1Qw6iDy8ptFBrcT2oT2SL7MHMqaPVfZWBiNwRvjp9FMNJnh5euk93pjMHDNLvd4rRC1pFtoKADXycCr", "cranker": "24NhH6Rkb9ZtvcE9PaSu1ZXWPieLgk6MUYoDtNRXK4nc", "slot": "100422", "blockTime": "2026-11-14T10:07:03.000Z", "computeUnits": 1180000, "rewardLamports": "10000", "completedTokenIds": [], "completedPieces": [], "textDelta": "" },
    { "runAddress": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb", "index": 3, "signature": "3J9RJ8rWi7h2m5QUDA6DxmT8pCCWcFKevB4q1QPWiVCySRnSyoASbiy4CLDKJPPNt5Yjb1hU4gWGMZxKgY3kNVzP", "cranker": "8YWiZFj2ygKtX9GZ5KAY2C48jStkKUuL3ZaSujEvLYdD", "slot": "100423", "blockTime": "2026-11-14T10:07:04.000Z", "computeUnits": 1180000, "rewardLamports": "10000", "completedTokenIds": [71], "completedPieces": ["Once"], "textDelta": "Once" },
    { "runAddress": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb", "index": 4, "signature": "5am56trz4qmVvWVNESPC5pd2knhzdJAFXkdksRotAES6vxB4M2tEREYBaVfAMFKovxMo6msBAdQUqwjED675MBCg", "cranker": "24NhH6Rkb9ZtvcE9PaSu1ZXWPieLgk6MUYoDtNRXK4nc", "slot": "100424", "blockTime": "2026-11-14T10:07:05.000Z", "computeUnits": 1180000, "rewardLamports": "10000", "completedTokenIds": [], "completedPieces": [], "textDelta": "" }
  ],
  "fundEvents": [
    { "signature": "4EvRAkUiEXJQ96SDKU4t58ns1GXuUXAkTDGZxntaxXgSitGrBLEsHeNad5Cu86ATUGRgq7hx5FQpu16e4Yz5zzBq", "funder": "A6LtbV456KWdL9XfNBN12UPN88wpangxkoMWWC2QeYDw", "lamports": "50000", "slot": "100423", "blockTime": "2026-11-14T10:07:03.000Z" }
  ],
  "settlement": null,
  "close": null
}

A settled and closed run carries these two objects in place of the two nulls:

settlement and close
"settlement": {
  "signature": "5AnK9QQVkNZ2jGaKcbxxR2xB2Sk2HzauNVW8jxAmq9pXM8nnRLXmaCPU2ewZ37WGpYnDXbfNZFnfHbFtuCN9CMCF",
  "royaltyPaidLamports": "800",
  "protocolSharePaidLamports": "5040",
  "slot": "100131",
  "blockTime": "2026-11-14T10:02:12.000Z"
},
"close": {
  "signature": "4Zc4FFEcdvgxL8tJWfaAv3cgvU5wLNE8dwFdFj8qDu65kNVP1VdgSKwMExVgjaH5pP9NCjSCm2yH8avJzp1tzkGp",
  "refundLamports": "44160",
  "rentReturnedLamports": "2039280",
  "slot": "100132",
  "blockTime": "2026-11-14T10:02:13.000Z"
}

GET /v1/runs/recorded

The RunDetail of the most recently opened run whose state is complete. The site replays it where it shows a run in motion. It answers NOT_FOUND while no run has completed.

Turns

GET /v1/runs/:address/turns

A page of one run’s turns, from index zero upward. It takes cursor and limit.

runAddressstring
The run.
indexnumber
The turn’s number in the run, from zero.
signaturestring
The turn’s transaction.
crankerstring
The wallet that landed it.
slotstring
The slot it landed in.
blockTimestring
The time of that block.
computeUnitsnumber
The compute units the transaction used.
rewardLamportsstring
The turn reward it paid the cranker.
completedTokenIdsnumber[]
The ids of the tokens this turn completed: none, one or several.
completedPiecesstring[]
The text of each, one for one.
textDeltastring
The pieces joined: the text this turn added.
a turn that completed two tokens
{
  "runAddress": "FDjiXFzfJ7fuqRjV9akjJisbAcbCDaVhivQJcgmScCx4",
  "index": 9,
  "signature": "5dPdgUngUVeD7nSCJdyBspANmC3K4gHpF359o4rz1XHrZ1QCFoE8idwDvEgYGnXAakw92KVJcHYchMsTCRgNfpKs",
  "cranker": "8YWiZFj2ygKtX9GZ5KAY2C48jStkKUuL3ZaSujEvLYdD",
  "slot": "100069",
  "blockTime": "2026-11-14T10:01:10.000Z",
  "computeUnits": 1180000,
  "rewardLamports": "10000",
  "completedTokenIds": [9, 432],
  "completedPieces": [" a", " time."],
  "textDelta": " a time."
}

Runs and turns describes what a turn computes.

Live streams

Two routes answer with Server-Sent Events, for a browser’s EventSource or any client that reads text/event-stream. Each event’s name is its type, and its data is one JSON envelope.

typestring
The event’s type, the same as the event’s name.
idstring
The event’s id.
timestampstring
When the API sent it.
dataobject
The payload, by type.
TypeDataSent
readyHealthFirst, on every connection.
run.updatedRunSummary on the list stream, RunDetail on a run’s streamWhen a run opens, turns, is funded, settles or closes.
turnTurnOn a run’s stream, for each turn as it is indexed.
resyncreason: behind, reconnected or loopWhen the client should read its snapshot again: after a reconnect, or when events passed it by.

GET /v1/runs/stream

Every run’s changes. It takes state, model and opener as GET /v1/runs does; model and opener narrow the events sent. With state, drop a run from your list when an update shows it has left that state.

GET /v1/runs/:address/stream

One run’s changes. It answers NOT_FOUND for an address that is not a run.

follow.ts
const source = new EventSource(API + '/v1/runs/' + address + '/stream')

source.addEventListener('turn', (e) => {
  const { data: turn } = JSON.parse(e.data)
  console.log(turn.index, turn.cranker, turn.textDelta)
})
source.addEventListener('run.updated', (e) => {
  const { data: run } = JSON.parse(e.data)
  if (run.state !== 'running') source.close()
})
source.addEventListener('resync', async () => {
  const run = await (await fetch(API + '/v1/runs/' + address)).json()
  console.log(run.outputText)
})

The stream replays nothing: a client that reconnects is sent resync and reads the run again. A comment line keeps an idle stream open every 15 seconds.

Models

GET /v1/models

A page of ModelSummary, newest first.

publisheraddressoptional
Models this wallet published.
sealedtrue | falseoptional
Sealed models only, or models still uploading only.
cursor, limitpagination
As under Pagination.
addressstring
The Model account.
namestring
The model’s name.
publisherstring
The wallet that published it.
sealedboolean
Whether it is sealed.
royaltyPerTokenLamportsstring
The royalty per token.
configurationobject
architecture (llama2), parameters, dimensions, hiddenDimensions, layers, heads, kvHeads, vocabulary, context and tokenizer.
metricsobject | null
For a sealed model: source, turnsPerPromptToken, turnsPerGeneratedToken and computeUnitsPerMultiplyAccumulate. source is measured when the figures come from measured runs of these exact weights, planned when they come from quotes, and fixture in recorded data. Measured metrics add the exact averages as averageTurnsPerPromptToken and averageTurnsPerGeneratedToken. Planned metrics have a null compute figure and add planning: the prompt, its token count and the two quotes the counts were taken from.
uploadobject
writtenBytes of totalBytes of weights on chain.
vaultobject
The royalty vault: its address, its balanceLamports above rent, totalEarnedLamports and totalClaimedLamports.
runsnumber
The runs opened on the model.
tokensProducednumber
The tokens it has produced.
createdAtstring
When it was created.
sealedAtstring | null
When it was sealed.

GET /v1/models/:address

A ModelDetail: the summary, and three more fields.

weightsobject[]
Each weight account in order: address, sizeBytes and writtenBytes.
weightsHashstring
The weights hash the model declares, 64 hex characters.
recentRunsRunSummary[]
The model’s five newest runs.
GET /v1/models/J3msA9j85AE2eigDHxsDqGBmx1ZCvL8kxDafpWzwQX8J
{
  "address": "J3msA9j85AE2eigDHxsDqGBmx1ZCvL8kxDafpWzwQX8J",
  "name": "stories260K",
  "publisher": "agVVouEyGWMAyUkjmR1F2zkAvYZZ3HLvJgxVC861Rjk",
  "sealed": true,
  "royaltyPerTokenLamports": "200",
  "configuration": {
    "architecture": "llama2",
    "parameters": 260000,
    "dimensions": 64,
    "hiddenDimensions": 172,
    "layers": 5,
    "heads": 8,
    "kvHeads": 4,
    "vocabulary": 512,
    "context": 512,
    "tokenizer": "llama2.c-512"
  },
  "metrics": {
    "source": "fixture",
    "turnsPerPromptToken": 1,
    "turnsPerGeneratedToken": 2,
    "computeUnitsPerMultiplyAccumulate": 5
  },
  "upload": { "writtenBytes": 270000, "totalBytes": 270000 },
  "vault": {
    "address": "DMtKtkRdWhBTArhu3T8wgGC4bpsHDQr1UkbryY15Ly7F",
    "balanceLamports": "2000",
    "totalEarnedLamports": "2600",
    "totalClaimedLamports": "600"
  },
  "runs": 7,
  "tokensProduced": 18,
  "createdAt": "2026-11-14T09:00:00.000Z",
  "sealedAt": "2026-11-14T09:10:00.000Z",
  "weights": [
    { "address": "9orNZ4zbgSHuTuu6N4eQgpSkLZoiihtRHmbWu91SAJnr", "sizeBytes": 270000, "writtenBytes": 270000 }
  ],
  "weightsHash": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
  "recentRuns": [
    {
      "address": "CTo3gEuJa8YZskNesnHjbDmdDAoXr9NHGwf18KSFjKWb",
      "model": { "address": "J3msA9j85AE2eigDHxsDqGBmx1ZCvL8kxDafpWzwQX8J", "name": "stories260K" },
      "opener": { "kind": "wallet", "address": "A6LtbV456KWdL9XfNBN12UPN88wpangxkoMWWC2QeYDw" },
      "state": "running",
      "outputTokens": 1,
      "turnsLanded": 5,
      "estimatedTotalTurns": 10,
      "turnRewardLamports": "10000",
      "openedAt": "2026-11-14T10:07:00.000Z",
      "updatedAt": "2026-11-14T10:07:05.000Z",
      "closedAt": null
    }
  ]
}

The example shows the first of the five recent runs. Models describes the configuration and Weight format the weight accounts.

Queue and crankers

GET /v1/crank/queue

A page of the running runs. Each item is a RunSummary with one more field.

estimatedTurnsLeftnumber
estimatedTotalTurns less turnsLanded, never below zero.

GET /v1/crankers

A page of CrankerTotals, most turns landed first.

GET /v1/crankers/:address

One wallet’s totals. A wallet that has landed no turn answers with zeros.

addressstring
The cranker’s wallet.
turnsLandednumber
The turns it has landed.
earnedLamportsstring
The turn rewards it has received.
lastTurnAtstring | null
The time of its latest turn.
GET /v1/crankers/24NhH6Rkb9ZtvcE9PaSu1ZXWPieLgk6MUYoDtNRXK4nc
{
  "address": "24NhH6Rkb9ZtvcE9PaSu1ZXWPieLgk6MUYoDtNRXK4nc",
  "turnsLanded": 27,
  "earnedLamports": "270000",
  "lastTurnAt": "2026-11-14T10:07:05.000Z"
}

Run a cranker covers the work behind these numbers.

Publishers

GET /v1/publishers/:address

addressstring
The publisher’s wallet.
modelsnumber
The count of its models.
ownedModelsModelSummary[]
Each of its models, with upload progress and vault totals.
modelsSealednumber
How many are sealed.
runsnumber
The runs opened on its models.
tokensProducednumber
The tokens its models have produced.
earnedLamportsstring
The royalties settled to its vaults.
claimedLamportsstring
The royalties it has claimed.
vaultBalanceLamportsstring
The royalties waiting in its vaults.
GET /v1/publishers/agVVouEyGWMAyUkjmR1F2zkAvYZZ3HLvJgxVC861Rjk
{
  "address": "agVVouEyGWMAyUkjmR1F2zkAvYZZ3HLvJgxVC861Rjk",
  "models": 2,
  "modelsSealed": 1,
  "runs": 7,
  "tokensProduced": 18,
  "earnedLamports": "2600",
  "claimedLamports": "600",
  "vaultBalanceLamports": "2000"
}

Publish a model covers royalties and claims.

CURTA

GET /v1/token

tickerstring
CURTA.
mintstring | null
The mint’s address.
venueUrlstring | null
Where CURTA trades.
feeVaultstring | null
The fee account.
feeVaultBalanceLamportsstring
The protocol share settled into the fee account and held there, above its rent.
protocolShareAccruedLamportsstring
The protocol share every run has accrued.
asOfstring
When the figures were read.
GET /v1/token
{
  "ticker": "CURTA",
  "mint": null,
  "venueUrl": null,
  "feeVault": "22P3dBWc667q6kiQBwFRoQDuy7vujt2hm4XWWUfAFzhP",
  "feeVaultBalanceLamports": "17130",
  "protocolShareAccruedLamports": "26180",
  "asOf": "2026-11-14T10:10:00.000Z"
}

CURTA says what the fee account’s balance is for.

Transactions

The API holds no keys of yours. For every action it builds an unsigned transaction, the wallet signs it, and the API sends the signed bytes and follows them. This is how each button on the site works.

  1. Build

    POST to the action’s build route with the signing wallet’s address. The answer is a TxBuild: an id and the unsigned version 0 transaction in base64.

  2. Sign

    The wallet signs transactionBase64 as it is.

  3. Submit

    POST the signed bytes to /v1/transactions/submit with the build’s id and an idempotency key of your choosing.

  4. Follow

    GET /v1/transactions/:id until the status is confirmed, finalized, failed or expired.

The build routes

RouteBodyThe wallet is
POST /v1/runs/buildwallet, model, prompt, maxTokens, temperature, seed, budgetLamports?The opener, who funds the run.
POST /v1/runs/:address/turn/buildwalletThe cranker. Any wallet.
POST /v1/runs/:address/fund/buildwallet, amountLamportsThe funder. Any wallet.
POST /v1/runs/:address/cancel/buildwalletThe run’s opener.
POST /v1/runs/:address/settle/buildwalletAny wallet.
POST /v1/runs/:address/close/buildwalletThe run’s opener.
POST /v1/models/:address/seal/buildwalletThe model’s publisher.
POST /v1/models/:address/royalty/buildwallet, royaltyPerTokenLamportsThe model’s publisher.
POST /v1/models/:address/claim/buildwalletThe model’s publisher.

The body of POST /v1/runs/build:

walletaddress
The opener: fee payer and source of the rent and the budget.
modeladdress
A sealed model.
promptstring
At least one character, at most 1,024 bytes of UTF-8.
maxTokensinteger
From 1 to the model’s context length.
temperaturenumber
From 0 to 16.
seedstring
An unsigned 64-bit integer as a decimal string.
budgetLamportsstringoptional
The budget to deposit. Left out, the run opens on the quote’s total budget for this prompt and these parameters.

amountLamports on the fund route is greater than zero. Both it and royaltyPerTokenLamports are decimal strings.

TxBuild

idstring
The build’s id, named when submitting and following.
transactionBase64string
The unsigned transaction.
blockhashstring
The blockhash it was built on.
lastValidBlockHeightnumber
The block height after which that blockhash is no longer accepted.
cosignedboolean
Whether the API adds a signature of its own after the wallet’s. True for a new run, whose account’s keypair the API holds until submission.
runAddressstringopen run only
The address of the run the transaction opens.
nextStepstringseal only
The model’s sealing step after this transaction.
moreRemainbooleanseal only
Whether the model takes further sealing steps. Confirm this transaction, then build again, until it is false.

POST /v1/transactions/submit

The body is one of two shapes, told apart by mode.

mode: relaybuildId, signedTransactionBase64, idempotencyKey
The API checks the signed bytes against what it built, adds its own signature when the build is cosigned, sends the transaction and records it. A build that opens a run is submitted this way.
mode: wallet-sentbuildId, signature, idempotencyKey
The wallet has sent the transaction itself, and the API follows that signature.

The same submission sent twice with the same idempotencyKey answers with what the first one did.

TxStatus

The answer of the submit route and of GET /v1/transactions/:id.

idstring
The build’s id.
statusstring
submitted, unconfirmed, confirmed, finalized, failed or expired. The last four are final.
signaturestringonce sent
The transaction’s signature.
errorTxFailurewhen failed
Why it failed.

TxFailure

stagestring
Where the transaction failed: build, wallet-rejected, wallet-changed, simulation, send or confirm.
messagestring
What happened.
nextStepstringoptional
What to do about it.
retryableboolean
Whether building again can succeed.
programobjectoptional
The program that refused: its address, the error’s code and its name.
a TxFailure
{
  "stage": "simulation",
  "message": "The run has insufficient budget for another turn.",
  "nextStep": "Fund the run before turning it.",
  "retryable": false,
  "program": {
    "address": "9bE6gq6FBeH9rXRfaHFyQkkfJ7DxSKbRSPPUxL2cc3tA",
    "code": 6014,
    "name": "InsufficientBudget"
  }
}

A build or submission that fails answers with the error envelope, the failure inside it as error.txFailure: SIMULATION_FAILED for the simulation stage, UPSTREAM_UNAVAILABLE for the send stage, CHAIN_REFUSED for the others.

turn.ts
const post = (path: string, body: unknown) =>
  fetch(API + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then((r) => r.json())

const build = await post('/v1/runs/' + run + '/turn/build', { wallet: wallet.publicKey.toBase58() })

const tx = VersionedTransaction.deserialize(Buffer.from(build.transactionBase64, 'base64'))
tx.sign([wallet])

let status = await post('/v1/transactions/submit', {
  mode: 'relay',
  buildId: build.id,
  signedTransactionBase64: Buffer.from(tx.serialize()).toString('base64'),
  idempotencyKey: crypto.randomUUID(),
})
while (!['confirmed', 'finalized', 'failed', 'expired'].includes(status.status)) {
  await new Promise((resolve) => setTimeout(resolve, 500))
  status = await (await fetch(API + '/v1/transactions/' + build.id)).json()
}
console.log(status.status, status.signature)

wallet is a Keypair and VersionedTransaction comes from @solana/web3.js. Instructions describes the instruction inside each build, and the SDK sends the same instructions without the API.

RPC relay

POST /rpc

A JSON-RPC relay to the cluster, so a browser’s Connection points at the API. It takes one JSON-RPC 2.0 request with an id, a method and array params, and returns the cluster’s response unchanged. It relays these methods:

getAccountInfo, getMultipleAccounts, getBalance, getTokenAccountBalance, getSlot, getBlockHeight, getLatestBlockhash, isBlockhashValid, getFeeForMessage, getMinimumBalanceForRentExemption, getSignatureStatuses, getTransaction, getSignaturesForAddress, getGenesisHash, getVersion, simulateTransaction, sendTransaction

Each client has 600 reads and 30 sendTransaction calls a minute. Its failures are JSON-RPC errors, not the envelope above:

CodeStatusWhen
-32700400The body is not JSON.
-32600400Not one JSON-RPC 2.0 request with an id, a method and array params. A body over 64 KiB answers 413.
-32601400The method is not relayed.
-32005429Over the allowance. Retry-After says when to return.
-32004200The program is not deployed on the cluster.
-32603502The cluster RPC could not be reached.
connection.ts
import { Connection } from '@solana/web3.js'
import { Curta } from '@curta/sdk'

const curta = new Curta({ connection: new Connection(API + '/rpc', 'confirmed') })
const run = await curta.getRun(address)

The typed client

The @curta/contract package is the API’s contract as code: a schema for every response on this page, the route table, and a client that checks each response against its schema. It depends only on zod and runs in the browser and in Node.

client.ts
import { ApiClientError, createClient } from '@curta/contract'

const api = createClient(baseUrl)

const { items, page } = await api.runs({ state: 'running', limit: 10 })
const run = await api.run(items[0].address)
const turns = await api.turns(run.address, { limit: 100 })
const queue = await api.queue()
const mine = await api.cranker(wallet)

try {
  await api.model(address)
} catch (error) {
  if (error instanceof ApiClientError) console.log(error.status, error.code, error.body.error.message)
}

const source = new EventSource(api.runStreamUrl(run.address))
GroupMethods
Servicehealth, config, stats, token
Runsruns, run, recordedRun, turns
StreamsrunsStreamUrl, runStreamUrl
Modelsmodels, model, publisher
Crankingqueue, crankers, cranker
BuildsopenRun, turn, fund, cancel, settle, closeRun, sealModel, setRoyalty, claimRoyalties
TransactionssubmitTransaction, transaction, rpc

A failed call throws ApiClientError, with the HTTP status, the error code and the whole envelope as body. The package also exports each schema and its type by the names used on this page, from Health to TxStatus, and routes, the table of every method, path and schema.