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.
| Account | Tag | Address | Size | How many | SDK decoder |
|---|---|---|---|---|---|
| Config | CURTACFG | PDA, seeds "config" | 128 bytes | One per program | decodeConfig |
| FeeVault | CURTAFEE | PDA, seeds "fee" | 8 bytes | One per program | decodeFeeVault |
| Model | CURTAMDL | Keypair | 4,096 bytes | One per model | decodeModel |
| ModelVault | CURTAVLT | PDA, seeds "model-vault", model address | 8 bytes | One per model | decodeModelVault |
| weight account | CURTAWGT | Keypair | 2,048 bytes plus the section | 2 to 64 per model | decodeWeights |
| Run | CURTARUN | Keypair | 256 bytes plus the working state | One per run | decodeRun |
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.
| Offset | Type | Field | SDK name | Meaning |
|---|---|---|---|---|
| 0 | 8 bytes | tag | CURTACFG | |
| 8 | address | authority | authority | The key that signs set_config. The program’s upgrade authority at init_config. |
| 40 | address | fee vault | feeVault | The FeeVault address. settle pays the protocol share here. |
| 72 | u64 | turn reward | turnReward | Lamports a cranker earns per turn, on runs opened from now on. |
| 80 | u16 | protocol share | protocolShareBps | The protocol share rate in basis points, 0 to 10,000, on runs opened from now on. |
| 82 | u8 | config bump | The bump of the Config address. | |
| 83 | u8 | fee bump | The bump of the FeeVault address. | |
| 84 | 44 bytes | zero | Zero 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.
| Offset | Type | Field | SDK name | Meaning |
|---|---|---|---|---|
| 0 | 8 bytes | tag | CURTAMDL | |
| 8 | address | publisher | publisher | The key that writes weights, seals, sets the royalty and claims. |
| 40 | address | model vault | vault | The ModelVault address. |
| 72 | u32 | dimension | config.dim | Width of the residual stream. |
| 76 | u32 | hidden dimension | config.hiddenDim | Width of the feed-forward block. |
| 80 | u32 | layers | config.layers | Number of transformer layers. |
| 84 | u32 | heads | config.heads | Attention heads per layer. |
| 88 | u32 | KV heads | config.kvHeads | Key and value heads per layer. |
| 92 | u32 | vocabulary size | config.vocabSize | Number of tokens. |
| 96 | u32 | context length | config.contextLength | Positions the model attends over. |
| 100 | u32 | section count | sections.length | Number of weight accounts, 2 to 64. |
| 104 | u8 | sealed | sealed | 1 once the weights hash is verified. Runs open on sealed models. |
| 105 | u8 | vault bump | The bump of the ModelVault address. | |
| 106 | u8 | sealing | sealing | 1 from the first seal step until the model is sealed. |
| 108 | u32 | seal section | sealSection | Index of the section the next seal step hashes. |
| 112 | u64 | seal offset | sealOffset | Bytes of that section hashed so far. |
| 120 | u64 | royalty | royalty | Lamports per generated token, on runs opened from now on. |
| 128 | 32 bytes | weights hash | expectedHash | The hash declared at create_model. |
| 160 | 32 bytes | running hash | runningHash | The hash of the bytes sealed so far. Zero before sealing begins. |
| 192 | u64 | weight bytes | weightBytes | Sum of every section’s length. |
| 200 | u64 | runs opened | runs | Count of runs opened on the model. |
| 208 | u64 | tokens settled | tokens | Generated tokens of every settled run. |
| 216 | u64 | royalties settled | earned | Lamports of royalty settled into the vault over the model’s life. |
| 224 | u64 | created slot | createdSlot | The slot of create_model. |
| 232 | u32 | name length | Bytes of the name, 1 to 64. | |
| 236 | 64 bytes | name | name | The name, UTF-8, zero after its length. |
| 304 | u32 | tokenizer cursor | Tokenizer entries validated so far during sealing. | |
| 312 | u64 | seal step | sealStep | Count of seal steps taken. The next seal_model names this value. |
| 512 | 48 bytes each | section descriptors | sections | One 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.
| Offset | Type | Field | SDK name | Meaning |
|---|---|---|---|---|
| 0 | address | weight account | address | The account that holds the section. |
| 32 | u64 | length | byteLength | The section’s length in bytes. |
| 40 | u64 | bytes written | written | Bytes 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.
| Offset | Type | Field | Meaning |
|---|---|---|---|
| 0 | 8 bytes | tag | CURTAWGT |
| 8 | address | model | The model the section belongs to. |
| 40 | u32 | section | The section’s index in the model. |
| 44 | 20 bytes | zero | Zero bytes. |
| 64 | 1,984 bytes | upload bitmap | One 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,048 | bytes | section | The 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.
| Offset | Type | Field | SDK name | Meaning |
|---|---|---|---|---|
| 0 | 8 bytes | tag | CURTARUN | |
| 8 | address | opener | opener | The wallet or program PDA that opened the run. Cancels and closes it. |
| 40 | address | model | model | The model the run computes. |
| 72 | address | config | config | The Config account the run opened under. |
| 104 | u8 | state | status | 0 running, 1 starved, 2 complete, 3 cancelled. |
| 105 | u8 | settled | settled | 1 once settle has paid the royalty and the protocol share. |
| 106 | u16 | protocol share | protocolShareBps | The rate in basis points, as the Config held it when the run opened. |
| 108 | u32 | max tokens | maxTokens | Tokens the run generates at most. |
| 112 | u32 | temperature | temperatureQ16 | The temperature times 65,536. |
| 116 | u32 | prompt length | promptLength | Bytes of the prompt. |
| 120 | u64 | seed | seed | The sampling seed. |
| 128 | u64 | turn reward | turnReward | Lamports per turn, as the Config held it when the run opened. |
| 136 | u64 | royalty | royalty | Lamports per generated token, as the model held it when the run opened. |
| 144 | u64 | budget deposited | funded | The opening budget plus every fund. |
| 152 | u64 | budget remaining | budgetRemaining | Budget deposited less every turn’s charge. |
| 160 | u64 | rewards paid | rewardsPaid | Turn rewards paid to crankers. |
| 168 | u64 | royalties accrued | royaltiesAccrued | Royalty owed to the model’s vault, paid at settle. |
| 176 | u64 | protocol share accrued | protocolAccrued | Protocol share owed to the FeeVault, paid at settle. |
| 184 | u64 | turns | turns | Turns landed. The next turn names this value. |
| 192 | u32 | generated | generated | Tokens generated so far. |
| 196 | u32 | working state length | coreLength | Bytes of working state after the header. The account’s length is 256 plus this. |
| 200 | u64 | opened slot | openedSlot | The slot of open_run. |
| 208 | u64 | last activity slot | lastSlot | The slot of the last turn that computed. |
| 216 | u64 | next turn cost | nextTurnCost | Lamports the next turn reserves. Zero once the run is complete. |
| 224 | 32 bytes | zero | Zero bytes to the end of the header. | |
| 256 | bytes | working state | core | The 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.
| Offset | Type | Field | Meaning |
|---|---|---|---|
| 0 | 8 bytes | tag | CURTAST2 |
| 28 | u32 | prompt length | Bytes of the prompt. |
| 36 | u32 | max tokens | Tokens the run generates at most. |
| 40 | u32 | temperature | The temperature times 65,536. |
| 44 | u32 | generated | Output token ids written so far. |
| 48 | u32 | text length | Output text bytes written so far. |
| 128 | bytes | prompt | The 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:
| Region | Bytes |
|---|---|
| Header | 128 |
| Prompt | promptBytes |
| Prompt token ids | (promptBytes + 2) × 4 |
| Output token ids | maxTokens × 4 |
| Output text | maxTokens × 128 |
| Four activation vectors | 4 × dimension × 8 |
| Two feed-forward vectors | 2 × hiddenDimension × 8 |
| Logits | vocabularySize × 8 |
| Attention scores | positions × 8 |
| Key cache | layers × positions × kvDimension × 4 |
| Value cache | layers × 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:
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 + stateThe 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:
| Model | Max tokens | Run account, bytes |
|---|---|---|
| stories260K | 12 | 48,304 |
| stories15M | 8 | 624,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.
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 runReading accounts
A Curta client fetches and decodes in one call, and checks that the account is owned by the program:
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 farAmounts 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:
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.