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 headerno-storeon 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.
curl "$API/v1/runs?state=running&limit=10"
curl "$API/v1/models?sealed=true"
curl -N "$API/v1/runs/stream"All routes
| Method | Path | Returns | What it is |
|---|---|---|---|
| GET | /health | Health | Whether the program is deployed and the index is caught up. |
| GET | /v1/config | Config | The program’s address, fee account, turn reward and protocol share. |
| GET | /v1/stats | Stats | Totals across every run and model. |
| GET | /v1/runs | page of RunSummary | Runs, newest first, filtered by state, model or opener. |
| GET | /v1/runs/recorded | RunDetail | The most recently opened complete run. |
| GET | /v1/runs/:address | RunDetail | One run with its turns, budget, settlement and close. |
| GET | /v1/runs/:address/turns | page of Turn | One run’s turns, from the first. |
| GET | /v1/runs/stream | event stream | Every run as it changes. |
| GET | /v1/runs/:address/stream | event stream | One run as it changes, turn by turn. |
| GET | /v1/models | page of ModelSummary | Models, newest first, filtered by publisher or sealed. |
| GET | /v1/models/:address | ModelDetail | One model with its weight accounts, vault and recent runs. |
| GET | /v1/crank/queue | page of QueueItem | Running runs with the turns each has left. |
| GET | /v1/crankers | page of CrankerTotals | Crankers by turns landed. |
| GET | /v1/crankers/:address | CrankerTotals | One wallet’s turns landed and rewards. |
| GET | /v1/publishers/:address | PublisherTotals | One wallet’s models and royalties. |
| GET | /v1/token | TokenInfo | The fee account and the protocol share behind CURTA. |
| POST | /v1/runs/build | OpenRunBuild | Builds the transaction that opens a run. |
| POST | /v1/runs/:address/turn/build | TxBuild | Builds one turn. |
| POST | /v1/runs/:address/fund/build | TxBuild | Builds a deposit into a run’s budget. |
| POST | /v1/runs/:address/cancel/build | TxBuild | Builds the opener’s cancel. |
| POST | /v1/runs/:address/settle/build | TxBuild | Builds a settlement. |
| POST | /v1/runs/:address/close/build | TxBuild | Builds the opener’s close. |
| POST | /v1/models/:address/seal/build | SealModelBuild | Builds the next sealing step. |
| POST | /v1/models/:address/royalty/build | TxBuild | Builds a royalty change. |
| POST | /v1/models/:address/claim/build | TxBuild | Builds a claim of everything in the model’s vault. |
| POST | /v1/transactions/submit | TxStatus | Takes a signed build and sends it. |
| GET | /v1/transactions/:id | TxStatus | Follows a submitted build to its end. |
| POST | /rpc | JSON-RPC | Relays 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
nextCursorof 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
nullon the last. page.hasMoreboolean- Whether a next page exists.
asOfstring- When the page was read.
{
"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,sendorconfirmation. error.fieldErrorsobjectoptional- For
VALIDATION_ERROR: each field’s name with its messages. error.programErrorobjectoptional- The program error behind a refusal:
programId,nameandnumber. 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.
{
"error": {
"code": "CHAIN_REFUSED",
"message": "The run has insufficient budget for another turn.",
"retryable": false,
"stage": "confirmation",
"programError": {
"programId": "9bE6gq6FBeH9rXRfaHFyQkkfJ7DxSKbRSPPUxL2cc3tA",
"name": "InsufficientBudget",
"number": 6014
}
}
}{
"error": {
"code": "VALIDATION_ERROR",
"message": "Check the highlighted fields.",
"retryable": false,
"stage": "validation",
"fieldErrors": {
"state": ["Choose running, starved, complete or cancelled."]
}
}
}| Code | Status | When |
|---|---|---|
| VALIDATION_ERROR | 400 | A query, a path address or a body does not match the route. fieldErrors names each field. |
| UNAUTHORIZED | 401 | The wallet did not authorize the action. |
| ORIGIN_NOT_ALLOWED | 403 | A browser request from an origin the API does not list. |
| NOT_FOUND | 404 | No such run, model, transaction build or route. |
| PROGRAM_UNAVAILABLE | 409 | The program is not deployed on the API’s cluster. |
| CONFIGURATION_REQUIRED | 409 | The program’s Config account does not exist. |
| IDEMPOTENCY_CONFLICT | 409 | The same action was submitted with different parameters. |
| TRANSACTION_EXPIRED | 409 | The transaction’s blockhash ran out before it confirmed. |
| PAYLOAD_TOO_LARGE | 413 | A body over 1 MiB, or over 64 KiB on /rpc. |
| UNSUPPORTED_MEDIA_TYPE | 415 | A POST whose Content-Type is not application/json. |
| SIMULATION_FAILED | 422 | The transaction failed in simulation. txFailure says why. |
| CHAIN_REFUSED | 422 | The program refused the action. programError or txFailure names the error. |
| RATE_LIMITED | 429 | Over the request allowance. retryAfterSeconds and the Retry-After header say when to return. |
| INTERNAL_ERROR | 500 | The request could not be completed. requestId identifies it. |
| UPSTREAM_UNAVAILABLE | 502 | The 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.
statusstringlivewhen the program is deployed and the index has caught up with the chain,waitinguntil then.modestringchainwhen the API reads the chain,fixturewhen it serves recorded data.programDeployedboolean- Whether the program and its
Configaccount exist on the cluster. indexStatusstringliveorwaiting: the index alone.clusterstringlocalnet,devnetormainnet.programIdstring | null- The address of the
curtaprogram.
GET /v1/config
sourcestringchainorfixture, asmodeabove.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.
{
"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.
{
"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.
statestringoptionalrunning,starved,completeorcancelled.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
addressandname. openerobject- For a wallet,
kind: "wallet"and itsaddress. For a program,kind: "program", the opening PDA asaddress, the program asprogramId, andprogramName, a string ornull. 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.
{
"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:
{
"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.
nulluntil 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 areoutputText. parametersobjectmaxTokens,temperatureas a number from 0 to 16, andseedas a decimal string.progress.phasestringtokenizing,prompt,generationorfinished.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,slotandblockTime. settlementobject | null- Once settled:
signature,royaltyPaidLamports,protocolSharePaidLamports,slotandblockTime. closeobject | null- Once closed:
signature,refundLamports,rentReturnedLamports,slotandblockTime.
{
"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": {
"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.
{
"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.
| Type | Data | Sent |
|---|---|---|
| ready | Health | First, on every connection. |
| run.updated | RunSummary on the list stream, RunDetail on a run’s stream | When a run opens, turns, is funded, settles or closes. |
| turn | Turn | On a run’s stream, for each turn as it is indexed. |
| resync | reason: behind, reconnected or loop | When 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.
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
Modelaccount. namestring- The model’s name.
publisherstring- The wallet that published it.
sealedboolean- Whether it is sealed.
royaltyPerTokenLamportsstring- The royalty per token.
configurationobjectarchitecture(llama2),parameters,dimensions,hiddenDimensions,layers,heads,kvHeads,vocabulary,contextandtokenizer.metricsobject | null- For a sealed model:
source,turnsPerPromptToken,turnsPerGeneratedTokenandcomputeUnitsPerMultiplyAccumulate.sourceismeasuredwhen the figures come from measured runs of these exact weights,plannedwhen they come from quotes, andfixturein recorded data. Measured metrics add the exact averages asaverageTurnsPerPromptTokenandaverageTurnsPerGeneratedToken. Planned metrics have anullcompute figure and addplanning: the prompt, its token count and the two quotes the counts were taken from. uploadobjectwrittenBytesoftotalBytesof weights on chain.vaultobject- The royalty vault: its
address, itsbalanceLamportsabove rent,totalEarnedLamportsandtotalClaimedLamports. 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,sizeBytesandwrittenBytes. weightsHashstring- The weights hash the model declares, 64 hex characters.
recentRunsRunSummary[]- The model’s five newest runs.
{
"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.
estimatedTurnsLeftnumberestimatedTotalTurnslessturnsLanded, 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.
{
"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.
{
"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
tickerstringCURTA.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.
{
"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.
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.Sign
The wallet signs
transactionBase64as it is.Submit
POST the signed bytes to
/v1/transactions/submitwith the build’s id and an idempotency key of your choosing.Follow
GET
/v1/transactions/:iduntil the status isconfirmed,finalized,failedorexpired.
The build routes
| Route | Body | The wallet is |
|---|---|---|
| POST /v1/runs/build | wallet, model, prompt, maxTokens, temperature, seed, budgetLamports? | The opener, who funds the run. |
| POST /v1/runs/:address/turn/build | wallet | The cranker. Any wallet. |
| POST /v1/runs/:address/fund/build | wallet, amountLamports | The funder. Any wallet. |
| POST /v1/runs/:address/cancel/build | wallet | The run’s opener. |
| POST /v1/runs/:address/settle/build | wallet | Any wallet. |
| POST /v1/runs/:address/close/build | wallet | The run’s opener. |
| POST /v1/models/:address/seal/build | wallet | The model’s publisher. |
| POST /v1/models/:address/royalty/build | wallet, royaltyPerTokenLamports | The model’s publisher. |
| POST /v1/models/:address/claim/build | wallet | The 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.
statusstringsubmitted,unconfirmed,confirmed,finalized,failedorexpired. 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,sendorconfirm. messagestring- What happened.
nextStepstringoptional- What to do about it.
retryableboolean- Whether building again can succeed.
programobjectoptional- The program that refused: its
address, the error’scodeand itsname.
{
"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.
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:
| Code | Status | When |
|---|---|---|
| -32700 | 400 | The body is not JSON. |
| -32600 | 400 | Not one JSON-RPC 2.0 request with an id, a method and array params. A body over 64 KiB answers 413. |
| -32601 | 400 | The method is not relayed. |
| -32005 | 429 | Over the allowance. Retry-After says when to return. |
| -32004 | 200 | The program is not deployed on the cluster. |
| -32603 | 502 | The cluster RPC could not be reached. |
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.
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))| Group | Methods |
|---|---|
| Service | health, config, stats, token |
| Runs | runs, run, recordedRun, turns |
| Streams | runsStreamUrl, runStreamUrl |
| Models | models, model, publisher |
| Cranking | queue, crankers, cranker |
| Builds | openRun, turn, fund, cancel, settle, closeRun, sealModel, setRoyalty, claimRoyalties |
| Transactions | submitTransaction, 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.