DocsReference

Accounts

Every account the curta program owns: where it lives, how big it is, and each field with its offset, type and meaning.

The six account types

Every account the curta program owns opens with an 8-byte ASCII tag that names its type. The tag identifies an account, and its size does not. Integers are little-endian and addresses are 32 raw bytes.

AccountTagAddressSizeHow manySDK decoder
ConfigCURTACFGPDA, seeds "config"128 bytesOne per programdecodeConfig
FeeVaultCURTAFEEPDA, seeds "fee"8 bytesOne per programdecodeFeeVault
ModelCURTAMDLKeypair4,096 bytesOne per modeldecodeModel
ModelVaultCURTAVLTPDA, seeds "model-vault", model address8 bytesOne per modeldecodeModelVault
weight accountCURTAWGTKeypair2,048 bytes plus the section2 to 64 per modeldecodeWeights
RunCURTARUNKeypair256 bytes plus the working stateOne per rundecodeRun

The program also derives one address that holds no account data: the event authority, seeds "__event_authority", which signs the program’s events. Program reference lists the helper that derives each address.

Config

One per program, at the address with seeds "config", 128 bytes. init_config creates it and set_config changes the turn reward and the protocol share rate. open_run copies both into the run, so a run keeps the values it opened with.

OffsetTypeFieldSDK nameMeaning
08 bytestagCURTACFG
8addressauthorityauthorityThe key that signs set_config. The program’s upgrade authority at init_config.
40addressfee vaultfeeVaultThe FeeVault address. settle pays the protocol share here.
72u64turn rewardturnRewardLamports a cranker earns per turn, on runs opened from now on.
80u16protocol shareprotocolShareBpsThe protocol share rate in basis points, 0 to 10,000, on runs opened from now on.
82u8config bumpThe bump of the Config address.
83u8fee bumpThe bump of the FeeVault address.
8444 byteszeroZero bytes to the end of the account.

FeeVault

One per program, at the address with seeds "fee", 8 bytes: the tag CURTAFEE and nothing else. Its balance is the account’s lamports. settle moves each run’s accrued protocol share into it, and the amount above its rent is the protocol share collected. CURTA says where it goes.

Model

One per model, at a keypair address, 4,096 bytes. create_model writes it, write_weights and seal_model advance its upload and sealing fields, open_run counts runs and settle adds tokens and royalties.

OffsetTypeFieldSDK nameMeaning
08 bytestagCURTAMDL
8addresspublisherpublisherThe key that writes weights, seals, sets the royalty and claims.
40addressmodel vaultvaultThe ModelVault address.
72u32dimensionconfig.dimWidth of the residual stream.
76u32hidden dimensionconfig.hiddenDimWidth of the feed-forward block.
80u32layersconfig.layersNumber of transformer layers.
84u32headsconfig.headsAttention heads per layer.
88u32KV headsconfig.kvHeadsKey and value heads per layer.
92u32vocabulary sizeconfig.vocabSizeNumber of tokens.
96u32context lengthconfig.contextLengthPositions the model attends over.
100u32section countsections.lengthNumber of weight accounts, 2 to 64.
104u8sealedsealed1 once the weights hash is verified. Runs open on sealed models.
105u8vault bumpThe bump of the ModelVault address.
106u8sealingsealing1 from the first seal step until the model is sealed.
108u32seal sectionsealSectionIndex of the section the next seal step hashes.
112u64seal offsetsealOffsetBytes of that section hashed so far.
120u64royaltyroyaltyLamports per generated token, on runs opened from now on.
12832 bytesweights hashexpectedHashThe hash declared at create_model.
16032 bytesrunning hashrunningHashThe hash of the bytes sealed so far. Zero before sealing begins.
192u64weight bytesweightBytesSum of every section’s length.
200u64runs openedrunsCount of runs opened on the model.
208u64tokens settledtokensGenerated tokens of every settled run.
216u64royalties settledearnedLamports of royalty settled into the vault over the model’s life.
224u64created slotcreatedSlotThe slot of create_model.
232u32name lengthBytes of the name, 1 to 64.
23664 bytesnamenameThe name, UTF-8, zero after its length.
304u32tokenizer cursorTokenizer entries validated so far during sealing.
312u64seal stepsealStepCount of seal steps taken. The next seal_model names this value.
51248 bytes eachsection descriptorssectionsOne per section, in order. Room for 64.

Bytes not listed are zero. The seven values from offset 72 to 100 are the model’s configuration, in the order create_model takes them.

Section descriptors

Descriptor i starts at offset 512 + 48 × i. Section zero holds the tokenizer and the lookup tables, and the sections after it hold the matrices, as Weight format describes.

OffsetTypeFieldSDK nameMeaning
0addressweight accountaddressThe account that holds the section.
32u64lengthbyteLengthThe section’s length in bytes.
40u64bytes writtenwrittenBytes of the section uploaded so far.

A section is fully uploaded when its bytes written equal its length, and sealing begins once that holds for every section.

ModelVault

One per model, at the address with seeds "model-vault" and the model’s address, 8 bytes: the tag CURTAVLT. Its balance is the account’s lamports. settle moves each run’s accrued royalty into it, and claim_royalties pays the publisher out of it, leaving the rent of its 8 bytes.

Weight accounts

A model’s weights live in 2 to 64 accounts at keypair addresses, one section each. An account is created at 2,048 bytes plus its section’s length, and a section is from 1 to 10,483,712 bytes, so the account fits Solana’s limit of 10,485,760 bytes. The first write_weights to an account writes its header.

OffsetTypeFieldMeaning
08 bytestagCURTAWGT
8addressmodelThe model the section belongs to.
40u32sectionThe section’s index in the model.
4420 byteszeroZero bytes.
641,984 bytesupload bitmapOne bit per 800-byte chunk of the section. Chunk c is bit c mod 8 of byte 64 + floor(c / 8), lowest bit first.
2,048bytessectionThe section’s bytes.

A set bit in the bitmap means that chunk is written. An interrupted upload resumes from the bitmap: read the header, send the chunks whose bits are clear. decodeWeights(data, model?, section?) checks the tag, and the model and section when given, and returns the section’s bytes. turn reads weight accounts and never writes them.

Run

One per run, at a keypair address: a 256-byte header, then the working state. open_run writes it, turn advances it, fund and cancel change its budget and state, settle sets its settled flag, and close_run removes it. Runs and turns follows those steps.

OffsetTypeFieldSDK nameMeaning
08 bytestagCURTARUN
8addressopeneropenerThe wallet or program PDA that opened the run. Cancels and closes it.
40addressmodelmodelThe model the run computes.
72addressconfigconfigThe Config account the run opened under.
104u8statestatus0 running, 1 starved, 2 complete, 3 cancelled.
105u8settledsettled1 once settle has paid the royalty and the protocol share.
106u16protocol shareprotocolShareBpsThe rate in basis points, as the Config held it when the run opened.
108u32max tokensmaxTokensTokens the run generates at most.
112u32temperaturetemperatureQ16The temperature times 65,536.
116u32prompt lengthpromptLengthBytes of the prompt.
120u64seedseedThe sampling seed.
128u64turn rewardturnRewardLamports per turn, as the Config held it when the run opened.
136u64royaltyroyaltyLamports per generated token, as the model held it when the run opened.
144u64budget depositedfundedThe opening budget plus every fund.
152u64budget remainingbudgetRemainingBudget deposited less every turn’s charge.
160u64rewards paidrewardsPaidTurn rewards paid to crankers.
168u64royalties accruedroyaltiesAccruedRoyalty owed to the model’s vault, paid at settle.
176u64protocol share accruedprotocolAccruedProtocol share owed to the FeeVault, paid at settle.
184u64turnsturnsTurns landed. The next turn names this value.
192u32generatedgeneratedTokens generated so far.
196u32working state lengthcoreLengthBytes of working state after the header. The account’s length is 256 plus this.
200u64opened slotopenedSlotThe slot of open_run.
208u64last activity slotlastSlotThe slot of the last turn that computed.
216u64next turn costnextTurnCostLamports the next turn reserves. Zero once the run is complete.
22432 byteszeroZero bytes to the end of the header.
256bytesworking statecoreThe inference core’s state: prompt, token ids, text, activations and attention cache.

The lamports of a run account are its rent, plus the budget remaining, plus the royalty and the protocol share accrued, until settle moves them out. A turn pays its reward straight out of the account, so rewards paid have already left it.

The working state’s header

The working state opens with its own 128-byte header. These are the fields a reader of the output needs, with offsets counted from byte 256 of the run account. Inference core covers the position cursors that sit between them.

OffsetTypeFieldMeaning
08 bytestagCURTAST2
28u32prompt lengthBytes of the prompt.
36u32max tokensTokens the run generates at most.
40u32temperatureThe temperature times 65,536.
44u32generatedOutput token ids written so far.
48u32text lengthOutput text bytes written so far.
128bytespromptThe prompt, UTF-8.

decodeRun reads the header and the output for you: it returns prompt, tokens (the output token ids) and text beside the header fields.

The size of a run account

A run account is created at its exact size, before open_run, and open_run checks it to the byte. The size is 256 plus the working state, and the working state’s size follows from the model’s configuration, the prompt’s byte length and maxTokens. Its regions sit in this order:

RegionBytes
Header128
PromptpromptBytes
Prompt token ids(promptBytes + 2) × 4
Output token idsmaxTokens × 4
Output textmaxTokens × 128
Four activation vectors4 × dimension × 8
Two feed-forward vectors2 × hiddenDimension × 8
LogitsvocabularySize × 8
Attention scorespositions × 8
Key cachelayers × positions × kvDimension × 4
Value cachelayers × positions × kvDimension × 4

The prompt token ids, the output token ids, the output text and the first activation vector each start on a multiple of 8. Two derived values complete the formula:

Run account size
positions   = min(contextLength, promptBytes + maxTokens + 1)
kvDimension = dimension × kvHeads / heads
align8(n)   = n rounded up to a multiple of 8

tokens  = align8(128 + promptBytes)
output  = align8(tokens + (promptBytes + 2) × 4)
text    = align8(output + maxTokens × 4)
vectors = align8(text + maxTokens × 128)

state = vectors
      + 4 × dimension × 8
      + 2 × hiddenDimension × 8
      + vocabularySize × 8
      + positions × 8
      + 2 × layers × positions × kvDimension × 4

runAccountSize = 256 + state

The prompt is at most 1,024 bytes and maxTokens runs from 1 to the context length. A token’s text is at most 128 bytes, which sizes the output text. Activations are 64-bit words and cached keys and values 32-bit words.

The reference runs on the prompt “Once upon a time”, 16 bytes, come to these sizes:

ModelMax tokensRun account, bytes
stories260K1248,304
stories15M8624,832

The SDK computes the size with the same core the program runs. client.quote returns it as runAccountSize with the rent as runAccountRentLamports, and client.openRun creates the account at that size holding the rent plus the budget.

size.ts
const quote = await curta.quote({ model, prompt: 'Once upon a time', maxTokens: 12 })

quote.runAccountSize          // bytes
quote.runAccountRentLamports  // bigint
quote.totalBudget             // bigint, lamports for the whole run

Reading accounts

A Curta client fetches and decodes in one call, and checks that the account is owned by the program:

read.ts
import { Connection } from '@solana/web3.js'
import { Curta } from '@curta/sdk'

const connection = new Connection(process.env.CURTA_RPC_URL!, 'confirmed')
const curta = new Curta({ connection })

const config = await curta.getConfig()   // ConfigAccount
const model = await curta.getModel(modelAddress)   // ModelAccount
const run = await curta.getRun(runAddress)   // RunAccount

run.status            // 'running' | 'starved' | 'complete' | 'cancelled'
run.turns             // bigint
run.budgetRemaining   // bigint, lamports
run.text              // the output so far

Amounts and counters that are u64 on chain are bigint in the decoded objects. The decoders also work on raw bytes: decodeConfig(data, address?), decodeModel(data, address?), decodeRun(data, address?), decodeWeights(data, model?, section?), decodeFeeVault(data) and decodeModelVault(data). Each throws when the tag or the length is wrong.

client.getRunHead(address) reads the first 384 bytes of a run, the header and the working state’s header, and returns the model, status, settled flag, turn count, turn reward, rewards paid, budget remaining and next turn cost. It is the cheap read between turns.

To list accounts of one type, filter the program’s accounts on the tag at offset 0:

list-runs.ts
import bs58 from 'bs58'

const runs = await connection.getProgramAccounts(curta.programId, {
  filters: [{ memcmp: { offset: 0, bytes: bs58.encode(Buffer.from('CURTARUN')) } }],
  dataSlice: { offset: 0, length: 256 },   // headers only
})

In Rust, curta_core::protocol holds the same layouts as zero-copy readers: ConfigView, ModelView, RunView, WeightView and VaultView, each with a read(data) that validates the bytes and accessors for the fields. The caller checks that the account is owned by the program. Call from a program reads a finished run this way.