Sealroom whitepaper

A Solana registry and payment rail where model owners sell per-call inference on vetted confidential-GPU operators, and the weight key is released only to attested, bonded enclaves.

Edition 1 · October 2026Download PDF

1. Introduction

1.1 What Sealroom is

Sealroom is a Solana registry and payment rail where model owners sell per-call inference on vetted confidential-GPU operators, and the weight key is released only to attested, bonded enclaves.

A model owner encrypts its weights once, under a random 256-bit data key, and publishes the ciphertext wherever it likes. The data key stays with the owner. It leaves the owner's key broker only as an HPKE ciphertext addressed to a public key that was generated inside a confidential virtual machine (CVM) and whose attestation is recorded on Solana. That CVM is a Sealroom node: one Intel TDX trust domain with one to eight NVIDIA H100 or H200 GPUs in confidential computing mode, running a measured Sealroom runtime, on a physical server that a vetted operator has registered and bonded in USDC.

Callers reach a node over TLS that terminates inside the enclave and pay for each call over x402 in USDC. Each payment lands in a lane vault, a token account the program owns for one (model, node) pair. Anyone can then call distribute on the lane, and one transaction pays the protocol fee, the serving operator's share and every recipient in the model's revenue split. The protocol fee then buys $SEALROOM, the protocol's fixed-supply, fairly launched token, on the market and burns it (14.7).

Solana holds the registry and the money: operators, platforms, nodes, attestations, runtime allowlists, models with their policies and splits, lanes, bonds and disputes. Weights, data keys, prompts, completions, raw attestation evidence and the GPUs themselves stay off chain. The chain records hashes of everything a verifier or a broker needs to compare against: the operator agreement, the platform identity, each GPU's identity, the runtime measurement, the evidence, and the enclave's public keys.

1.2 The problem it answers

A team with a valuable specialist model has three established ways to sell inference, and each leaves one party exposed.

OptionOwner's positionBuyer's position
Run its own APIPays for idle GPUs; confidential-computing GPUs are mostly sold reserved or by quoteMust send prompts to the owner
Host on an inference cloudThe host can read the weights unless confidential computing is used; protection is contractualMust trust both the host and the owner
License the weights for on-premises useGives up control of the weights permanentlyMust run its own GPUs

Sealroom serves the case of two-sided distrust. The owner will not ship weights to the buyer's infrastructure, and the buyer will not send data to the owner's. Both can accept a third machine when its hardware proves which software it runs and its operator is named and bonded, with access to the weights decided by a public policy the owner wrote.

Between many owners, many operators and many callers, that requires four things, and Sealroom provides each of them:

  1. A public, machine-checkable record of which physical machines are attested and which operator is liable for them.
  2. Key release gated on that record, by the owner's own broker.
  3. Per-call payment without an account, so that software agents can pay as easily as enterprises.
  4. Revenue splits to upstream contributors, enforced by the program at distribution time.

1.3 Who uses it

Model owners list models, each with its own key broker, runtime pins, operator requirements, price and split. The primary segment is model vendors selling to regulated buyers, such as banks, hospitals and government agencies, that refuse vendor-hosted APIs. Enterprises licensing internal models to partners and subsidiaries, and specialist fine-tuners in legal, medical and financial domains seeking agent distribution, use the same path.

Operators are vetted datacenter companies running confidential-mode GPUs. Each signs the Operator Agreement, posts a USDC bond, and registers its servers' platform identities and the device identities of their GPUs. An operator earns the Operator-role share of every lane its nodes serve.

Callers are applications, enterprises and AI agents. A caller checks a node's attestation on chain, opens TLS to the enclave, and pays per call over x402, either a fixed price (exact) or a metered amount under a ceiling (upto).

Contributors are the base-model owners, fine-tuners and data providers named in a model's split. A base model can require a derivative royalty, which every derivative model must carry as a locked split entry paid to the base owner's canonical USDC account.

Verifiers form a committee of five independent parties. Each verifier checks node evidence against Intel and NVIDIA collateral and against the registry, and signs a fixed 200-byte message; three signatures are required for an attestation to land. Sealroom runs at most two of the five verifiers.

The arbitration panel is a named multisig, distinct from the admin multisig, that rules on disputes under the Operator Agreement. It can freeze an operator, revoke every attestation the operator holds, and slash its bond to the claimant.

1.4 Design commitments

The rest of this paper elaborates a small number of fixed decisions.

  • Supply is permissioned. Operators are vetted off chain, then admitted on chain by the admin multisig with an agreement hash and a tier. Only admitted, bonded operators can register platforms and nodes.
  • Platforms are pinned. A quote counts only if its platform identity and the identities of the GPUs that produced its companion evidence were registered by that operator for that node.
  • Owners hold their keys. The default key broker is run by the owner, reads Solana directly, re-verifies the evidence itself, and publishes an audit record for every release and every denial.
  • Policy is public. Runtime allowlists, tier and bond minimums, operator allowlists, attestation expiry and revocation state are on-chain accounts that any party can read.
  • Payment needs no account. A node answers HTTP 402 with x402 payment terms; the caller signs a USDC transfer to the lane vault and retries.
  • Splits are settled at distribution. Each call is one transfer to one vault; distribute later allocates the vault across the protocol fee and the split in one atomic transaction.
  • Money is USDC. Prices, bonds, fees, deposits and payouts are USDC base units moved with checked integer arithmetic.

1.5 What this paper covers

Chapter 2 gives the system overview: actors, the lifecycle, trust boundaries, the division between chain and off-chain services, and the reasons for Solana. Chapter 3 covers Intel TDX, NVIDIA Hopper confidential computing, the device identities Sealroom pins, and the construction that binds a node's fresh keys and a recent slot hash into both pieces of hardware evidence.

Chapter 4 describes operator admission and platform pinning, Chapter 5 the attestation pipeline, and Chapter 6 key management and release. Chapter 7 documents the sealroom program: accounts, PDAs, instructions, errors, timelocks and upgrade authority. Chapters 8 to 10 cover payments over x402, settlement and distribution, and bonds, disputes and arbitration. Chapters 11 to 14 cover services and infrastructure, security design, parameters and governance, and economics. Appendix A is an instruction reference and Appendix B a glossary.

1.6 Conventions

Amounts are USDC base units (1 USDC = 1,000,000 base units). Hashes are SHA-256 and 32 bytes unless stated otherwise; runtime measurements are 48 bytes. On chain, integers are little-endian, public keys are 32 raw bytes, and every account begins with an 8-byte Anchor discriminator followed by a version: u8 and a bump: u8. A program-derived address is written with its seeds, for example ["att", node]. The symbol ‖ (written || in formulas) denotes byte concatenation. In the API, keys and signatures are base58, hashes are lowercase hex, and amounts and slots are decimal strings.

2. System overview

2.1 Actors and roles

sealroom programSolana · registry, lanes, bondsModel ownerweights, policy, key brokerVerifier committee3 of 5 signOperatorbonded, vettedArbitrationmultisig rulingsCallerapp or agent, USDCNode (CVM)key from the owner's brokercreate_modelrecord_attestationbond · registerrule_disputex402 call → lane vaultdistribute

Figure: The seven actors of Sealroom and their relationships to the sealroom program.

ActorRoleOn-chain identitySigns
Model ownerEncrypts weights, publishes the ciphertext, registers a Model with policy, price and split; runs or delegates a key brokerModel ["model", owner, id]create_model, propose_split, cancel_split, open_dispute, claim_dispute
OperatorVetted datacenter company running CC-mode GPUs; signs the Operator Agreement, posts a USDC bond, registers servers and GPUsOperator ["op", authority]deposit_bond, request_unbond, withdraw_bond, register_platform (with the admin), register_node
NodeOne CVM: an Intel TDX trust domain with 1 to 8 GPUs, running the measured Sealroom runtimeNode ["node", platform, index], Attestation ["att", node]Enclave keys only: TLS sessions and usage vouchers
Verifier committeeFive independent parties; three must check the evidence and signVerifierSet ["verifiers", epoch]Ed25519 signatures over the 200-byte attestation message; revoke_attestation
CallerApplication, enterprise or agent paying per call over x402 in USDCIts walletIts own USDC transfer to the lane vault
ContributorsBase-model owners, fine-tuners and data providers listed in a model's splitSplit entries in Model and LaneConsent to lower or remove a locked entry; claims of deferred payouts
Arbitration panelNamed multisig that rules on disputes and slashes bonds under the Operator AgreementConfig.arbitrationrule_dispute, arbitrate_operator, clear_attestation_revocation

Alongside the actors, a set of services operates the system. Each runs as its own process with its own credentials.

ServiceFunction
Admin multisigSquads multisig in Config.admin: admits, tiers and freezes operators, co-signs platforms, pauses, allows and revokes runtimes, proposes fee changes
Key brokerOwner-run: reads the chain, re-verifies evidence, wraps the data key, audits every decision
VerifiersOne process per committee member, each with one Ed25519 key and its own collateral cache
CoordinatorCollects committee signatures and submits record_attestation; holds only a fee-payer key outside the committee
FacilitatorVerifies and settles x402 payments; co-signs only as fee payer
Indexer and APIMirror accounts and events into PostgreSQL and serve the public registry API under /v1
KeeperApplies matured splits, activates lanes, reconciles expired upto sessions

2.2 The lifecycle

Listcreate_model1Onboardbond · node2Attest3 of 5 · 24 h3Releasekey → enclave4Serve and payx4025Distributefee · split6Disputeslash7

Figure: List, Onboard, Attest, Release, Serve and pay, Distribute and Dispute, with the instruction or service call at each step.

A model is served by a node through seven steps. The first two are registrations that happen once. Attestation repeats at least every 24 hours for each node. Release happens when a node loads a model. Serving, payment and distribution run continuously. Dispute is invoked when an owner has evidence against an operator.

2.2.1 List

  1. The owner generates a random 256-bit data key, encrypts the weights with AES-256-GCM in chunks, and uploads the ciphertext to any storage, public storage included.
  2. The owner signs create_model, which creates Model ["model", owner, id] (2,348 bytes) with the 32-byte manifest hash, the key broker's public key and URL (at most 128 bytes), up to 8 allowed runtime measurements of 48 bytes, min_tier (1 to 3), min_bond, an operator allowlist of up to 16 operators (empty admits any operator meeting tier and bond), a price (Exact { per_call } or Upto { input_per_million_tokens, output_per_million_tokens, max_per_call }), derivative_royalty_bps, an optional base model, and a split of 1 to 8 entries summing to exactly 10,000 bps. Each entry names a recipient token account, a beneficiary wallet, bps, locked and a role: Owner, Operator, BaseModel or Contributor.
  3. The program checks every split recipient is a canonical USDC account of its declared owner and, if the base model sets a royalty, requires a locked BaseModel entry of at least that many bps paid to the base owner.

Later split changes go through propose_split and become applicable after 7 days (604,800 seconds); a locked entry can be lowered or removed only with its beneficiary's signature.

2.2.2 Onboard

  1. Sealroom vets the operator off chain: legal entity, datacenter certifications, and the signed Operator Agreement.
  2. The admin multisig signs register_operator(agreement_hash, tier), creating Operator ["op", authority] with its bond vault and payout account.
  3. The operator signs deposit_bond. An operator is healthy while it is unfrozen and its bond is at least its tier's minimum in Config.tier_bond_minimums.
  4. For each server, the operator and the admin multisig both sign register_platform(platform_id_hash, gpu_ueid_hashes), creating Platform ["plat", operator, platform_id_hash] with up to 8 GPU UEID hashes, a global PlatformPin, and one global GpuPin per GPU.
  5. For each CVM, the operator signs register_node with an index (u16), a node key, an HTTPS endpoint of at most 128 bytes, the GPU model (H100 or H200) and the UEID hashes of the GPUs it claims; each claimed GPU must be on the platform and unclaimed.

Runtime images are admitted separately: allow_runtime, signed by the admin multisig, creates Runtime ["rt", SHA-256(measurement)], which becomes effective after a 48-hour timelock (172,800 seconds). revoke_runtime takes effect immediately.

2.2.3 Attest

  1. The node boots the measured runtime in a TDX trust domain with its GPUs in confidential computing mode.
  2. Inside the CVM it generates three key pairs: an X-Wing HPKE key pair (ML-KEM-768 + X25519), an Ed25519 TLS key and an Ed25519 voucher key.
  3. It reads a recent (slot, hash) pair from Solana's SlotHashes sysvar.
  4. It computes H = SHA-256(public keys ‖ recent slot hash), places H in TDX REPORTDATA, and uses H as the attestation nonce for every GPU (Chapter 3 gives the exact construction).
  5. It assembles a sealroom-evidence-v1 object with the node address, the slot pair, the three public keys, the TD quote and one GPU report per GPU, and submits it to the coordinator.
  6. The coordinator proposes the next revision (sequence + 1, the active verifier epoch, the operator's attestation epoch, expires_at = chain time + 86,400 − 1 seconds) to the five verifiers. Each independently verifies the TD quote against Intel collateral and the GPU evidence against NVIDIA RIMs and OCSP, checks the registry, archives the evidence by its hash, and signs the 200-byte canonical message.
  7. The coordinator checks each signature against its member key and submits one v0 transaction: an Ed25519 precompile instruction with three signatures, then record_attestation, which writes Attestation ["att", node]. The transaction is 1,056 bytes, under the 1,232-byte packet limit.

2.2.4 Release

  1. The node posts a key request to Model.key_broker_url: a request UUID, the model address, and the evidence object that was attested.
  2. The broker reads the model, node, platform, operator, attestation, runtime, verifier set and pin accounts directly from RPC in one context, and runs the eligibility rules: attestation current, unexpired and unrevoked; runtime allowed and pinned by the model; platform registered; tier and bond at the required minimums; operator unfrozen and allowlisted; TCB UpToDate; node enabled; protocol running.
  3. It checks that the evidence hash, enclave key hash and slot pair equal those in the Attestation account, re-verifies the vendor evidence itself, and repeats the registry checks.
  4. It wraps the 32-byte data key with HPKE (X-Wing, HKDF-SHA256, AES-256-GCM) to the enclave's HPKE public key, with associated data that binds the chain, program, model, node, attestation revision, evidence hash and request.
  5. It reads the chain a final time, persists the audit record, and returns the wrapped key.

Inside the CVM the key client unwraps the data key, decrypts the weights in CVM memory and loads them into GPU protected memory. Only the CVM holding the HPKE private key can unwrap it. Chapter 6 gives the full protocol.

2.2.5 Serve and pay

  1. The caller's SDK reads the node's Attestation account and the model's eligibility, recomputes the enclave key hash from the node's public keys, and checks that the node's TLS key is the committed one, so prompts terminate inside the enclave.
  2. Without a payment, the node answers HTTP 402 with x402 version 2 PAYMENT-REQUIRED terms: scheme, USDC mint, the lane vault as payTo, amount, a 120-second timeout, fee payer, invoice memo and session identifier.
  3. The caller signs its own transaction (compute-budget instructions, one TransferChecked of USDC to the payee, the memo) and retries with the PAYMENT-SIGNATURE header.
  4. The facilitator verifies the authorization without moving funds; the node runs inference and checks eligibility again; the facilitator co-signs as fee payer and submits; the node returns the completion.

With exact, the amount is per_call. With upto, the caller escrows max_per_call in the payment-channels program, and the node settles actual usage with a voucher signed by the enclave's voucher key, binding the domain sealroom:usage:v1, the chain, lane, channel, request, nonce, cumulative amount, expiry and attestation revision. Settlement and refund of the unused ceiling form one atomic transaction. The metered charge is:

charge = ceil((input_tokens * input_per_million_tokens + output_tokens * output_per_million_tokens) / 1000000) charge <= max_per_call

Because the voucher key exists only inside the attested enclave, the operator cannot raise a charge without breaking the enclave. The runtime stops serving when its own attestation lapses.

2.2.6 Distribute

Anyone can call distribute(lane). With G the vault's unreserved balance:

G = vault.amount - lane.reserved_amount fee = floor(G * protocol_fee_bps / 10000) share_i = floor((G - fee) * bps_i / 10000) share_last = share_last + ((G - fee) - sum(share_i))

The fee goes to the treasury. The Operator-role weight is paid to the payout account of the operator whose node owns the lane, so one model can serve on several operators and each receives its own share. Every other entry is paid to its declared account; a share whose account cannot receive it is reserved in the lane as a claimable entitlement. With the 500 bps launch fee, each 1,000,000 base units of revenue yields 50,000 to the treasury and 950,000 to the split.

2.2.7 Dispute

  1. A model owner with evidence of leaked weights or attestation fraud signs open_dispute against an operator, with the evidence hash and the configured deposit. The owner must hold a lane on that operator whose unconsumed protocol fees, actually paid to the treasury, cover the deposit.
  2. The arbitration multisig signs rule_dispute. A ruling for the claimant can slash up to the tracked bond principal, freeze the operator, and advance Operator.attestation_epoch, which invalidates every attestation of that operator at once. A dismissal forfeits the deposit.
  3. settle_slash funds the reserved slash, and the claimant signs claim_dispute to receive it.

An operator with an open dispute cannot withdraw its bond. arbitrate_operator lets the panel freeze or revoke an operator without a claimant.

2.3 Trust boundaries

Sealroom's components sit in six domains, each with its own material and its own enforcement.

The enclave. A node's trust domain and its GPUs in confidential computing mode hold the plaintext weights, the data key, the three enclave private keys, and every prompt and completion. The operator's host schedules the CVM and carries its encrypted traffic without access to its memory. The data key enters HPKE-wrapped to a key generated inside the enclave, and prompts enter over TLS terminated inside it. The runtime's egress allowlist permits only the key broker, the Solana RPC and the facilitator, and the runtime runs no owner-supplied code (trust_remote_code is disabled).

The physical platform. A node counts only on a server whose Intel platform identity and GPU identities are pinned to a vetted, bonded operator. The Operator Agreement, recorded as Operator.agreement_hash, covers the facility, and the USDC bond backs the operator's liability. Evidence from genuine hardware that no operator registered fails the pin checks.

The owner. The key broker holds the data key at rest and releases it only HPKE-wrapped, after its own chain reads and its own vendor verification. Each release and denial is public in /v1/key-releases with the attestation account and revision, the evidence hash, a policy hash, the chain read slot and the broker's public key.

The committee. Five verifiers each hold one Ed25519 key. The program accepts an attestation only with signatures from at least three distinct members of the active VerifierSet over a message that commits to the program, the genesis hash, the node, the epoch, the replay sequence, the attestation content and the recent slot. The coordinator that relays the signatures holds no verifier key.

The chain. The program enforces registration, pinning, timelocks, expiry, revocation, payment destinations and settlement arithmetic. The admin and arbitration multisigs are separate Squads multisigs that must differ. Runtime allowance, fee changes and dispute-configuration changes wait 48 hours, and an owner's own runtime pins and operator allowlist apply on top of the protocol's.

The caller. The caller signs only its own transfer to the lane vault, verifies the enclave's TLS key against the chain before sending data, and, under upto, accepts only vouchers signed by the attested voucher key.

Two vendor roots anchor the enclave domain: Intel, through its PCK certificate hierarchy and the Provisioning Certification Service collateral, and NVIDIA, through its device certificate hierarchy, Reference Integrity Manifests and OCSP. Each verifier keeps its own collateral cache, and running nodes continue to serve on their current attestation while collateral is refreshed.

2.4 On chain and off chain

The program stores identity, policy, attestation results and money. The table lists its accounts with their seeds and maximum allocated sizes.

AccountPDA seedsBytesHolds
Configconfig594Admin and arbitration multisigs, USDC mint, genesis hash, fee and treasury, tier bond minimums, durations, active verifier epoch, pause flag
VerifierSetverifiers, epoch264Room for 7 Ed25519 keys; the program requires 5 distinct members and threshold 3
Operatorop, authority193Agreement hash, tier, bond vault and amount, unbond deadline, frozen flag, payout account, attestation epoch
Platformplat, operator, platform id hash343Platform identity hash, up to 8 GPU UEID hashes
PlatformPinplatform_pin, platform id hash106The one platform and operator holding this identity
GpuPingpu_pin, GPU UEID hash107The platform and the one node holding this GPU
Nodenode, platform, index503Node key, endpoint, GPU model, count and UEID hashes, status
Attestationatt, node384Measurement, RIM hash, evidence hash, enclave key hash, TCB status, slot pair, expiry, method, signer bitmap, revocation
Runtimert, SHA-256(measurement)25648-byte measurement, image name and version, source commit, effective time, revocation
Modelmodel, owner, id2,348Manifest hash, key broker, policy, price, royalty, split and pending split
Lanelane, model, node719Vault, totals in, allocated and distributed, reserved amount, active split and revision
Claimclaim, lane, recipient account122A deferred payout and its beneficiary
Disputedispute, operator, sequence415Claimant, evidence hash, deposit, status, ruling and slash
ClaimantDisputesclaimant_disputes, claimant, operator78Open-dispute counter for one claimant against one operator

USDC itself sits in program-controlled token accounts: one bond vault per operator, one vault per lane, and deposit vaults for disputes.

Off chain, the system holds everything that is large, secret or both:

MaterialLocationOn-chain anchor
Encrypted weightsAny storage the owner choosesModel.manifest_hash
Data keyThe owner's key store, read by the key brokerModel.key_broker
TD quote and GPU reportsEach verifier's private evidence archive, keyed by evidence hashAttestation.evidence_hash
Enclave public keysThe evidence object; the registry API publishes their hashes and the voucher key once they reproduce the chain valueAttestation.enclave_key_hash
Operator Agreement and vetting recordsSealroom and the operatorOperator.agreement_hash, Platform.platform_id_hash
Prompts and completionsThe enclave and the callerNone
Key-release decisionsThe broker's audit log, public through /v1/key-releasesAttestation account and signed revision
Payment sessions and vouchersThe facilitator and nodeThe settled transfer to the lane vault

Raw quotes stay off chain. A TD quote carries the full PCK certificate chain in its certification data, and the GPU reports carry their own certificate chains; the chain stores one 32-byte hash of the canonical evidence object and the derived values every party compares against.

2.5 Why Solana

Agent payments run on Solana. On January 20, 2026, x402 payments on Solana passed those on Base, 518,400 against 505,000. By mid-August 2026, Solana carried about 70% of monthly x402 volume and had processed more than 35 million x402 transactions in total.

Metered billing exists natively. Coinbase announced the x402 upto scheme in April 2026. On Solana, a caller escrows a ceiling in the Solana Foundation payment-channels program (CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX, audited by Cantina in July 2026), and the server settles actual usage with an Ed25519-signed voucher. This maps directly onto per-token LLM billing; in Sealroom the voucher is signed by the enclave's voucher key.

State updates are cheap. Every node re-attests at least once a day, and lanes are distributed continuously, each transaction costing a fraction of a cent. A three-signature record_attestation uses 49,314 compute units in a 1,056-byte transaction; a distribution to a fee and eight split recipients uses 165,610 compute units in 268 bytes. Both fit in one packet, against a per-transaction limit of 1.4 million compute units.

Committee signatures verify natively. The Ed25519 precompile verifies the verifiers' signatures in the runtime. The program reads the precompile instructions through the instructions sysvar, requires every offset to point inside the verified instruction, and matches the signed message byte for byte against the message it derives itself. No signature arithmetic runs in the program.

The chain supplies its own freshness beacon. The SlotHashes sysvar exposes the hashes of the most recent 512 slots. A node binds one of them into its hardware evidence, and the program accepts the attestation only while that pair is still in the sysvar.

USDC is native. Bonds, prices, vaults and payouts are SPL Token USDC moved with TransferChecked. SVM x402 facilitators validate a fixed instruction layout around one TransferChecked to the payee; Sealroom keeps that layout by making each call a single transfer to the lane vault and moving the split into distribute, which stays atomic across all recipients.

Proof-based attestation fits the compute budget. The Attestation.method field distinguishes Committee from Zk. Groth16 verification over the alt_bn128 syscalls takes under 200,000 compute units in the groth16-solana crate and about 280,000 in sp1-solana, inside the 1.4 million limit, so a proof whose public inputs commit to the same attestation fields records into the same account.

3. Confidential computing foundations

3.1 The confidential node

A Sealroom node is a confidential virtual machine built from two hardware mechanisms. The CPU side is an Intel TDX trust domain, which isolates the VM's memory and CPU state from the host. The GPU side is one to eight NVIDIA Hopper GPUs (H100 or H200) in confidential computing mode, passed through to that trust domain. The runtime image is reproducibly built on Confidential Containers and Kata, and contains the attestation agent, the key client, vLLM, the x402 middleware for exact and upto, the voucher signer, TLS termination and the egress allowlist.

Each mechanism produces its own signed evidence. Intel's evidence is a TD quote, signed under a key that Intel certifies for one specific platform. NVIDIA's evidence is a GPU attestation report, signed by a key that NVIDIA certifies for one specific GPU. Sealroom ties both to one node, one boot and one set of enclave keys by writing the same 32-byte value into each: a hash over the enclave's public keys and a recent Solana slot hash. The following sections describe each mechanism, the identities Sealroom pins, and that binding.

3.2 Intel TDX

3.2.1 Trust domains

A trust domain (TD) is a virtual machine whose private memory and register state are protected by the processor and by the Intel TDX module. The TDX module is Intel-signed software that runs in the processor's Secure Arbitration Mode (SEAM) and mediates every transition between the host's virtual machine monitor and a TD. TD private memory is encrypted with a key specific to that TD, and the TDX module prevents the host from reading it or remapping it. The host still schedules the TD and carries its I/O, so everything a node exchanges with the outside is either public or encrypted end to end. The TDX module is itself measured: its measurement (MRSEAM), its signer's measurement (MRSIGNERSEAM) and its security version numbers (TEE_TCB_SVN) appear in every quote.

3.2.2 Measurement registers

A TD has one build-time measurement register and four runtime registers, each 48 bytes (the size of a SHA-384 digest).

  • MRTD measures the initial contents of the TD: the memory pages the host adds and measures before the TD starts, which hold the virtual firmware. It is fixed once the TD is finalized.
  • RTMR0 to RTMR3 are extended by software inside the TD as it boots. By convention RTMR0 records the firmware's configuration, RTMR1 the OS loader and kernel, RTMR2 the kernel command line and initial file system, and RTMR3 is available to the application.

An extend operation folds a new digest into a register; a register can only be extended, never set:

RTMR[i]' = SHA-384(RTMR[i] || digest)

A reproducibly built runtime image therefore produces the same register values on every compliant platform. Sealroom records the runtime as one 48-byte measurement, the value the verifier's Intel adapter reports for the quote's measured image. That value is the key of the Runtime allowlist account (["rt", SHA-256(measurement)]), the content of Attestation.runtime_measurement, and an entry in each model's allowed_runtimes.

3.2.3 TDREPORT and REPORTDATA

Software inside a TD asks the TDX module for a TDREPORT with the TDG.MR.REPORT call, passing 64 bytes of its own choice: REPORTDATA. The TDX module returns a 1,024-byte TDREPORT containing the TD's measurements, attributes and REPORTDATA, authenticated with a MAC that only the same platform can check.

REPORTDATA is the channel through which a TD commits to data it generated itself. Whatever the TD writes there is carried unchanged into the quote and covered by the quote's signature. Sealroom writes H, the binding value defined in Section 3.5, into the first 32 bytes.

3.2.4 The TD quote

A TDREPORT is local to one machine. To make it verifiable anywhere, the host passes it to the TD Quoting Enclave (QE), an Intel-signed SGX enclave on the same platform. The QE verifies the TDREPORT's MAC, then signs the report contents with its ECDSA P-256 attestation key. The attestation key is in turn certified by the platform's Provisioning Certification Key (PCK): the QE's own SGX report, whose REPORTDATA holds SHA-256 of the attestation public key and the QE authentication data, is signed with the PCK. The PCK certificate chains to the Intel SGX Root CA.

A version 4 quote is laid out as follows; version 5 adds a type-and-size descriptor before the body.

TD quote v4
  Header                       48 bytes   version, attestation key type (ECDSA P-256),
                                          TEE type (TDX), QE vendor ID, user data
  TD quote body               584 bytes   see below
  Signature data length         4 bytes
  Signature data             variable
    ECDSA signature            64 bytes   over header || body, by the attestation key
    ECDSA attestation key      64 bytes   P-256 public key
    Certification data       variable
      QE report               384 bytes   SGX report body of the Quoting Enclave
      QE report signature      64 bytes   by the PCK
      QE authentication data variable
      PCK certificate chain  variable    PEM: PCK leaf, intermediate CA, root CA

The 584-byte body carries every value a verifier compares:

OffsetBytesField
016TEE_TCB_SVN
1648MRSEAM
6448MRSIGNERSEAM
1128SEAMATTRIBUTES
1208TDATTRIBUTES
1288XFAM
13648MRTD
18448MRCONFIGID
23248MROWNER
28048MROWNERCONFIG
32848RTMR0
37648RTMR1
42448RTMR2
47248RTMR3
52064REPORTDATA

Because the PCK certificate chain travels inside the quote, the quote identifies its platform by itself: the PCK leaf certificate carries Intel's platform extensions, including the Platform Provisioning ID (PPID), the FMSPC that identifies the platform model, and the TCB levels the certificate was issued for.

3.2.5 Collateral and TCB status

A quote's signatures prove origin. Whether the platform's firmware and microcode are current is decided against collateral that Intel publishes through its Provisioning Certification Service (PCS): the TCB Info for the platform's FMSPC, the QE Identity, the PCK certificate revocation list and the root CA revocation list, each signed under the Intel SGX Root CA. Quote verification with Intel's DCAP libraries, or through Intel Trust Authority, proceeds in this order:

  1. Validate the PCK certificate chain to the Intel SGX Root CA and check both revocation lists.
  2. Verify the QE report signature with the PCK public key, check the QE report against the QE Identity, and check that the QE report's REPORTDATA commits to the attestation key and authentication data.
  3. Verify the quote signature over the header and body with the attestation key.
  4. Compare the PCK certificate's TCB components and the body's TEE_TCB_SVN against the TCB Info levels, and derive the platform's TCB status.

Sealroom carries the outcome as the TcbStatus enum (UpToDate, OutOfDate, ConfigurationNeeded, Revoked, Unknown). The verifier's claims type admits only UpToDate, and record_attestation rejects any other value with InvalidTcb.

3.3 NVIDIA Hopper confidential computing

3.3.1 Confidential computing mode

An H100 or H200 placed in confidential computing mode partitions its memory so that a protected region is reachable only by the GPU's own engines and by the confidential VM it is assigned to. The host cannot read protected memory or inspect the GPU's internal state through debug paths. The GPU driver inside the CVM and the GPU establish an authenticated session using SPDM (the DMTF Security Protocol and Data Model), and all data moving between CVM memory and the GPU passes through bounce buffers in shared memory, encrypted and authenticated with AES-GCM under that session's keys. Model weights decrypted inside the CVM are copied into GPU protected memory over that channel.

3.3.2 The GPU attestation report

Each GPU holds a device identity key pair provisioned by NVIDIA, with a certificate chain that leads to NVIDIA's device identity root. The attestation report is an SPDM measurement exchange:

  • Request. The requester, here the attestation agent in the CVM, sends a 32-byte nonce.
  • Response. The GPU returns its measurement record (indexed measurement blocks covering firmware, VBIOS and security configuration), opaque data including the driver and VBIOS versions, and a signature by the device's attestation key over the whole exchange, the requester's nonce included.

The report is accompanied by the GPU's certificate chain. The device certificate carries the GPU's unique entity identifier (UEID), and the verification result reports it.

3.3.3 RIMs and OCSP

NVIDIA publishes Reference Integrity Manifests (RIMs) for each driver and VBIOS release: signed documents listing the expected value of every measurement index. A verifier using NVIDIA's local verifier proceeds as follows:

  1. Validate the GPU certificate chain to NVIDIA's root and check the status of every certificate with NVIDIA's OCSP service.
  2. Verify the report signature with the device certificate's key.
  3. Check that the nonce in the report equals the expected nonce.
  4. Fetch the driver RIM and the VBIOS RIM matching the versions in the report from NVIDIA's RIM service, verify their signatures, and check their signing certificates by OCSP.
  5. Compare every measurement in the report with the corresponding RIM value.

The NVIDIA Remote Attestation Service (NRAS) performs the same evaluation as a hosted service, and a verifier may use it as an optional cross-check. Sealroom's verifier reduces the result to a 32-byte gpu_rim_hash identifying the reference set the GPUs were checked against; all GPUs of one node must report under the same RIM hash and with distinct UEIDs.

3.4 Device identities Sealroom pins

Genuine evidence proves that a genuine Intel platform and genuine NVIDIA GPUs produced it. Sealroom additionally requires that they are the specific machines a vetted operator registered. Two identities make that possible, both readable from the evidence itself.

Intel platform identity. The PPID is the identifier Intel uses to provision a platform's PCK, and the PCK certificate that carries it is embedded in every quote that platform produces. Sealroom stores a 32-byte hash of this identity (Platform.platform_id_hash, identity kind intel_ppid_pck in the API). During onboarding the operator supplies it from its vetting record; at attestation the verifier derives it from the quote.

GPU identity. Each GPU's UEID comes from its NVIDIA device certificate and appears in its verified evidence. Sealroom stores a 32-byte hash per GPU.

The registry holds these identities in four accounts:

AccountContentRule
Platform ["plat", operator, platform_id_hash]platform_id_hash, gpu_ueid_hashes (1 to 8, distinct), registeredCreated by register_platform with both the admin and the operator signing, while the operator is healthy
PlatformPin ["platform_pin", platform_id_hash]platform, operator, platform_id_hashGlobal: one platform identity maps to one operator and one Platform account
GpuPin ["gpu_pin", gpu_ueid_hash]platform, gpu_ueid_hash, nodeGlobal: one GPU belongs to one platform, and register_node sets node once
Node ["node", platform, index]gpu_ueid_hashes, gpu_count, gpu_modelEvery assigned GPU must be registered on the platform and unclaimed by any other node

Because the pins are global accounts derived from the identity hashes alone, a platform identity cannot be registered under two operators and a GPU cannot be assigned to two nodes. Evidence from an unregistered machine, from a registered machine attributed to the wrong operator, or from GPUs borrowed from another server fails these comparisons before any signature is produced.

3.5 Binding enclave keys and a recent slot hash

3.5.1 The enclave keys

On every boot, the runtime generates three key pairs inside the CVM. Their private halves never leave it.

KeyAlgorithmPublic sizeUse
HPKE keyX-Wing (ML-KEM-768 + X25519), suite X-Wing-draft06-HKDF-SHA256-AES256GCM1,216 bytesReceives the data key from the owner's broker
TLS keyEd2551932 bytesTerminates callers' TLS sessions inside the enclave
Voucher keyEd2551932 bytesSigns upto usage vouchers

The X-Wing public key is the ML-KEM-768 public key (1,184 bytes) followed by the X25519 public key (32 bytes).

3.5.2 The public-key transcript

The three public keys are concatenated in a fixed order. Fixed order and fixed sizes make the encoding unambiguous: no two different key sets produce the same byte string.

public-key transcript (1,280 bytes)
  offset     0   1,184 bytes   ML-KEM-768 public key   } X-Wing HPKE public key
  offset 1,184      32 bytes   X25519 public key       } (1,216 bytes)
  offset 1,216      32 bytes   TLS Ed25519 public key
  offset 1,248      32 bytes   voucher Ed25519 public key

Its hash is the enclave key hash recorded on chain:

enclave_key_hash = SHA-256(hpke_public_key || tls_public_key || voucher_public_key)

3.5.3 The recent slot hash

Solana's SlotHashes sysvar holds the most recent slot hashes as a list: an 8-byte little-endian count, followed by up to 512 entries of 40 bytes each, an 8-byte slot number and its 32-byte hash. The node reads one (recent_slot, recent_slot_hash) pair before it requests any evidence. A slot hash cannot be predicted before that slot exists, so evidence that commits to it was produced after it.

3.5.4 H in REPORTDATA and the GPU nonce

The binding value appends the slot hash to the transcript:

H = SHA-256(hpke_public_key || tls_public_key || voucher_public_key || recent_slot_hash)

The input is 1,312 bytes and H is 32 bytes. The node writes H into both pieces of evidence:

TDX REPORTDATA (64 bytes)
  bytes  0..31   H
  bytes 32..63   reserved; a verifier rejects unexpected bytes here

GPU attestation nonce (32 bytes), for every GPU in the node
  bytes  0..31   H

The complete procedure inside the CVM is:

  1. Generate the HPKE, TLS and voucher key pairs.
  2. Build the 1,280-byte transcript and compute enclave_key_hash.
  3. Read (recent_slot, recent_slot_hash) from SlotHashes.
  4. Compute H over the transcript and the slot hash.
  5. Call TDG.MR.REPORT with REPORTDATA = H in bytes 0 to 31, and have the Quoting Enclave convert the TDREPORT into a TD quote.
  6. For each assigned GPU, request an attestation report with nonce = H.
  7. Assemble the evidence object.

H links four facts. The TD quote proves that a measured runtime on a specific Intel platform produced H. Each GPU report proves that a specific genuine GPU, in a measured state, was asked to attest with H. H commits to exactly one set of enclave public keys, so the HPKE key the broker wraps to, the TLS key the caller connects to and the voucher key that signs charges all belong to that runtime. And H commits to a slot hash, so all of it happened after that slot.

HPKE public keyX25519 + ML-KEM-768TLS public keyVoucher public keyEd25519Recent slot hashSolana SlotHashesH = SHA-256(…)keys ‖ slot hashREPORTDATAGPU nonceTDX quoteMRTD · RTMRs · REPORTDATAGPU attestation reportHopper CC · nonce = Hsame HVerifiers recompute H from the enclave key hash and the slot hash named in the record; both reports must carry it.

Figure: The three enclave public keys and a recent slot hash are hashed into H, which is written into TDX REPORTDATA and used as every GPU's attestation nonce.

3.5.5 The evidence object

The node submits its evidence as a strict JSON object, format sealroom-evidence-v1:

FieldContentBound
version"sealroom-evidence-v1"Literal
nodeIdNode account address32-byte base58 key
recentSlotSlot numberu64 decimal string
recentSlotHashSlot hash64 hex characters
keys.hpkePublicKeyX-Wing public keyBase64, exactly 1,216 bytes
keys.tlsPublicKeyTLS Ed25519 public keyBase64, exactly 32 bytes
keys.voucherPublicKeyVoucher Ed25519 public key32-byte base58 key
tdxQuoteTD quoteCanonical base64, at most 700,000 characters
gpuEvidenceOne report per GPU1 to 8 canonical base64 blobs

The whole object is bounded to 1 MiB. Its hash is taken over a serialization with a fixed field order (version, nodeId, recentSlot, recentSlotHash, keys with hpkePublicKey, tlsPublicKey, voucherPublicKey, then tdxQuote, gpuEvidence):

evidence_hash = SHA-256(serialize(evidence))

evidence_hash is what the committee signs into the attestation and what verifiers use as the key of their private evidence archives. Any party holding the archived object can recompute it, recompute enclave_key_hash and H, and compare them with the chain.

3.6 What each verifier checks

Four parties examine a node's evidence or its on-chain result: each committee verifier (vendor verification, then registry checks), the program, the owner's key broker and the caller. Checks are listed with the error codes they produce.

3.6.1 Vendor verification

Each committee verifier runs Intel quote verification (Section 3.2.5) and NVIDIA GPU verification (Section 3.3.3) and reduces the result to a fixed set of claims:

ClaimSizeSource
platformIdHash32Platform identity from the PCK certificate in the quote
runtimeMeasurement48The quote's measured runtime image
reportDataHash32Bytes 0 to 31 of REPORTDATA
tcbStatusenumIntel TCB evaluation; only UpToDate is admitted
gpuUeidHashes32 eachOne per GPU report, from the device certificate
gpuNonceHashes32 eachThe nonce in each GPU report
gpuRimHash32The RIM set every GPU was checked against

3.6.2 Registry checks by the committee verifier

The verifier reads all policy accounts from RPC in one context, retrying if any changes between reads, and checks:

  1. The chain's genesis hash is the expected one and the evidence names the node read (CHAIN_IDENTITY_MISMATCH, NODE_MISMATCH).
  2. Protocol running, operator unfrozen, node enabled (PROTOCOL_PAUSED, OPERATOR_FROZEN, NODE_DISABLED).
  3. The platform is registered to this operator, its platform_id_hash equals the claim, and the global PlatformPin agrees (PLATFORM_NOT_REGISTERED, PLATFORM_ID_MISMATCH, GLOBAL_PLATFORM_PIN_MISMATCH).
  4. The UEID claims are distinct, match the node's assigned GPUs one for one, are registered on the platform, and every GpuPin names this platform and node (GPU_UEID_MISMATCH, EXCLUSIVE_GPU_PIN_MISMATCH).
  5. The runtime is allowed, unrevoked and effective (RUNTIME_NOT_ALLOWED).
  6. The slot pair is in SlotHashes (RECENT_SLOT_HASH_MISMATCH).
  7. reportDataHash and every GPU nonce equal H recomputed from the evidence (ENCLAVE_KEY_BINDING_MISMATCH).
  8. The bond meets the tier minimum (OPERATOR_BOND_BELOW_TIER_MINIMUM).
  9. No sticky revocation; next sequence; current operator epoch; active verifier epoch with this verifier a member; expiry within the TTL (ATTESTATION_REVOKED_ADMIN_CLEAR_REQUIRED, ATTESTATION_SEQUENCE_MISMATCH, VERIFIER_NOT_ACTIVE, ATTESTATION_EXPIRY_INVALID).

Only then does the verifier archive the evidence and sign a 200-byte message: the 16-byte domain sealroom.att.v0\0, the program id, the genesis hash, the node, the verifier epoch, the replay sequence, the SHA-256 of a 194-byte commitment, the recent slot and the recent slot hash. The commitment carries the platform, the runtime measurement, the GPU RIM hash, the evidence hash, the enclave key hash, the TCB status, the expiry, the method and the operator epoch. Chapter 5 gives the byte offsets.

3.6.3 The program

record_attestation is permissionless to relay and checks the result rather than the raw evidence:

CheckError
Protocol not paused; operator not frozen and bond at its tier minimumPaused, OperatorFrozen, InsufficientBond
Platform registered; node Registered; every node GPU on the platformPlatformNotRegistered, NodeDisabled, InvalidGpuSet
Runtime not revoked and effective; commitment's measurement and platform match the accountsRuntimeNotAllowed, AttestationMismatch
TCB UpToDate and method CommitteeInvalidTcb
Expiry after now, within 86,400 seconds, and not after the operator's unbond deadlineAttestationExpired, AttestationBeyondUnbond
Epoch is the active, unretired verifier epoch; operator epoch is currentInvalidVerifierSet, AttestationReplay
Not revoked; sequence is exactly the next oneAttestationRevoked, AttestationReplay
Slot pair present in SlotHashesInvalidRecentSlot
Ed25519 precompile instructions earlier in the transaction carry signatures over exactly the derived message by at least 3 distinct members of a 5-member setWrongAttestationMessage, UnknownVerifier, DuplicateVerifier, InsufficientSignatures

The resulting Attestation account (384 bytes) stores the committed values together with the slot pair, recorded_at, the verifier epoch, a signer bitmap and the SHA-256 of the signed message. Chapter 7 gives its full layout.

3.6.4 The key broker

The owner's broker repeats the decisive checks with its own reads and its own vendor verification. It requires the model to name this broker (BROKER_AUTHORITY_MISMATCH), the evidence hash, enclave key hash and slot pair to equal the attestation (EVIDENCE_CHAIN_MISMATCH), the eligibility rules and the registry checks of Section 3.6.2 to pass on its fresh vendor claims, and the runtime measurement and RIM hash to equal those recorded (VENDOR_CLAIMS_CHAIN_MISMATCH). The slot pair need not still be in SlotHashes at release time, because freshness was proved when the attestation was recorded and the record carries its own 24-hour expiry. A final chain read must match the first (REGISTRY_CHANGED_DURING_RELEASE).

3.6.5 The caller

The caller checks that the node it is about to send data to is the node the chain attested:

  1. Read Attestation ["att", node] and the model's eligibility for this node: current, unexpired, unrevoked, operator epoch current, runtime pinned by the model.
  2. Obtain the node's three public keys, recompute enclave_key_hash and compare it with the account. The registry API also publishes tlsPublicKeyHash, hpkePublicKeyHash and voucherPublicKey for each attestation whose archived evidence reproduces the on-chain value.
  3. In the TLS handshake, require the server's Ed25519 public key to equal the 32-byte TLS key in the transcript.
  4. Under upto, accept a usage voucher only if the transcript's voucher key signed it and it names the current attestation and revision.

These checks use only public data, so a caller performs them without an account with any party.

4. Operator admission and platform pinning

Sealroom admits operators by decision of its admin multisig after off-chain vetting, and it accepts a node only on hardware that an admitted operator registered. The threat model behind this design is an attacker with physical control of a confidential-computing server, who can extract the platform's attestation keys and produce quotes that verify as UpToDate. If key release accepted any genuine Intel platform, one purchased server would be enough to impersonate a node. Sealroom therefore accepts evidence only when its platform identity and every GPU UEID were registered by a vetted, bonded operator bound by the Operator Agreement.

This chapter follows the admission path in the order an operator walks it.

4.1 Off-chain vetting

4.1.1 Intake

An operator applies through the Sealroom intake at /apply with a strict, typed record:

FieldContentBound
organisation, contactName, contactEmail, websiteApplicant and contactNames 2 to 120 characters; optional HTTPS website
authorityWallet that will control the Operator account32-byte base58 key, optional
datacenterRegionsRegions of the facilities offered1 to 16 entries
gpuFleetGPU model and count1 to 8 entries; H100 or H200; count 1 to 100,000
certificationsDatacenter certifications heldAt most 16 entries
confidentialComputeReadyWhether the fleet runs TDX with GPUs in CC modeBoolean

Each submission carries an Idempotency-Key and passes a server-side Cloudflare Turnstile challenge. Its status moves from received to reviewing, then to accepted or declined.

4.1.2 What vetting establishes

Vetting is a legal and physical-security decision, so it happens off chain. Sealroom establishes:

  1. Legal entity. The applicant is an identified company, with KYC on the entity behind the operator authority.
  2. Facilities. The datacenters that will host registered servers, and their certifications.
  3. Hardware. Intel TDX with NVIDIA H100 or H200 GPUs in confidential computing mode, the v0 hardware.
  4. Machine identities. For each server, the platform identity (the Intel PPID carried by its PCK certificate) and the UEID of every GPU. The admin later co-signs these records into the registry.
  5. The Operator Agreement. The contract under which the arbitration panel rules on disputes and slashes bonds.

Vetting continues as periodic review after onboarding, charged at cost recovery.

4.1.3 What reaches the chain

Vetting produces the operator's authority wallet, the hash of its signed agreement and its tier, which register_operator records, and the platform and GPU identities of each server, which register_platform records with their global pins. Both instructions are signed by Config.admin, the Squads admin multisig, which keeps the registry's identities tied to the vetting records.

4.2 register_operator and tiers

4.2.1 The instruction

register_operator(agreement_hash: [u8; 32], tier: u8) creates the operator's account, its bond vault and its link to a payout account. The operator does not sign: admission is the admin's act. The operator's first signature on chain is its first deposit_bond.

AccountRole and constraint
adminSigner and rent payer; must equal Config.admin
configConfig ["config"]
authorityThe admitted operator key; read only, does not sign
operatorCreated: Operator ["op", authority], 193 bytes
usdc_mintMust equal Config.usdc_mint
bond_vaultUSDC associated token account whose authority is the Operator PDA; created if absent
payout_ataThe canonical USDC associated token account of authority; must already exist
token_program, associated_token_program, system_programPrograms used to create the vault

Checks, in order:

  1. The signer is Config.admin (Unauthorized).
  2. The protocol is not paused (Paused).
  3. Config.tier_bond_minimums has an entry for tier (ConfigurationUnset).

The instruction then writes the account and emits OperatorRegistered { operator, authority, tier, agreement_hash, timestamp }. Because the PDA is seeded by the authority alone, one wallet controls at most one Operator.

4.2.2 The Operator account

FieldTypeBytesValue at registration
discriminator8Anchor account discriminator
version, bumpu8, u821, PDA bump
authorityPubkey32Admitted wallet
agreement_hash[u8; 32]32Hash of the signed Operator Agreement
tieru811, 2 or 3
bond_vaultPubkey32USDC vault owned by the Operator PDA
bond_amountu6480
unbond_atOption<i64>9None
frozenbool1false
payout_ataPubkey32Authority's canonical USDC account
open_dispute_countu3240
next_dispute_sequenceu6480
registered_ati648Chain clock
attestation_epochu6480
pending_slash_amountu6480
Total193

payout_ata is where the Operator-role share of every lane on this operator's nodes is paid (Chapter 9). attestation_epoch is the operator-wide revocation counter described in Section 5.9.

4.2.3 Tiers

Config.tier_bond_minimums is a list of exactly three TierBondMinimum { tier: u8, amount: u64 } entries, for tiers 1, 2 and 3 in that order. The program refuses to initialize with any other shape (ConfigurationUnset, InvalidTier), and it refuses any register_operator or set_tier whose tier has no entry.

set_tier(tier) is signed by the admin and changes Operator.tier under the same check, emitting OperatorTierSet. A tier change takes effect at once in every rule that reads it: if the operator's bond is below the new tier's minimum, it stops being healthy (Section 4.3) and its next attestation is refused.

Models use tiers as a floor. Model.min_tier is between 1 and 3, and the key broker releases only to nodes whose operator's tier is at least that value (tier_below_min, Section 6.4).

4.3 Bonds per tier

4.3.1 The bond vault

Every operator's bond sits in its bond_vault, a USDC token account whose authority is the Operator PDA, so only the program can move it. Amounts are USDC base units (6 decimals), and every transfer is a TransferChecked.

Operator.bond_amount is the tracked principal. It rises only through deposit_bond and falls only through withdrawal or a slash. Tokens sent to the vault by a plain transfer do not increase it, and they do not count toward any minimum.

4.3.2 Health

An operator is healthy when it is not frozen and its tracked bond meets its tier's minimum:

tier_minimum = Config.tier_bond_minimums[Operator.tier].amount healthy = (Operator.frozen == false) and (Operator.bond_amount >= tier_minimum)

Health is required by register_platform, register_node and record_attestation. The failures are OperatorFrozen and InsufficientBond.

A model can demand more than the protocol does. At release time the key broker applies the larger of the two minimums:

release_minimum = max(Model.min_bond, tier_minimum) eligible_bond = Operator.bond_amount >= release_minimum

4.3.3 Deposits, unbonding and withdrawal

InstructionSignerPreconditionsEffect
deposit_bond(amount)Operator authorityNot paused; amount > 0; not frozen; no unbond requestTransfers amount from the authority's USDC account to the vault; bond_amount += amount
request_unbondOperator authoritybond_amount > 0; not frozen; open_dispute_count == 0; no request pendingunbond_at = now + 2,592,000 (30 days)
cancel_unbondOperator authorityA request is pendingunbond_at = None; no funds move
withdraw_bondOperator authorityNot frozen; open_dispute_count == 0; now >= unbond_at; vault balance at least bond_amountTransfers the full bond_amount to the authority's USDC account; bond_amount = 0; unbond_at = None

A slash ruled by arbitration subtracts from bond_amount and adds to pending_slash_amount in the same instruction, so a reserved slash is never part of the withdrawable principal. Chapter 10 describes slashing and its settlement.

4.3.4 Attestations follow the bond

An operator that has asked to leave cannot hold attestations past its departure. record_attestation requires every new attestation to expire no later than unbond_at when one is set (AttestationBeyondUnbond):

expires_at > now expires_at <= now + 86400 expires_at <= Operator.unbond_at (when unbond_at is set)

Worked timeline, in days from the unbond request:

TimeEventLongest attestation the program accepts
Day 0request_unbond; unbond_at = day 3024 hours
Day 28Daily renewal24 hours, ends day 29
Day 29, 12:00Renewal12 hours, ends exactly at day 30
Day 30 onwardAny renewalNone: no expiry can be after now and at or before unbond_at
Day 30 onwardwithdraw_bondBond returned; the operator is no longer healthy

When the bond leaves the vault, every node of the operator is already outside a valid attestation, and the broker has nothing to release against.

4.4 register_platform

4.4.1 What a platform is

A Platform is one physical server: one Intel platform identity and the GPUs installed in it. Its identity is stored as platform_id_hash, a 32-byte hash of the PPID/PCK identity that the server's TD quotes carry in their PCK certificate chain (identity kind intel_ppid_pck in the API). Each GPU is stored as gpu_ueid_hash, a 32-byte hash of the UEID from its NVIDIA device certificate. A platform holds 1 to 8 GPUs. Section 3.4 explains how the verifier derives both identities from evidence.

4.4.2 The instruction

register_platform(platform_id_hash: [u8; 32], gpu_ueid_hashes: Vec<[u8; 32]>) requires two signatures: the operator authority, which pays rent and binds the machine to its bond, and the admin multisig, which attests that the identities match the vetting record.

AccountRole and constraint
authoritySigner, rent payer; has_one on the Operator
adminSigner; must equal Config.admin
configConfig ["config"]
operatorOperator ["op", authority]
platformCreated: Platform ["plat", operator, platform_id_hash], 343 bytes
platform_pinCreated: PlatformPin ["platform_pin", platform_id_hash], 106 bytes
system_programAccount creation
remaining accountsOne writable GpuPin ["gpu_pin", gpu_ueid_hash] per GPU, in the order of gpu_ueid_hashes

Steps:

  1. The admin signed, the protocol is running and the operator is healthy (Unauthorized, Paused, OperatorFrozen, InsufficientBond).
  2. gpu_ueid_hashes has 1 to 8 distinct entries and exactly that many remaining accounts are supplied (InvalidGpuSet).
  3. Each GpuPin address equals its derived PDA, is writable, holds no data and is owned by the System Program (GpuAlreadyClaimed). The program tops up rent if the address already holds lamports, allocates 107 bytes, assigns the account to itself and writes { platform, gpu_ueid_hash, node: None }.
  4. The PlatformPin is created; creation fails if the address is in use.
  5. The Platform is written with registered = true, and PlatformRegistered is emitted.

The steps run in one transaction, so a platform is registered with all of its pins or not at all.

4.4.3 Layouts

Platform (343 bytes):

FieldTypeBytes
discriminator, version, bump10
operatorPubkey32
platform_id_hash[u8; 32]32
gpu_ueid_hashesVec<[u8; 32]>, at most 84 + 256
registeredbool1
registered_ati648

PlatformPin (106 bytes) and GpuPin (107 bytes):

AccountSeedsFields
PlatformPin["platform_pin", platform_id_hash]platform: Pubkey, operator: Pubkey, platform_id_hash: [u8; 32]
GpuPin["gpu_pin", gpu_ueid_hash]platform: Pubkey, gpu_ueid_hash: [u8; 32], node: Option<Pubkey>

4.5 Global pins: one machine, one registration

The Platform PDA includes the operator in its seeds, so by itself it would let two operators each create a Platform for the same server. The pins close that gap. Their seeds contain only the identity hash, so each pin address exists once across the whole program:

  • PlatformPin ["platform_pin", platform_id_hash] maps one platform identity to one operator and one Platform account.
  • GpuPin ["gpu_pin", gpu_ueid_hash] maps one GPU to one platform, and after register_node to one node.
AttemptWhat stops itResult
The same operator registers the same server twicePlatform and PlatformPin addresses are in useAccount creation fails
A second operator registers a server already pinnedPlatformPin address is in useAccount creation fails
A new platform lists a GPU pinned to another platformGpuPin holds data and is program-ownedGpuAlreadyClaimed
A third party pre-funds a future GpuPin address to block itThe program tops up rent, then allocates and assignsRegistration proceeds
Two nodes claim the same GPUGpuPin.node is already setGpuAlreadyClaimed
A node claims a GPU registered on a different platformHash absent from this platform's list, or GpuPin.platform differsInvalidGpuSet or GpuAlreadyClaimed

Pins have no release or migration instruction. A platform identity or GPU, once registered, stays bound to the operator and platform that first registered it.

The verifier and the broker read the pins again at every attestation and every release. They require the PlatformPin to name this platform, the node's operator and the identity in the quote (GLOBAL_PLATFORM_PIN_MISMATCH); the GPU evidence to carry exactly the node's assigned UEIDs (GPU_UEID_MISMATCH); and every GpuPin to name this platform and this node (EXCLUSIVE_GPU_PIN_MISMATCH). Evidence borrowed from a GPU outside the node fails the UEID check. The GPU nonce, bound to H (Section 3.5.4), proves the evidence is fresh; the UEID check proves which GPUs produced it.

4.6 register_node

4.6.1 The instruction

A node is one CVM: a TDX trust domain with 1 to 8 of its platform's GPUs, running the measured Sealroom runtime. The operator registers it alone; the admin's co-signature was given at the platform level.

register_node(args: RegisterNodeArgs):

ArgumentTypeBound
indexu160 to 65,535; part of the PDA seed
node_keyPubkeyThe node's 32-byte public key
endpointStringHTTPS URL, at most 128 bytes
gpu_modelenumH100 or H200
gpu_ueid_hashesVec<[u8; 32]>1 to 8 distinct hashes, each registered on the platform
AccountRole and constraint
authoritySigner, rent payer
configConfig ["config"]
operatorOperator ["op", authority], has_one authority
platformPlatform ["plat", operator, platform_id_hash], has_one operator
nodeCreated: Node ["node", platform, index] (index as u16 little-endian), 503 bytes
system_programAccount creation
remaining accountsOne writable GpuPin per hash in gpu_ueid_hashes, same order

Steps:

  1. The protocol is running, the operator is healthy and the platform is registered (Paused, OperatorFrozen, InsufficientBond, PlatformNotRegistered).
  2. The endpoint passes the URL rules below (InvalidEndpoint).
  3. gpu_ueid_hashes has 1 to 8 distinct entries, all in Platform.gpu_ueid_hashes, with one GpuPin supplied per entry (InvalidGpuSet).
  4. Each GpuPin is at its derived address, program-owned, with a matching bump and hash (InvalidGpuSet), names this platform and has no node (GpuAlreadyClaimed). The program sets GpuPin.node to the new node.
  5. The Node is written with gpu_count equal to the number of hashes and status Registered, and NodeRegistered is emitted.

Because each GPU can be claimed by one node and every node has at least one GPU, a platform's GPUs are partitioned among its nodes: an 8-GPU server can be one 8-GPU node, eight 1-GPU nodes, or any split in between.

4.6.2 Endpoint rules

The program accepts only an unambiguous HTTPS URL: ASCII, at most 128 bytes, no whitespace, control characters, backslash or user@ component; an optional port from 1 to 65,535; host labels of 1 to 63 letters, digits or hyphens with no xn-- labels; and numeric hosts only as canonical four-part IPv4 or bracketed IPv6. The same rules apply to Model.key_broker_url.

4.6.3 The Node account

FieldTypeBytes
discriminator, version, bump10
platformPubkey32
operatorPubkey32
indexu162
node_keyPubkey32
endpointString, at most 128 bytes4 + 128
gpu_modelenum (H100 = 0, H200 = 1)1
gpu_countu81
gpu_ueid_hashesVec<[u8; 32]>, at most 84 + 256
statusenum (Registered = 0, Disabled = 1)1
Total503

A Disabled node cannot be attested (NodeDisabled) and fails the broker's node_disabled check. The registry API reports node status as registered, serving, offline or disabled; serving means the node is healthy and its current attestation matches its evidence.

4.7 Operator status and freezing

4.7.1 Status

The registry API derives one status for each operator from its account, in this order:

ConditionStatus
frozen == truefrozen
unbond_at is setunbonding
bond_amount == 0unbonded
Otherwisebonded

The API also publishes tier, agreementHash, bondAmount, the vault balance, attestationEpoch, openDisputeCount, pendingSlashAmount and the operator's platforms and nodes.

4.7.2 Who can freeze

PathSignerEffect
freeze_operator(frozen: bool)Config.adminSets or clears Operator.frozen; emits OperatorFrozen
rule_dispute(..., freeze = true, ...)Config.arbitrationSets frozen, as part of a ruling for the claimant
arbitrate_operator(..., freeze = true, ...)Config.arbitrationSets frozen as a security action, with or without a slash

None of these paths is blocked by the protocol pause, and none makes a token transfer, so a frozen USDC account cannot prevent a freeze. Clearing a freeze is an admin decision through freeze_operator(false).

4.7.3 What freezing stops

OperationResult for a frozen operator
deposit_bond, request_unbond, withdraw_bondOperatorFrozen: the bond stays in the vault for arbitration
register_platform, register_nodeOperatorFrozen
record_attestationOperatorFrozen: no new or renewed attestation
Verifier signingOPERATOR_FROZEN: no committee signature
Key brokeroperator_frozen and OPERATOR_FROZEN: no key release
Node servingThe node's eligibility check before inference and again before settlement fails, so no call is served or charged

A freeze stops new key releases and service at the next read. To invalidate every attestation the operator holds at once, arbitration also advances Operator.attestation_epoch (Section 5.9.4), which makes every attestation signed under the previous epoch revoked for every consumer.

5. The attestation pipeline

An Attestation account states that a node, on a registered platform with registered GPUs, booted an allowed runtime measurement and holds a specific set of enclave keys, and that at least three of the five committee verifiers checked its vendor evidence within the last 24 hours. This chapter follows that statement from evidence to transaction, then through revocation, renewal and the ZK path that follows the committee.

Nodequote + GPU evidenceVerifier 1signsVerifier 2signsVerifier 3abstainsVerifier 4signsVerifier 5abstainsCoordinator3 signaturesAttestation24 htx: Ed25519 × 3 · record_attestationv0 + lookup table

Figure: The node submits bound evidence to the coordinator, five independent verifiers check it and sign one 200-byte message, and three signatures land on chain through the Ed25519 precompile and record_attestation.

5.1 The pipeline end to end

  1. Bind. The runtime generates its enclave keys, binds them and a recent slot hash into H, and obtains a TD quote and one GPU report per GPU carrying H (Section 3.5).
  2. Submit. The node sends its sealroom-evidence-v1 object to the attestation coordinator.
  3. Propose. The coordinator verifies the evidence, reads the registry and fixes the next sequence, the verifier epoch, the operator epoch and the expiry.
  4. Sign. All five verifiers check the evidence independently; each archives it and signs the 200-byte canonical message.
  5. Record. The coordinator keeps three valid signatures and submits an Ed25519 precompile instruction followed by record_attestation, which re-derives the message, checks the signatures through the instructions sysvar and writes Attestation ["att", node].
  6. Publish. The indexer mirrors AttestationRecorded as a new revision for the API, callers and key brokers.

5.2 The verifier set

5.2.1 Composition

The verifier committee has five members. They are independent parties; Sealroom operates at most two of the five. Each verifier runs as its own service with exactly one Ed25519 signing key, its own credential, its own host and secret store, and its own cache of Intel and NVIDIA collateral. A quorum is three distinct members signing the same message, so the committee produces attestations with any two members unavailable, and no two members can produce one on their own.

The coordinator is a separate service that holds no verifier key. Its only key is the fee payer that submits transactions, and it refuses to run if that key is a committee member. It cannot create a committee signature; it can only collect and relay them.

5.2.2 The VerifierSet account

FieldTypeBytes
discriminator, version, bump10
epochu648
membersVec<Pubkey>, at most 74 + 224
thresholdu81
activated_ati648
retired_atOption<i64>9
Total264

The PDA is ["verifiers", epoch] with the epoch as u64 little-endian, so each committee membership lives in its own account. Config.active_verifier_epoch names the set whose signatures count. The account allocates room for seven members; the v0 committee is exactly five distinct, non-default keys with threshold 3. The program enforces that shape at initialization (InvalidVerifierSet) and again inside every record_attestation, before it counts a single signature. A set with retired_at set is never accepted.

A member's position in members is its bit in the attestation's signer_bitmap: member 0 is bit 0, member 4 is bit 4. Bitmap 0b00000111 (7) means members 0, 1 and 2 signed.

5.3 What each verifier checks

A verifier exposes one signing route that accepts only server requests carrying its own credential. The request is an attestation proposal: the evidence object plus the proposed sequence, verifier epoch, operatorEpoch and expiresAt. The verifier works through three groups of checks; Section 3.6 lists each with its error code.

  1. Vendor verification. The TD quote is verified against Intel collateral (DCAP with Intel PCS, or Intel Trust Authority), admitting only TCB status UpToDate. Each GPU report is verified against NVIDIA's reference integrity manifests and OCSP with the NVIDIA local verifier, with NVIDIA's remote attestation service as an optional cross-check.
  2. Registry checks. In one RPC read at confirmed commitment, the verifier loads Config, Node, Platform, Operator, Attestation, the active VerifierSet, the claimed Runtime, the PlatformPin, every GpuPin and the Clock and SlotHashes sysvars. It checks the genesis hash, pause and freeze flags, platform identity and pins, GPU set, runtime activation, slot pair, H binding and tier bond. "Now" is the chain's Clock.
  3. Proposal checks. No sticky revocation is in place; sequence is the current sequence plus one; operatorEpoch equals Operator.attestation_epoch; epoch is active and includes this verifier; expiresAt is after now and at most 86,400 seconds ahead.

When every check passes, the verifier archives the evidence privately under evidence_hash with its byte length, format version and capture time; builds the 200-byte canonical message (Section 5.4) from its own registry read and vendor claims; and returns { publicKey, signature, messageHash, evidenceHash }. Raw quotes run to several kilobytes and include the PCK certificate chain, so they stay off chain; the on-chain hash and the archive let an auditor reproduce the verification. A verifier signs only a message it computed itself.

5.4 The canonical attestation message

5.4.1 Message layout

Every committee member signs the same 200 bytes. The program builds the identical bytes from its own accounts and the instruction arguments, and the client SDK encodes them the same way.

OffsetBytesFieldSource in record_attestation
016Domain sealroom.att.v0 followed by one NUL byteConstant
1632Program idcrate::ID
4832Genesis hashConfig.genesis_hash
8032Node PDAnode account address
1128Verifier epoch, u64 LEargs.epoch
1208Replay sequence, u64 LEargs.sequence
12832SHA-256 of the Borsh-encoded commitmentargs.commitment
1608Recent slot, u64 LEargs.recent_slot
16832Recent slot hashargs.recent_slot_hash
200

5.4.2 Commitment layout

The commitment is the AttestationCommitment struct, Borsh-encoded in field order. It is always 194 bytes.

OffsetBytesFieldEncoding
032platformPlatform PDA
3248runtime_measurementRaw measurement
8032gpu_rim_hashRaw hash
11232evidence_hashSHA-256 of the evidence serialization
14432enclave_key_hashSHA-256 of the 1,280-byte key transcript
1761tcb_statusEnum tag: UpToDate 0, OutOfDate 1, ConfigurationNeeded 2, Revoked 3, Unknown 4
1778expires_ati64 LE, Unix seconds
1851methodEnum tag: Committee 0, Zk 1
1868operator_epochu64 LE
194
commitment_hash = SHA-256(borsh(AttestationCommitment)) (194 bytes in, 32 out) message = domain || program_id || genesis_hash || node || epoch || sequence || commitment_hash || recent_slot || recent_slot_hash canonical_message_hash = SHA-256(message) (stored in the Attestation)

5.4.3 What each field binds

FieldA signature over it cannot be reused for
DomainAny other message type signed with a verifier key
Program idAnother deployment of the program
Genesis hashAnother cluster
Node PDAAnother node, including another node on the same platform
Verifier epochA committee other than the active one
Replay sequenceAny earlier or later revision of this node's attestation
Commitment hashA different platform, runtime, GPU reference set, evidence, key set, TCB status, expiry, method or operator epoch
Recent slot and hashA recording after that slot has left SlotHashes

5.5 Collecting signatures and the Ed25519 packet

5.5.1 The coordinator

The coordinator runs the vendor verification and registry checks itself before it asks anyone to sign, then fixes the proposal:

sequence = Attestation.sequence + 1 (1 when the node has no Attestation yet) epoch = Config.active_verifier_epoch operator_epoch = Operator.attestation_epoch expires_at = clock_now + 86400 - 1

It sends the proposal to all five verifiers in parallel, each with a 30-second timeout. It accepts a response only if the public key equals the key configured for that endpoint and is a member of the active set, the evidenceHash matches, and the Ed25519 signature verifies over the coordinator's own copy of the message. Valid responses are deduplicated by public key; with fewer than three the coordinator records nothing (VERIFIER_QUORUM_UNAVAILABLE), and with more it uses exactly three.

5.5.2 Precompile instruction layout

The three signatures travel in one instruction to Solana's native Ed25519 program (Ed25519SigVerify111111111111111111111111111), placed before record_attestation in the same transaction. The message appears once and all three signatures reference it.

Ed25519 precompile instruction data, n signatures
  offset         size     field
  0              1        n (1 to 7)
  1              1        padding, must be 0
  2              14 x n   one offsets record per signature, seven u16 LE values:
                            signature_offset
                            signature_instruction_index   = 0xFFFF
                            public_key_offset
                            public_key_instruction_index  = 0xFFFF
                            message_data_offset
                            message_data_size             = 200
                            message_instruction_index     = 0xFFFF
  2 + 14n        96 x n   per signer: public key (32), then signature (64)
  2 + 110n       200      canonical message, shared by all signatures

For the three-signature packet:

data_bytes = 2 + 3 * 14 + 3 * 96 + 200 = 532

5.5.3 How the program reads the packet

The Ed25519 program verifies every signature in the packet before record_attestation runs; a single invalid signature fails the whole transaction. record_attestation then reads the instructions sysvar to learn what was verified. For every instruction before its own index whose program id is the Ed25519 program, it requires:

  1. The instruction has no accounts, n is between 1 and 7, the padding byte is 0, and the data holds all n offsets records (InvalidSignatureInstruction).
  2. Every instruction-index field is 0xFFFF, meaning "this instruction". A packet therefore cannot point the precompile at bytes in another instruction while the program inspects different ones (InvalidSignatureInstruction).
  3. The 64-byte signature and 32-byte public key lie inside the data (InvalidSignatureInstruction).
  4. The message size is exactly 200 and the message bytes equal the message the program derived (WrongAttestationMessage).
  5. The public key is a member of the active VerifierSet (UnknownVerifier).
  6. That member's bit is not already set in the bitmap, across all Ed25519 instructions in the transaction (DuplicateVerifier).

After the scan, the number of set bits must reach the set's threshold of 3 (InsufficientSignatures). The resulting bitmap is stored as Attestation.signer_bitmap.

5.6 record_attestation

5.6.1 Signers, accounts and arguments

The only signer is the payer. Anyone can relay a committee packet; the authority of the record comes from the three committee signatures, which the program checks itself.

#AccountConstraint
1payerSigner, writable; pays rent on first creation
2configConfig ["config"]
3verifier_setVerifierSet ["verifiers", args.epoch]
4operatorOperator ["op", operator.authority]
5platformPlatform ["plat", operator, platform_id_hash], has_one operator
6nodeNode ["node", platform, index], has_one platform and operator
7runtimeRuntime ["rt", SHA-256(measurement)]
8attestationAttestation ["att", node], created on first use, writable
9instructionsInstructions sysvar, exact address
10slot_hashesSlotHashes sysvar, exact address
11system_programAccount creation
12event_authoritySelf-CPI event authority
13programThe sealroom program

RecordAttestationArgs:

ArgumentTypeBytes
commitmentAttestationCommitment194
epochu648
sequenceu648
recent_slotu648
recent_slot_hash[u8; 32]32

With the 8-byte instruction discriminator, the instruction data is 258 bytes.

5.6.2 Checks, in program order

  1. The protocol is not paused (Paused).
  2. The operator is healthy: not frozen, bond at its tier minimum (OperatorFrozen, InsufficientBond).
  3. The platform is registered (PlatformNotRegistered).
  4. The node's status is Registered (NodeDisabled).
  5. Every GPU hash on the node is on the platform (InvalidGpuSet).
  6. The runtime is not revoked and its 48-hour timelock has passed (RuntimeNotAllowed).
  7. The commitment's measurement equals Runtime.measurement and its platform equals the platform account (AttestationMismatch).
  8. The commitment's TCB status is UpToDate and its method is Committee (InvalidTcb).
  9. now < expires_at <= now + 86,400, and expires_at <= unbond_at when an unbond is pending (AttestationExpired, AttestationBeyondUnbond).
  10. args.epoch equals Config.active_verifier_epoch and the set's own epoch, and the set is not retired (InvalidVerifierSet).
  11. The commitment's operator_epoch equals Operator.attestation_epoch (AttestationReplay).
  12. The attestation is not revoked (AttestationRevoked), and args.sequence is 1 for a new account or the stored sequence plus 1 (AttestationReplay).
  13. (recent_slot, recent_slot_hash) appears as a pair in SlotHashes, which holds at most 512 entries of 40 bytes after an 8-byte count (InvalidRecentSlot).
  14. The program derives the 200-byte message and verifies the committee packet as in Section 5.5.3.

It then overwrites the node's single Attestation account and emits AttestationRecorded with every stored field, the signer bitmap and the canonical message hash.

5.6.3 The Attestation account

Offsets are from the start of the account. The three optional fields at the end occupy up to 75 bytes; the allocation reserves their maximum.

OffsetFieldTypeBytesWritten as
0discriminator8
8versionu811
9bumpu81PDA bump
10nodePubkey32Node PDA
42sequenceu648args.sequence
50signed_sequenceu648args.sequence
58operator_epochu648From the commitment
66runtime_measurement[u8; 48]48From the commitment
114gpu_rim_hash[u8; 32]32From the commitment
146evidence_hash[u8; 32]32From the commitment
178enclave_key_hash[u8; 32]32From the commitment
210tcb_statusenum1UpToDate
211slotu648args.recent_slot
219recent_slot_hash[u8; 32]32args.recent_slot_hash
251expires_ati648From the commitment
259recorded_ati648Chain clock
267methodenum1Committee
268verifier_epochu648args.epoch
276signer_bitmapu81Bits of the three signers
277canonical_message_hash[u8; 32]32SHA-256 of the 200-byte message
309revoked_atOption<i64>up to 9None
revoked_byOption<Pubkey>up to 33None
revoke_reason_hashOption<[u8; 32]>up to 33None
Allocation384

There is one Attestation per node. A renewal overwrites it; the history of every signed revision is kept by the indexer from the AttestationRecorded events, keyed by account address and sequence, together with the transaction signature, the signing verifiers and a link to the archived evidence.

5.7 Measured cost

A three-signature record_attestation is sent as a v0 transaction with an address lookup table:

FormBytesCompute units
v0 transaction with lookup table1,05649,314
Equivalent legacy transaction1,361Over the 1,232-byte packet limit

The two instructions carry 532 bytes of Ed25519 packet and 258 bytes of record_attestation data. The legacy form spends 32 bytes on each of the 13 account addresses; the lookup table replaces them with one-byte indexes. The coordinator creates the table once, reuses it, and refuses to send any attestation packet over 1,232 bytes.

49,314 compute units is 3.5% of the 1.4 million units a transaction may use. With renewal at least every 24 hours, the committee path costs one such transaction per node per day.

5.8 Sequence, revision and expiry

sequence is the replay barrier: every committee record and every revocation consumes the next value. signed_sequence is the revision number of the latest committee-signed record, and it is the value that key-release logs, usage vouchers and the API cite. operator_epoch is fixed at signing; expires_at is at most 86,400 seconds after recording.

Transitionsequencesigned_sequencerevoked_atexpires_at
First record11NoneAt most now + 86,400
Renewal+1= new sequenceNoneNew expiry
revoke_attestation+1UnchangedNowUnchanged
clear_attestation_revocationUnchangedUnchangedNoneNow (expired)
First record after a clear+1= new sequenceNoneNew expiry

The registry API reports each attestation as valid, expiring_soon (3,600 seconds or less remain), expired or revoked. An attestation counts as revoked when revoked_at is set or when its operator_epoch is lower than the operator's current attestation_epoch. Every consumer compares expires_at with the chain's Clock.

5.9 Revocation

5.9.1 revoke_attestation

revoke_attestation(reason_hash: [u8; 32]) takes four accounts: the signing authority, config, a verifier_set and the writable attestation. The signer must be Config.admin or a member of the supplied VerifierSet when that set is the one at Config.active_verifier_epoch; anyone else fails with Unauthorized.

If the attestation is not already revoked, the instruction increments sequence, sets revoked_at to now, revoked_by to the signer and revoke_reason_hash to the supplied hash, and emits AttestationRevoked. Revoking an already revoked attestation succeeds and changes nothing. The instruction is not gated by the protocol pause.

5.9.2 Immediate effect

Revocation takes effect at the next read of the account by each consumer:

ConsumerEffect
Key brokerattestation_revoked: no release, including for a request already in progress whose final chain read sees the revocation
NodeIts eligibility check before inference fails, so it serves no new call; its check before settlement fails, so it charges for none
Caller SDKSees the attestation as revoked and does not send data
VerifiersRefuse to sign any proposal for the node (ATTESTATION_REVOKED_ADMIN_CLEAR_REQUIRED)
Programrecord_attestation fails with AttestationRevoked

5.9.3 Held packets and the sticky latch

A revocation is sticky. The node cannot clear it by renewing, and a renewal packet that was already signed when the revocation landed can never be recorded:

  1. The node's attestation is at sequence 10. The coordinator holds a fully signed renewal for sequence 11 that it has not yet submitted.
  2. A verifier revokes. The revocation consumes sequence 11: sequence = 11, revoked_at set.
  3. The held packet for 11 is submitted. It fails with AttestationRevoked.
  4. The admin or arbitration multisig calls clear_attestation_revocation. It clears revoked_at, revoked_by and revoke_reason_hash and sets expires_at to now, so the account is valid for no time at all. sequence stays 11.
  5. The held packet for 11 is submitted again. The program now expects sequence 12 and fails with AttestationReplay.
  6. The node must produce fresh evidence and the committee must sign a new proposal at sequence 12. That record becomes signed_sequence = 12.

clear_attestation_revocation is signed by Config.admin or Config.arbitration and requires the attestation to be revoked (AttestationNotRevoked). Clearing never revalidates anything: it only permits a new committee record.

5.9.4 Operator-wide revocation through the epoch

Arbitration can revoke every attestation an operator holds with one field. rule_dispute and arbitrate_operator accept revoke_attestations: bool; when true, the ruling increments Operator.attestation_epoch.

Every attestation carries the epoch it was signed under, inside its signed commitment. After the increment, each of the operator's attestations has an operator_epoch below the operator's: the broker treats it as revoked (attestation_revoked), the indexer marks it revoked at the ruling's time, and the node's own eligibility check fails. Any held packet signed under the old epoch fails record_attestation with AttestationReplay. New attestations need fresh evidence signed under the new epoch, which a frozen operator cannot obtain. The Attestation accounts themselves are not touched, so the revocation costs one instruction however many nodes the operator runs.

5.9.5 Runtime revocation

revoke_runtime marks a Runtime revoked immediately. From then on record_attestation refuses that measurement (RuntimeNotAllowed), verifiers refuse to sign it (RUNTIME_NOT_ALLOWED), and the broker's eligibility rule requires the runtime to be globally allowed as well as pinned by the model (runtime_not_allowed). Existing attestations under a revoked runtime stop receiving keys at once.

5.10 Renewal

An attestation lives at most 86,400 seconds; the coordinator proposes chain now + 86,399. A node renews by running the pipeline again with a new (slot, hash) pair. If it keeps its key pairs, enclave_key_hash is unchanged while H and evidence_hash change with the slot hash. The committee signs at sequence + 1 under the operator's current epoch, and record_attestation overwrites Attestation ["att", node] with signed_sequence at the new revision.

The API flags an attestation as expiring_soon in its last hour. A renewal passes every rule a first attestation does, so the operator must still be healthy, the runtime still allowed and the pins intact each day. The node's key release is tied to the revision it was issued under (Section 6.6): after a renewal it requests a release for the new revision before serving again, and the runtime stops serving when its own attestation lapses.

5.11 The ZK verification path

The committee is the v0 verification method. The account layout already carries its successor: AttestationMethod has two variants, Committee and Zk, and the method tag is part of the signed commitment. record_attestation accepts only Committee (InvalidTcb for any other method).

The next method, record_attestation_zk, replaces the three committee signatures with one Groth16 proof:

PropertyCommittee (v0)ZK
Evidence checkFive verifiers run Intel and NVIDIA verification off chain; three signA zkVM program (SP1 or RISC Zero) runs the same verification; its execution is wrapped in a Groth16 proof
On-chain inputEd25519 packet with three signatures over the 200-byte messageOne proof of about 256 bytes, with public inputs that commit to the same fields as the commitment
On-chain verificationEd25519 precompile; 49,314 compute units for the transactionGroth16 over the alt_bn128 syscalls: under 200,000 compute units with the groth16-solana crate, about 280,000 per sp1-solana's README, and a measured mean of about 246,906 per transaction in one study
Proving timeSecondsMinutes; Automata reported about five minutes to prove a DCAP verification on Bonsai
TrustThree of five verifiers honestSoundness of the zkVM and the guest program
Resulting accountAttestation with method = CommitteeThe same Attestation with method = Zk

The proof fits within the 1.4 million compute units of one transaction. Because the public inputs commit to the same fields, every check downstream of the account (the broker's eligibility rules, the caller's key check, revocation by epoch and sequence) reads a ZK attestation exactly as it reads a committee one. The ZK path removes trust in the committee's code; platform pinning and operator vetting remain the basis of hardware trust under both methods.

6. Key management and release

The model owner keeps its weights encrypted everywhere outside an attested enclave. One 256-bit data key per model opens them, and that key leaves the owner's key broker only as an HPKE ciphertext addressed to a specific enclave's key, after the broker has checked the chain, the evidence and the model's policy for itself. Every decision, release or denial, is logged with the attestation it relied on.

6.1 Weight packaging

The owner packages a model once:

  1. Generate a random 256-bit data key for the model.
  2. Encrypt the weights with AES-256-GCM in chunks. Each chunk is authenticated, so a modified chunk fails to decrypt.
  3. Write a manifest describing the encrypted package and compute its 32-byte hash.
  4. Upload the ciphertext and the manifest to any storage, public storage included; the ciphertext is unreadable without the data key.
  5. Place the data key in the broker's key store (Section 6.2).
  6. Sign create_model with the manifest hash, the broker's identity and the release policy.

The manifest hash pins exactly which ciphertext the model refers to. A node fetches the manifest, checks it against Model.manifest_hash, and decrypts chunk by chunk with the released data key.

The Model account fields that govern release:

FieldTypeBoundRole in release
ownerPubkeyPDA seed; signs model changes
idu64PDA seed: Model ["model", owner, id]
manifest_hash[u8; 32]Identifies the encrypted package
key_brokerPubkeyNot the default keyThe only broker that may release this model's key
key_broker_urlStringHTTPS, at most 128 bytes, same rules as node endpointsWhere nodes send key requests
allowed_runtimesVec<[u8; 48]>1 to 8, distinctMeasurements the owner accepts
min_tieru81 to 3Lowest operator tier
min_bondu64USDC base unitsLowest operator bond; the larger of this and the tier minimum applies
operator_allowlistVec<Pubkey>At most 16, distinct, non-defaultEmpty: any admitted operator; otherwise only listed operators
derivative_royalty_bpsu160 to 10,000Royalty this model requires from derivatives (Section 6.8)
base_modelOption<Pubkey>The base this model derives from

create_model validates every bound (BoundsExceeded, InvalidBps, InvalidEndpoint). The full Model account, with price and split, is 2,348 bytes; Chapter 7 gives its layout.

6.2 The owner-run key broker

The key broker is an open-source service that the owner runs, or delegates to a party it chooses. Sealroom does not hold model keys.

AspectBehavior
IdentityAn Ed25519 public key recorded as Model.key_broker; the broker serves only models that name it
EndpointPOST /v1/key-requests at key_broker_url; authenticated server requests only
Chain readsDirectly from Solana RPC at confirmed commitment, with account ownership and IDL decoding checked; never from the indexer's database
Deployment pinExpected genesis hash and program id
Data keysLoaded per request from the owner's key store or HSM; exactly 32 bytes; zeroed after wrapping
Vendor verificationIts own Intel and NVIDIA verification, independent of the committee
Audit storeAppend-only release log, written before any ciphertext is returned

A node contacts the broker after boot and after each renewal of its attestation; between those, it serves with the release it holds.

The owner-run broker is the v0 default. As an opt-in for owners who prefer not to run one, the v1 design adds a Sealroom threshold committee: the data key split into 3-of-5 Shamir shares held in HSMs at independent firms, each re-verifying the evidence before contributing its share.

6.3 The key request

The node sends a strict JSON body with three fields: requestId, a fresh UUID; modelId, the Model address; and evidence, the same sealroom-evidence-v1 object the committee attested.

OutcomeHTTPBody
Released201{ auditId, attestationId, attestationRevision, wrappedKey }
Denied403FORBIDDEN, not retryable, with reasonCodes and the auditId of the logged denial
Unavailable503SERVICE_UNAVAILABLE, retryable, with a reason code

attestationRevision is the attestation's signed_sequence. The request UUID never appears in any public record; the public audit identity is a separate random value.

6.4 Release checks

Node (CVM)HPKE key inside the enclaveOwner's key brokerholds the data keySolanaAttestation · Platform · Model1 key request + evidence2 read accounts4 wrapped data key3 check, re-verifyGPU memoryweights decrypted hererelease log → attestation id

Figure: The broker reads the chain, checks the evidence against the on-chain attestation, re-verifies it, wraps the data key to the enclave's HPKE key, reads the chain again and logs the decision before returning the ciphertext.

6.4.1 The release procedure

The broker processes each request in this order:

  1. Parse. Validate the request, compute evidence_hash, and draw a fresh audit id (32 random bytes, base58).
  2. First chain read. In one RPC call, read Config, Node, Platform, Operator, Attestation ["att", node], the Clock and SlotHashes sysvars, the active VerifierSet, the Model, the attested Runtime, the PlatformPin and every GpuPin. If the node's links or the verifier epoch change between the preview and the full read, retry, up to three times. The genesis hash must be the configured one (CHAIN_IDENTITY_MISMATCH).
  3. Policy checks. Run the twelve eligibility rules (Section 6.4.2); require Model.key_broker to be this broker (BROKER_AUTHORITY_MISMATCH); and require evidence_hash, the SHA-256 of the evidence's key transcript, recentSlot and recentSlotHash to equal the attestation's evidence_hash, enclave_key_hash, slot and recent_slot_hash (EVIDENCE_CHAIN_MISMATCH). Any failure denies with every failing reason.
  4. Fix the release identity: genesis hash, attestation address, sequence, signed_sequence, evidence_hash, broker key and policy hash as read.
  5. Re-verify the TD quote against Intel collateral and every GPU report against NVIDIA RIMs and OCSP.
  6. Registry checks on fresh claims. Repeat the committee's registry checks (Section 5.3) against the broker's own vendor claims. The SlotHashes presence check is omitted: freshness was proved when the attestation was recorded, and its expiry bounds its age.
  7. Claims against chain. The verified runtime measurement and RIM hash must equal Attestation.runtime_measurement and Attestation.gpu_rim_hash (VENDOR_CLAIMS_CHAIN_MISMATCH).
  8. Build the context for the key release (Section 6.5.2).
  9. Load and wrap the model's 32-byte data key with HPKE to evidence.keys.hpkePublicKey, with the context as associated data.
  10. Final chain read. Every check of step 3 must still pass and the release identity must be unchanged. A revocation, a freeze, a slash below the minimum or a renewal between the two reads denies the release (REGISTRY_CHANGED_DURING_RELEASE).
  11. Log the audit record with outcome released. If the write fails, nothing is returned.
  12. Return the wrapped key with the audit id, attestation address and revision, and zero the broker's copy of the data key.

On any failure, the broker re-reads the chain once if it has not completed the final read, logs the denial with the policy reasons from its latest read plus the error that stopped the procedure, and returns 403. Every request is checked from the beginning; a repeated request never reuses an earlier decision.

6.4.2 Eligibility rules

The broker runs the same eligibility function that the public API serves at /v1/eligibility?modelId={modelId}&nodeId={nodeId}, so anyone can compute a broker's policy decision in advance from public data. The function evaluates twelve checks, in this order, and reports every one that fails:

#CodePasses when
1no_valid_attestationAn attestation exists and is the node's current one
2attestation_expiredexpires_at is after the chain's current time
3attestation_revokedrevoked_at is unset and Attestation.operator_epoch equals Operator.attestation_epoch
4runtime_not_allowedThe Runtime account is allowed (not revoked, past its timelock) and the attested measurement is in Model.allowed_runtimes
5platform_not_registeredThe platform is registered, belongs to the node's operator and is the node's platform; the attestation names the same node, platform and operator
6tier_below_minOperator.tier >= Model.min_tier
7bond_below_minOperator.bond_amount >= max(Model.min_bond, tier_minimum)
8operator_frozenOperator.frozen is false
9operator_not_on_allowlistModel.operator_allowlist is empty or contains the operator
10tcb_not_up_to_datetcb_status is UpToDate
11node_disabledThe node's status is not Disabled
12protocol_pausedConfig.paused is false

6.4.3 Broker reason codes

Beyond the twelve eligibility codes and the registry codes of Section 3.6.2, a denial can carry:

CodeMeaning
MODEL_UNAVAILABLEThe Model account could not be read
BROKER_AUTHORITY_MISMATCHThe model names a different broker key
EVIDENCE_CHAIN_MISMATCHEvidence hash, enclave key hash, slot or slot hash differs from the attestation
VENDOR_CLAIMS_CHAIN_MISMATCHVerified measurement or RIM hash differs from the attestation
MODEL_KEY_NOT_CONFIGUREDThe key store holds no valid 32-byte key for the model
REGISTRY_CHANGED_DURING_RELEASEThe final read differs from the first
REGISTRY_RECHECK_UNAVAILABLEThe re-read after a failure could not complete

6.5 HPKE wrapping to the enclave's key

6.5.1 The suite

ParameterValue
ModeHPKE Base mode (RFC 9180)
KEMX-Wing, the hybrid of ML-KEM-768 and X25519 (draft-connolly-cfrg-xwing-kem-06)
KDFHKDF-SHA256
AEADAES-256-GCM
Suite identifierX-Wing-draft06-HKDF-SHA256-AES256GCM
infosealroom.owner-key-release.v1
Recipient public keyevidence.keys.hpkePublicKey: 1,216 bytes, the ML-KEM-768 public key (1,184) followed by the X25519 public key (32)
PlaintextThe 32-byte data key
Associated dataThe key-release context
Sender contextOne fresh context per wrap

The hybrid KEM derives the shared secret from both ML-KEM-768 and X25519, so recovering the data key from a recorded envelope requires breaking both.

The broker trusts the recipient key because its hash is the on-chain enclave_key_hash (step 3), and the same transcript produced the H carried in REPORTDATA and every GPU nonce (step 6) of a quote from an allowed runtime on a pinned platform. The private key was generated inside that runtime and never leaves the CVM.

6.5.2 The key-release context

The associated data is the UTF-8 JSON encoding of:

FieldValue
domainsealroom.key-release.v1
programIdThe sealroom program id
genesisHashThe cluster's genesis hash
modelIdModel address
nodeIdNode address
attestationIdAttestation ["att", node] address
attestationRevisionsigned_sequence of the attestation used
evidenceHashevidence_hash of the request's evidence
requestIdThe node's request UUID

The envelope returned to the node has five fields: version (sealroom-key-wrap-v1), suite, and the base64 enc (KEM encapsulation), ciphertext (the sealed data key) and context (the associated data).

6.5.3 Unwrap inside the CVM

The runtime's key client:

  1. Authenticates the broker's TLS identity against the broker recorded for the model in the registry, before accepting the response.
  2. Rebuilds the expected context from its own values (program, genesis hash, model, node, its evidence hash and request id) and the attestation address and revision in the response, and requires the envelope's context to equal it byte for byte.
  3. Opens the ciphertext with its HPKE private key, the same info and the context as associated data.
  4. Requires a 32-byte plaintext.

Any failure in steps 2 to 4 yields one generic error, KEY_UNWRAP_FAILED. The client then fetches the manifest, checks it against Model.manifest_hash, decrypts the weights chunk by chunk in CVM memory and loads them into GPU protected memory.

6.6 Holding and using the key

The node keeps one release per model, bound to the attestation revision it was issued under. Before each inference it checks that its release revision equals the attestation's current signed_sequence and that the model is still eligible on the node; it checks eligibility again before settling the payment. After a renewal it requests a release for the new revision. After a revocation, a freeze or an epoch advance, both checks fail and the node neither serves nor charges.

The runtime's network egress is limited to the key broker, the Solana RPC and the payment facilitator, and it serves only architectures its pinned vLLM build supports, with trust_remote_code off, so no owner-supplied code runs next to the weights and prompts.

6.7 The release log

6.7.1 Record schema

The broker appends one record per attempt, released or denied:

FieldTypeContent
idbase58, 32 bytesAudit id, random per attempt
modelIdbase58Model address
nodeIdbase58Node address
operatorIdbase58Operator address observed
brokerPublicKeybase58This broker's key
attestationIdbase58Attestation address relied on
attestationRevisionu64 stringsigned_sequence relied on
evidenceHash64 hexHash of the request's evidence
enclaveKeyHash64 hexSHA-256 of the request's key transcript
policyHash64 hexSHA-256 of the model policy observed (below)
readSlotu64 stringSlot of the chain read the decision used
releasedAtISO 8601Decision time
outcomeenumreleased or denied
reasonCodesstring arrayEmpty for a release; every failing code for a denial
audit.chainMatchboolWhether the evidence matched the on-chain attestation
audit.checkedAtISO 8601Time of the check

The policy hash is taken over the JSON encoding of the model's release policy as read from chain:

policyHash = SHA-256(json({ allowedRuntimes, minTier, minBond, operatorAllowlist }))

6.7.2 Rules

A ciphertext leaves the broker only after its released record is persisted. Every attempt has its own audit id, and a denial is logged as durably as a release, with the revision and policy from the latest chain read. The log holds hashes and identifiers only: data keys, private keys, raw evidence, wrapped ciphertexts and request UUIDs never enter it. The log is public at /v1/key-releases, filterable by outcome, and each record also appears in the registry's activity feed as key_released or key_denied with source key_broker.

6.7.3 Auditing against the chain

Each record names everything needed to check it against the chain without trusting the broker. For a released record, an auditor:

  1. Finds the AttestationRecorded event for attestationId at attestationRevision and checks that its evidence_hash and enclave_key_hash equal the record's.
  2. Checks that the attestation had not expired at releasedAt and had no AttestationRevoked event or operator-epoch advance before readSlot.
  3. Recomputes the model's policy hash from the Model account and compares it with policyHash.
  4. Re-evaluates the twelve eligibility rules at readSlot.

Comparing the broker log with the chain this way is how Sealroom measures that no key is released outside policy.

6.8 Base-model royalties through key gating

6.8.1 What the program enforces

A base model sets derivative_royalty_bps, the share it requires from every model built on it. A derivative, such as a LoRA adapter trained on an encrypted base model, names its base in base_model. Split entries carry a role (Owner, Operator, BaseModel or Contributor) and a locked flag. At create_model with base_model = Some(base):

  1. The base Model account must be supplied and equal base (InvalidBaseModel). Without a base_model, no base account may be supplied.
  2. If the base's derivative_royalty_bps is above zero, the derivative's split must contain an entry with role BaseModel, locked = true, bps >= base.derivative_royalty_bps, paying the canonical USDC associated token account of the base's owner, supplied and verified as a token account owned by that owner (RoyaltyRequired).

At propose_split, every locked entry of the current split must reappear with the same recipient, beneficiary and role, still locked, with at least the same bps, unless the entry's beneficiary signs the proposal (LockedSplit). The base owner's royalty therefore cannot be lowered, unlocked or removed without the base owner's signature. Each lane snapshots its model's split, and distribute pays the BaseModel entry from every lane of the derivative.

6.8.2 The key gate

The base model's weights are encrypted under the base owner's own data key, held by the base owner's broker. A node serving the derivative needs two releases: the derivative's key from the derivative owner's broker, and the base key from the base owner's broker. The base owner's broker releases the base key only for derivatives whose split pays the base's derivative_royalty_bps as a locked entry. The program already guarantees that any Model naming the base in base_model carries that entry, so the broker's royalty check reduces to reading the derivative's base_model and confirming the locked BaseModel entry is present in its split.

6.8.3 Worked example

The protocol fee is 500 bps at launch. Take a base owner that sets derivative_royalty_bps = 1000, a derivative split that gives the base exactly that, and a lane that receives 100 USDC (100,000,000 base units) of caller payments:

fee = floor(100000000 * 500 / 10000) = 5000000 net = 100000000 - 5000000 = 95000000 base_royalty = floor(95000000 * 1000 / 10000) = 9500000

The base owner receives 9.5 USDC from this distribution, in the same transaction that pays the protocol fee, the operator and the derivative's other recipients. Chapter 9 gives the complete distribution arithmetic, including the rounding remainder paid to the final entry.

7. The sealroom program

Sealroom keeps its registry and its money in one Solana program, sealroom, written with Anchor 0.31.1 and deployed through the upgradeable BPF loader. The program records operators and their bonds, the physical platforms and GPUs each operator has pinned, the nodes that run on them, the runtime measurements that are allowed, the attestations the verifier committee has signed, and the models that owners list with their policy, price and split. It holds three kinds of funds: operator bonds, the USDC that callers pay into per-(model, node) lane vaults, and dispute deposits. Each sits in a token account whose authority is a program-derived address, and leaves it only through the instructions described here.

The published interface (IDL sealroom, version 0.2.0) contains 39 instructions, 14 account types, 35 event types and 54 program error codes. This chapter documents all of them from the program source: the account layouts byte by byte, every instruction with its signers, constraints, state changes, events and failure codes, the fixed durations that govern change, the deployment and upgrade model, and the measured transaction sizes and compute costs.

7.1 Design principles

One program, one configuration. All Sealroom state lives in accounts owned by the sealroom program, anchored by a single Config account at the seed config. Config fixes the USDC mint, the genesis hash of the cluster, the admin and arbitration authorities, the fee treasury and protocol fee, the bond minimum for each operator tier, the dispute settings and the pause flag. Every instruction that reads configuration derives Config from that seed and its stored bump, so a transaction cannot substitute a configuration account of its own.

Program-derived addresses for every record. All fourteen account types are PDAs, and their seeds encode their relationships: a Node is derived from its Platform and an index, a Lane from a Model and a Node, a Claim from a Lane and a recipient token account, a Dispute from an Operator and a sequence number. Instructions re-derive each address from the account's own stored fields and check parent links with Anchor has_one constraints, so an account from another operator, model or lane fails before the handler runs. Uniqueness comes from creation with init: a second PlatformPin for the same platform identity, a second Lane for the same pair or a second Dispute with the same sequence number cannot exist.

USDC through the original SPL Token program. Every amount is a u64 count of base units of a six-decimal mint; initialize rejects a mint with any other decimals. Every USDC movement is an SPL Token transfer_checked against the Config mint with six decimals, so a token account of another mint fails inside the token program. Fee and split arithmetic multiplies in u128 before dividing, every counter uses checked addition and subtraction, and no value anywhere in the program is a floating-point number.

Canonical associated token accounts. Where an account holds funds, its address follows from derivation. The bond vault is the associated token account (ATA) of the Operator PDA for USDC, a lane vault is the ATA of the Lane PDA, and a dispute deposit vault is the ATA of the Dispute PDA. Recipients are held to the same rule: each split entry's recipient_ata must be the canonical USDC ATA of its recipient_owner, an operator's payout_ata must be the canonical ATA of the operator authority, and both the fee treasury and the dispute forfeiture account must be canonical ATAs whose owners are recorded at the moment they are bound. Deferred claims pay into the beneficiary's canonical ATA unless governance selects the alternative destination policy.

Custody by the program, with no admin path to user funds. Funds leave a vault only when the program signs with that vault's PDA seeds, and every such path names its destination by rule:

VaultToken authorityInstructions that move funds outDestination
Bond vaultOperator PDA ["op", authority]withdraw_bond; settle_slashA USDC account owned by the operator authority; the deposit vault of a ruled dispute
Lane vaultLane PDA ["lane", model, node]distribute; claim_deferredThe configured fee treasury and the lane's resolved split recipients; the beneficiary of a Claim
Deposit vaultDispute PDA ["dispute", operator, sequence]claim_dispute; claim_forfeited_depositThe claimant; the forfeiture beneficiary recorded in the Dispute

The admin key signs none of these instructions. Its influence over destinations is limited to the identity of the fee treasury and of the forfeiture recipient, both of which change only through 48-hour proposals that anyone can read on chain before they take effect, and to the claim destination policy, which under either setting pays only accounts owned by the beneficiary.

Timelocks on changes that redirect value or trust, immediate effect for protective actions. Allowing a runtime, changing the fee or treasury, changing dispute settings and changing a model's split all wait a fixed delay compiled into the program. Revoking a runtime, revoking an attestation, freezing an operator and pausing the protocol take effect in the slot they land.

Fail-closed configuration. Configuration values that represent a policy decision are Option fields, and None means the decision has not been made. Instructions that depend on them refuse to run until they are set: distribution and claims require a claim policy, dispute admission requires a deposit, an arbitration authority, a deposit policy and a per-claimant cap, and a positive arbitration slash requires a configured deposit.

Separated authorities. The admin and the arbitration authority must be different keys at initialization, at every dispute-configuration proposal and again when that proposal is applied. The arbitration authority must also differ from the owner of the forfeiture account, so the party that dismisses a complaint is never the party that receives the forfeited deposit.

Permissionless cranks. apply_fee, apply_dispute_config, apply_split, open_lane, prepare_claim, distribute and settle_slash take no privileged signer, and record_attestation accepts any relayer as fee payer. Their outcome is determined entirely by on-chain state and by the arguments, which the program validates, so the keeper service, a model owner or any third party can submit them.

Bounded accounts. Each account is allocated at its maximum size when it is created, with every vector and string bounded by max_len, and instructions check the same bounds before writing. No account is reallocated. The largest, Model, is 2,348 bytes.

Events through self-CPI. Every instruction except initialize emits its events with Anchor's emit_cpi!, which records the event bytes as an inner instruction signed by the __event_authority PDA. Indexers read those bytes from the transaction's inner instructions, so events survive the 10 KB limit on textual program logs. initialize emits its single event to the program log and is reconciled from the Config account it creates.

7.2 Accounts

7.2.1 Encoding

Every account begins with Anchor's eight-byte discriminator, followed by version: u8 and bump: u8. The version is 1 for every account written by this program. Fields are Borsh-encoded in declaration order: public keys and hashes are raw 32-byte arrays, the runtime measurement is a raw 48-byte array, amounts, identifiers, slots and sequence numbers are little-endian u64, timestamps are little-endian i64 Unix seconds, basis points are u16, and enum variants are one-byte tags. An Option<T> takes one tag byte plus the size of T, a vector takes a four-byte length prefix plus its maximum element count times the element size, and a string takes a four-byte prefix plus its maximum byte length. The sizes below are the allocated maximums, including the discriminator.

AccountSeeds before bumpBytesCreated byRent payer
Config"config"594initializeAdmin
VerifierSet"verifiers", epoch u64 LE264initializeAdmin
Operator"op", operator authority193register_operatorAdmin
Platform"plat", Operator PDA, platform id hash343register_platformOperator authority
PlatformPin"platform_pin", platform id hash106register_platformOperator authority
GpuPin"gpu_pin", GPU UEID hash107register_platformOperator authority
Node"node", Platform PDA, index u16 LE503register_nodeOperator authority
Attestation"att", Node PDA384record_attestationRelayer
Runtime"rt", SHA-256(measurement)256allow_runtimeAdmin
Model"model", owner, id u64 LE2,348create_modelModel owner
Lane"lane", Model PDA, Node PDA719open_laneAny payer
Claim"claim", Lane PDA, recipient ATA122prepare_claimAny payer
Dispute"dispute", Operator PDA, sequence u64 LE415open_dispute, arbitrate_operatorClaimant or arbitration
ClaimantDisputes"claimant_disputes", claimant, Operator PDA78open_dispute, arbitrate_operatorClaimant or arbitration
Config["config"]VerifierSet["verifiers", epoch]Runtime["rt", measurement]Operator["op", authority]Platform["plat", op, id]Node["node", plat, i]Attestation["att", node]Model["model", owner, id]Lane["lane", model, node]Lane vaultUSDC ATA of the laneClaim["claim", lane, ata]Dispute["dispute", op, n]Seeds in brackets. Platform and GPU pins, one per identity hash,stop the same machine or GPU from being registered twice.

Figure: The fourteen program accounts, the seeds that derive them and the parent links that instructions check.

Several value types are embedded inside accounts:

TypeLayoutBytes
SplitEntryrecipient_ata 32, recipient_owner 32, bps: u16 2, locked: bool 1, role: SplitRole 168
PendingSplitrevision: u64 8, proposed_at: i64 8, effective_at: i64 8, entries: Vec<SplitEntry> (max 8) 548572
PendingFeeprotocol_fee_bps: u16 2, treasury_ata 32, treasury_owner 32, effective_at: i64 874
PendingDisputeConfigarbitration 32, dispute_deposit: u64 8, max_open_per_claimant_operator: u32 4, max_open_per_operator: u32 4, forfeiture_ata 32, forfeiture_owner 32, deposit_policy 1, effective_at: i64 8121
TierBondMinimumtier: u8 1, amount: u64 89
Pricetag 1, largest variant (three u64) 2425

The enumerations are:

EnumVariants
GpuModelH100, H200
NodeStatusRegistered, Disabled
TcbStatusUpToDate, OutOfDate, ConfigurationNeeded, Revoked, Unknown
AttestationMethodCommittee, Zk
SplitRoleOwner, Operator, BaseModel, Contributor
PriceExact { per_call }, Upto { input_per_million_tokens, output_per_million_tokens, max_per_call }
DisputeStatusOpen, RuledForClaimant, Dismissed
DisputeDepositPolicyReturnToClaimant (not selectable), ForfeitOnDismissal
ClaimPolicyBeneficiaryOnlyNoExpiry
ClaimDestinationPolicyCanonicalAta, AnyOwnedUsdc
SettlementMechanismLaneBoundary
RescuePolicy, SplitSettlementPolicyThreePartyOwnerRefund, DistributionTime (retained in the interface, never stored)

7.2.2 Config

Seeds ["config"]. One per deployment, created by initialize. It holds every protocol-wide parameter, the authorities and the two governance proposals that can be pending at once.

FieldTypeBytesMeaning
discriminator[u8; 8]8Anchor account discriminator
versionu81Layout version, 1
bumpu81PDA bump
genesis_hash[u8; 32]32Genesis hash of the cluster, bound into every committee message
usdc_mintPubkey32The USDC mint (six decimals)
adminOption<Pubkey>33Admin authority, the upgrade authority that signed initialize
arbitrationOption<Pubkey>33Arbitration authority
fee_treasury_ataOption<Pubkey>33Canonical USDC ATA that receives the protocol fee
protocol_fee_bpsOption<u16>3Protocol fee in basis points; 500 at launch
default_operator_share_bpsOption<u16>3Reserved; written as None
dispute_depositOption<u64>9Complaint deposit in base units
mainnet_total_bonds_capOption<u64>9Reserved aggregate bond cap; written as None
mainnet_total_vaults_capOption<u64>9Reserved aggregate vault cap; written as None
tier_bond_minimumsVec<TierBondMinimum>, max 331Minimum bond for tiers 1, 2 and 3
attestation_ttl_secondsu64886,400
unbonding_period_secondsu6482,592,000
split_delay_secondsu648604,800
runtime_timelock_secondsu648172,800
active_verifier_epochOption<u64>9Epoch of the verifier set whose signatures count; 0 at initialization
pausedbool1Pause flag
fee_treasury_ownerOption<Pubkey>33Wallet that owned fee_treasury_ata when it was bound; beneficiary of deferred fees
pending_feeOption<PendingFee>75Proposed fee and treasury with their effective time
claim_policyOption<ClaimPolicy>2BeneficiaryOnlyNoExpiry once selected
rescue_policyOption<RescuePolicy>2Retained; always None
split_settlement_policyOption<SplitSettlementPolicy>2Retained; always None
dispute_deposit_policyOption<DisputeDepositPolicy>2ForfeitOnDismissal once dispute settings apply
claim_destination_policyClaimDestinationPolicy1CanonicalAta at initialization, or AnyOwnedUsdc
pending_dispute_configOption<PendingDisputeConfig>122Proposed dispute settings with their effective time
max_open_per_claimant_operatorOption<u32>5Cap on simultaneously open disputes per (claimant, operator)
max_open_per_operatoru324Cap on simultaneously open disputes per operator; 8 at initialization
dispute_forfeiture_ataOption<Pubkey>33Canonical ATA that receives dismissed deposits
dispute_forfeiture_ownerOption<Pubkey>33Owner of that ATA, the forfeiture beneficiary
settlement_mechanismSettlementMechanism1LaneBoundary
Total594

The four duration fields record the compiled constants for readers; handlers apply the constants themselves, so no instruction can shorten a delay by editing configuration.

7.2.3 VerifierSet

Seeds ["verifiers", epoch.to_le_bytes()]. The committee whose Ed25519 signatures admit attestations. initialize creates the epoch-0 set with exactly five distinct keys and threshold three; the allocation holds up to seven members.

FieldTypeBytesMeaning
discriminator, version, bump10Header
epochu648Committee epoch
membersVec<Pubkey>, max 7228Verifier Ed25519 public keys; a member's index is its bit in an attestation's signer_bitmap
thresholdu81Signatures required, 3
activated_ati648Activation time
retired_atOption<i64>9Retirement time; a retired set admits nothing
Total264

7.2.4 Operator

Seeds ["op", authority]. One per vetted operator, created by the admin after off-chain vetting. The PDA is also the token authority of the operator's bond vault.

FieldTypeBytesMeaning
discriminator, version, bump10Header
authorityPubkey32Operator wallet
agreement_hash[u8; 32]32Hash of the signed Operator Agreement
tieru81Tier 1, 2 or 3
bond_vaultPubkey32ATA of this PDA for USDC
bond_amountu648Withdrawable bond principal; excludes reserved slashes and direct donations to the vault
unbond_atOption<i64>9Earliest withdrawal time once unbonding is requested
frozenbool1Frozen by admin or arbitration
payout_ataPubkey32Canonical USDC ATA of authority; destination of every Operator-role share
open_dispute_countu324Open ordinary disputes against this operator
next_dispute_sequenceu648Sequence number the next Dispute must use
registered_ati648Registration time
attestation_epochu648Revocation epoch committed into every attestation of this operator
pending_slash_amountu648Slashed amount reserved in the bond vault and not yet settled
Total193

7.2.5 Platform, PlatformPin and GpuPin

A Platform is one physical server identity (the hash of its Intel PPID or PCK identity) registered by one operator, with the UEID hashes of the GPUs installed in it. Seeds ["plat", operator, platform_id_hash].

FieldTypeBytesMeaning
discriminator, version, bump10Header
operatorPubkey32Operator PDA
platform_id_hash[u8; 32]32Hash of the platform identity
gpu_ueid_hashesVec<[u8; 32]>, max 8260Hashes of the GPU UEIDs pinned to this platform
registeredbool1Registration flag, checked by node registration and attestation
registered_ati648Registration time
Total343

The Platform seeds include the operator, so platform identity is made unique across operators by a second, global account. A PlatformPin at ["platform_pin", platform_id_hash] is created with init in the same instruction, so a platform identity can be registered exactly once across all operators.

FieldTypeBytesMeaning
discriminator, version, bump10Header
platformPubkey32Platform PDA that owns this identity
operatorPubkey32Operator PDA
platform_id_hash[u8; 32]32The pinned identity hash
Total106

A GpuPin at ["gpu_pin", gpu_ueid_hash] pins one GPU globally to one platform and, once a node claims it, to one node. GpuPins are created and read by the program directly from the instruction's remaining accounts.

FieldTypeBytesMeaning
discriminator, version, bump10Header
platformPubkey32Platform the GPU belongs to
gpu_ueid_hash[u8; 32]32Hash of the GPU UEID
nodeOption<Pubkey>33Node that claimed the GPU, or None
Total107

7.2.6 Node

Seeds ["node", platform, index.to_le_bytes()] with a u16 index. One confidential VM on a registered platform, with one to eight of the platform's GPUs.

FieldTypeBytesMeaning
discriminator, version, bump10Header
platformPubkey32Platform PDA
operatorPubkey32Operator PDA
indexu162Index of the node on its platform
node_keyPubkey32Node's registered public key
endpointString, max 128132HTTPS endpoint of the node
gpu_modelGpuModel1H100 or H200
gpu_countu81Number of GPUs claimed
gpu_ueid_hashesVec<[u8; 32]>, max 8260GPUs claimed by this node
statusNodeStatus1Registered or Disabled
Total503

7.2.7 Attestation

Seeds ["att", node]. One per node, overwritten by each committee-signed renewal. It is the record the key broker and callers read before trusting a node.

FieldTypeBytesMeaning
discriminator, version, bump10Header
nodePubkey32Node PDA
sequenceu648Replay barrier; advanced by each record and by revocation
signed_sequenceu648Sequence of the latest committee-signed record
operator_epochu648Operator revocation epoch the committee signed
runtime_measurement[u8; 48]48TDX runtime measurement
gpu_rim_hash[u8; 32]32Hash of the NVIDIA reference measurements matched
evidence_hash[u8; 32]32Hash of the archived attestation evidence
enclave_key_hash[u8; 32]32Hash of the enclave's HPKE, TLS and voucher public keys
tcb_statusTcbStatus1UpToDate for every record the program accepts
slotu648Recent slot bound into the evidence
recent_slot_hash[u8; 32]32Hash of that slot
expires_ati648Expiry, at most 24 hours after recording
recorded_ati648Recording time
methodAttestationMethod1Committee
verifier_epochu648Epoch of the signing committee
signer_bitmapu81Bit i set when member i signed
canonical_message_hash[u8; 32]32SHA-256 of the 200-byte signed message
revoked_atOption<i64>9Revocation time; a set value latches the record as revoked
revoked_byOption<Pubkey>33Revoking authority
revoke_reason_hashOption<[u8; 32]>33Hash of the published reason
Total384

7.2.8 Runtime

Seeds ["rt", SHA-256(measurement)]. The 48-byte measurement is hashed only to form the seed; the full measurement is stored. A Runtime is the protocol-level allowlist entry for one measured image; each model additionally pins its own list of measurements.

FieldTypeBytesMeaning
discriminator, version, bump10Header
measurement[u8; 48]48Runtime measurement
image_nameString, max 6468Image name, 1 to 64 bytes
image_versionString, max 3236Image version, up to 32 bytes
source_commitString, max 6468Source commit, exactly 40 lowercase hexadecimal characters
proposed_ati648Time allow_runtime landed
effective_ati648proposed_at + 172,800 seconds
revokedbool1Revocation flag
revoked_atOption<i64>9Revocation time
Total256

7.2.9 Model

Seeds ["model", owner, id.to_le_bytes()]. A model listing: the commitment to its encrypted weights, the key broker that releases its data key, its eligibility policy, its price and its revenue split.

FieldTypeBytesMeaning
discriminator, version, bump10Header
ownerPubkey32Model owner
idu648Owner-local model identifier
manifest_hash[u8; 32]32Hash of the encrypted-weights manifest
key_brokerPubkey32Key broker public key
key_broker_urlString, max 128132HTTPS URL of the key broker
allowed_runtimesVec<[u8; 48]>, max 8388Runtime measurements this model accepts
min_tieru81Minimum operator tier, 1 to 3
min_bondu648Minimum operator bond in base units
operator_allowlistVec<Pubkey>, max 16516Operators this model accepts
pricePrice25Exact or Upto price in base units
derivative_royalty_bpsu162Royalty that derivatives of this model must pay to it
base_modelOption<Pubkey>33Model this one derives from
split_revisionu648Revision of the current split, 1 at creation
splitVec<SplitEntry>, max 8548Current split
pending_splitOption<PendingSplit>573Proposed split and its effective time
Total2,348

7.2.10 Lane

Seeds ["lane", model, node]. The settlement unit for one model served on one node. Callers pay into its vault, the lane's own ATA, and distribute allocates the vault balance.

FieldTypeBytesMeaning
discriminator, version, bump10Header
modelPubkey32Model PDA
nodePubkey32Node PDA
vault_ataPubkey32ATA of this lane for USDC
total_inu648Revenue counted into the lane; equal to total_allocated after each distribution
total_distributedu648USDC actually transferred out, including later claims
last_distributedOption<i64>9Time of the last allocating distribution
total_allocatedu648Revenue assigned to fee and recipients
reserved_amountu648Deferred entitlements still held in the vault
active_split_revisionu648Model split revision this lane settles under
active_splitVec<SplitEntry>, max 8548Snapshot of that split
eligible_protocol_fees_paidu648Protocol fees actually paid to an independent treasury; the basis of dispute admission
admission_credit_consumedu648Part of that amount already used to admit disputes
Total719

Two identities reconcile every lane, where payments_in is all USDC that has entered the vault:

payments_in = lane.total_distributed + vault.amount lane.total_allocated = lane.total_distributed + lane.reserved_amount

7.2.11 Claim

Seeds ["claim", lane, recipient_ata]. A deferred entitlement for one recipient on one lane. Claims are prepared for recipients whose positive share cannot be paid directly; healthy payouts need none.

FieldTypeBytesMeaning
discriminator, version, bump10Header
lanePubkey32Lane PDA
recipient_ataPubkey32Recipient token account the share was addressed to
beneficiaryPubkey32Wallet entitled to claim; fixed when the Claim is created
amountu648Accrued, unclaimed amount
eligible_protocol_fee_amountu648Part of amount that is an eligible protocol fee
Total122

7.2.12 Dispute and ClaimantDisputes

Seeds ["dispute", operator, sequence.to_le_bytes()]. One complaint, or one arbitration action, against an operator. The Dispute PDA is the token authority of its deposit vault.

FieldTypeBytesMeaning
discriminator, version, bump10Header
operatorPubkey32Operator PDA
sequenceu648Operator-local sequence number
modelPubkey32Claimant's model, or the default key for a security-only action
claimantPubkey32Claimant wallet
evidence_hash[u8; 32]32Hash of the complaint evidence
depositu648Deposit taken at admission; 0 for arbitration actions
deposit_vaultPubkey32ATA of this PDA for USDC
statusDisputeStatus1Open, RuledForClaimant or Dismissed
ruling_hashOption<[u8; 32]>33Hash of the published ruling
slash_amountu648Bond slashed by the ruling
opened_ati648Admission time
ruled_atOption<i64>9Ruling time
claimable_amountu648Amount the claimant can withdraw
slash_pending_amountu648Slash reserved in the bond vault, not yet moved
lanePubkey32Lane the admission credit came from
nodePubkey32Node of that lane
forfeiture_ataPubkey32Forfeiture destination snapshotted at admission
forfeiture_beneficiaryPubkey32Forfeiture beneficiary snapshotted at admission
deposit_policyDisputeDepositPolicy1ForfeitOnDismissal
forfeited_amountu648Deposit forfeited on dismissal, not yet claimed
arbitration_initiatedbool1True for records created by arbitrate_operator
admission_credit_usedu648Lane credit consumed at admission
admission_required_amountu648Paid-fee threshold that applied
Total415

ClaimantDisputes at ["claimant_disputes", claimant, operator] counts the open disputes one claimant has against one operator, across all of that claimant's models.

FieldTypeBytesMeaning
discriminator, version, bump10Header
claimantPubkey32Claimant wallet
operatorPubkey32Operator PDA
open_countu324Currently open disputes
Total78

7.3 Instructions

7.3.1 Conventions

Every instruction except initialize carries two trailing accounts for self-CPI events: event_authority, the PDA at seed __event_authority, and program, the sealroom program itself. Three checks recur. The admin check compares the signer with Config.admin and raises Unauthorized. The running check raises Paused while Config.paused is set. The healthy-operator check raises OperatorFrozen for a frozen operator and InsufficientBond when bond_amount is below the minimum for the operator's tier (ConfigurationUnset if the tier has none).

Anchor validates account constraints before a handler runs. An account typed as a Sealroom account must be owned by the program and carry the right discriminator. A failed seeds, has_one, address, token::mint, token::authority or associated_token constraint aborts with Anchor's constraint error (ConstraintSeeds, ConstraintHasOne, ConstraintAddress, ConstraintTokenMint, ConstraintTokenOwner, ConstraintAssociated), and creating an account with init at an address already in use fails in the System Program. The program's own error codes, listed in 7.5, are raised by handler checks and by the constraints that name them explicitly. Every instruction is atomic: any failure, including a failed token CPI, reverts the whole transaction.

7.3.2 Admin and governance

initialize(genesis_hash, protocol_fee_bps, arbitration, tier_bond_minimums, verifiers)

  • Signer: admin, which pays for Config and the epoch-0 VerifierSet.
  • Accounts: program must be sealroom with programdata_address equal to program_data; program_data.upgrade_authority_address must equal admin (Unauthorized). config is created at ["config"] and verifier_set at ["verifiers", 0]. fee_treasury is a token account of usdc_mint.
  • Checks: fee ≤ 10,000 bps (InvalidBps); exactly five distinct, non-default verifier keys (InvalidVerifierSet); exactly three tier minima (ConfigurationUnset) listed for tiers 1, 2, 3 in order (InvalidTier); mint decimals 6 (InvalidMint); arbitration different from the admin (AuthoritiesMustDiffer); the treasury is the canonical ATA of its owner for the mint (InvalidRecipients).
  • Effects: writes Config with the admin, mint, genesis hash, fee, treasury and treasury owner, the four duration constants, active_verifier_epoch = 0, claim_destination_policy = CanonicalAta, max_open_per_operator = 8, settlement_mechanism = LaneBoundary and every undecided policy as None; writes the VerifierSet with threshold 3.
  • Event: ConfigInitialized (program log).

set_paused(paused) — Signer: admin (admin check). Sets Config.paused. No event.

set_settlement_policy(claim_policy, rescue_policy, split_settlement_policy, claim_destination_policy) — Signer: admin. Sets claim_policy and claim_destination_policy. The two retained arguments are accepted and ignored; their fields stay None, so the admin cannot alter the lane-boundary settlement mechanism. No event.

propose_fee(protocol_fee_bps)

  • Signer: admin. Accounts: treasury, a token account of the Config mint.
  • Checks: treasury is the canonical ATA of its owner (InvalidRecipients); fee ≤ 10,000 bps (InvalidBps); no fee proposal pending (PendingSplitExists).
  • Effects: pending_fee = { protocol_fee_bps, treasury_ata, treasury_owner, effective_at = now + 172,800 }. The treasury owner is snapshotted, so a later change of the token account's owner cannot redirect fees.
  • Event: FeeProposed.

cancel_fee() — Signer: admin. Clears pending_fee (NoPendingSplit if none). No event.

apply_fee() — No privileged signer. Requires a pending fee (NoPendingSplit) whose effective_at has passed (TimelockPending). Writes protocol_fee_bps, fee_treasury_ata and fee_treasury_owner and clears the proposal. Event: FeeApplied.

propose_dispute_config(arbitration, dispute_deposit, max_open_per_claimant_operator, max_open_per_operator, deposit_policy)

  • Signer: admin. Accounts: forfeiture_treasury, a token account of the Config mint.
  • Checks: arbitration is neither the admin nor the default key (AuthoritiesMustDiffer); deposit and both caps are positive and the policy is ForfeitOnDismissal (ConfigurationUnset); no dispute proposal pending (PendingSplitExists); the forfeiture treasury is canonical (InvalidRecipients) and its owner is not the proposed arbitration key (AuthoritiesMustDiffer).
  • Effects: pending_dispute_config with the snapshotted forfeiture owner and effective_at = now + 172,800.
  • Event: DisputeConfigProposed.

cancel_dispute_config() — Signer: admin. Clears the proposal (NoPendingSplit if none). No event.

apply_dispute_config() — No privileged signer. Requires a pending proposal (NoPendingSplit) past its effective time (TimelockPending), and re-checks that the proposed arbitration differs from the current admin and from the forfeiture owner (AuthoritiesMustDiffer). Writes arbitration, dispute_deposit, both caps, dispute_forfeiture_ata, dispute_forfeiture_owner and dispute_deposit_policy, and clears the proposal. Event: DisputeConfigApplied.

set_dispute_config(arbitration, dispute_deposit, deposit_policy) — Retained in the interface. It always fails with GovernanceTimelockRequired and changes no state; dispute settings change only through the proposal path above.

7.3.3 Operators

register_operator(agreement_hash, tier)

  • Signer: admin, who pays rent. The operator's authority is named but does not sign; the admin admits it after off-chain vetting.
  • Accounts: operator created at ["op", authority]; bond_vault created if needed as the ATA of the Operator PDA for USDC; payout_ata must already exist as the canonical USDC ATA of authority; usdc_mint must equal Config.usdc_mint.
  • Checks: admin check, running check, the tier has a configured minimum (ConfigurationUnset).
  • Effects: a new Operator with zero bond, no unbond request, not frozen, zero counters and attestation_epoch = 0.
  • Event: OperatorRegistered.

set_tier(tier) — Signer: admin. The tier must have a minimum (ConfigurationUnset). Sets Operator.tier. Event: OperatorTierSet.

freeze_operator(frozen) — Signer: admin. Sets or clears Operator.frozen. A frozen operator cannot deposit, unbond, withdraw, register platforms or nodes, or record attestations. Event: OperatorFrozen.

deposit_bond(amount)

  • Signer: operator authority. Accounts: operator at ["op", authority] with has_one = authority and has_one = bond_vault; source, a USDC account whose token authority is the signer; bond_vault, a USDC account whose token authority is the Operator PDA.
  • Checks: running; amount > 0 (InvalidAmount); not frozen (OperatorFrozen); no unbond request (Unbonding).
  • Effects: transfer_checked of amount into the vault; bond_amount += amount. Tokens sent to the vault by any other route do not raise bond_amount.
  • Event: BondDeposited.

request_unbond() — Signer: operator authority (has_one = authority). Requires a positive bond (InvalidAmount), no freeze (OperatorFrozen), no open dispute (OpenDispute) and no existing request (Unbonding). Sets unbond_at = now + 2,592,000. Event: UnbondRequested.

cancel_unbond() — Signer: operator authority. Requires a request (UnbondNotRequested) and clears it; no funds move. Event: UnbondCancelled.

withdraw_bond()

  • Signer: operator authority. Accounts: Operator with has_one = authority, bond_vault; destination, a USDC account owned by the authority.
  • Checks: not frozen (OperatorFrozen); no open dispute (OpenDispute); a request exists (UnbondNotRequested) and now ≥ unbond_at (TimelockPending); the vault holds at least bond_amount (InvalidAmount).
  • Effects: transfers exactly bond_amount, signed by the Operator PDA, and resets bond_amount and unbond_at. Reserved slashes are already outside bond_amount and stay in the vault for settle_slash. A matured request with zero principal, as after a full slash, clears unbond_at without a transfer.
  • Event: BondWithdrawn when a transfer occurs.

register_platform(platform_id_hash, gpu_ueid_hashes)

  • Signers: operator authority (rent payer) and admin. Accounts: Operator with has_one = authority; platform created at ["plat", operator, platform_id_hash]; platform_pin created at ["platform_pin", platform_id_hash]; one remaining account per GPU, each the GpuPin PDA for its UEID hash.
  • Checks: admin check; running; healthy operator; 1 to 8 distinct GPU hashes with one remaining account each (InvalidGpuSet); each GpuPin address is the canonical PDA, writable, empty and System-owned (GpuAlreadyClaimed).
  • Effects: creates every GpuPin with node = None, the PlatformPin and the Platform with registered = true. The program tops up a GpuPin address only by the lamports it lacks for rent, so lamports sent to that address in advance cannot block its creation.
  • Event: PlatformRegistered.

register_node(index, node_key, endpoint, gpu_model, gpu_ueid_hashes)

  • Signer: operator authority (rent payer). Accounts: Operator with has_one = authority; Platform at its seeds with has_one = operator; node created at ["node", platform, index]; one GpuPin per GPU as remaining accounts.
  • Checks: running; healthy operator; platform registered (PlatformNotRegistered); endpoint is a valid HTTPS URL of at most 128 bytes (InvalidEndpoint); 1 to 8 distinct GPU hashes, all on the platform, with one account each (InvalidGpuSet); each GpuPin is the program-owned canonical PDA for its hash (InvalidGpuSet), belongs to this platform and has no node (GpuAlreadyClaimed).
  • Effects: sets each GpuPin's node; writes the Node with gpu_count equal to the number of hashes and status = Registered.
  • Event: NodeRegistered.

The endpoint validator accepts only ASCII https:// URLs with no user information, ports from 1 to 65535, DNS labels of at most 63 characters without punycode prefixes, a host of at most 253 characters, dotted-quad IPv4 without leading zeros, or bracketed IPv6. The same validator governs Model.key_broker_url.

7.3.4 Attestation

record_attestation(commitment, epoch, sequence, recent_slot, recent_slot_hash)

  • Signer: any relayer, which pays for the Attestation account on first use. In service the coordinator submits it after collecting committee signatures.
  • Accounts: verifier_set at ["verifiers", epoch]; operator at its seeds; platform at its seeds with has_one = operator; node at its seeds with has_one = platform, operator; runtime at ["rt", SHA-256(runtime.measurement)]; attestation created if needed at ["att", node]; instructions and slot_hashes at the exact sysvar addresses.

The handler checks, in order:

  1. Running (Paused) and healthy operator (OperatorFrozen, InsufficientBond).
  2. Platform registered (PlatformNotRegistered) and node status Registered (NodeDisabled).
  3. Every GPU hash on the node is on the platform (InvalidGpuSet).
  4. Runtime not revoked and now ≥ runtime.effective_at (RuntimeNotAllowed).
  5. The commitment's measurement equals the Runtime's and its platform equals the Platform PDA (AttestationMismatch).
  6. tcb_status = UpToDate and method = Committee (InvalidTcb).
  7. now < expires_at ≤ now + 86,400 (AttestationExpired) and, during unbonding, expires_at ≤ unbond_at (AttestationBeyondUnbond).
  8. epoch equals Config.active_verifier_epoch and the set's epoch, and the set is not retired (InvalidVerifierSet).
  9. The commitment's operator_epoch equals Operator.attestation_epoch (AttestationReplay).
  10. The record is not latched revoked (AttestationRevoked), and sequence is 1 for a new account or the stored sequence plus 1 (AttestationReplay).
  11. The pair (recent_slot, recent_slot_hash) appears in the SlotHashes sysvar, which holds up to 512 recent entries (InvalidRecentSlot).
  12. At least three distinct members of the set signed the canonical message (below).

It then writes every commitment field, sets sequence and signed_sequence to the new sequence, stores the signer bitmap and canonical_message_hash = SHA-256(message), clears the revocation fields and emits AttestationRecorded.

The committee signs a 200-byte message. Its 32-byte commitment field is the SHA-256 of the 194-byte Borsh encoding of the AttestationCommitment:

Canonical message (200 bytes)
offset  size  field
     0    16  "sealroom.att.v0\0"
    16    32  program id
    48    32  Config.genesis_hash
    80    32  Node PDA
   112     8  verifier epoch (u64 LE)
   120     8  sequence (u64 LE)
   128    32  SHA-256(AttestationCommitment)
   160     8  recent slot (u64 LE)
   168    32  recent slot hash

AttestationCommitment (194 bytes, Borsh)
offset  size  field
     0    32  platform (Platform PDA)
    32    48  runtime_measurement
    80    32  gpu_rim_hash
   112    32  evidence_hash
   144    32  enclave_key_hash
   176     1  tcb_status
   177     8  expires_at (i64 LE)
   185     1  method
   186     8  operator_epoch (u64 LE)

The message binds the program, the cluster, the node, the committee epoch, the replay sequence, the evidence and a recent slot, so a signature cannot be replayed against another deployment, another node or another revision.

Signatures arrive in native Ed25519 precompile instructions placed earlier in the same transaction; the runtime verifies them before the program executes. The program reads the Instructions sysvar, visits every Ed25519 instruction before the current one and parses each. The instruction must carry no accounts, a signature count from 1 to 7 and a zero padding byte; each 14-byte offsets entry must reference data inside that same instruction (all three instruction indices equal to 0xFFFF), and each referenced signature, key and message must lie within bounds (InvalidSignatureInstruction). The message must be exactly the 200 canonical bytes (WrongAttestationMessage), the key must be a member of the set (UnknownVerifier), and no member may count twice, including across separate Ed25519 instructions (DuplicateVerifier). Fewer than three distinct members raises InsufficientSignatures. The set itself must still be five distinct keys at threshold three (InvalidVerifierSet).

revoke_attestation(reason_hash)

  • Signer: the admin, or a member of the VerifierSet whose epoch is the active epoch (Unauthorized).
  • Effects: if the record is not already revoked, sequence += 1, revoked_at = now, revoked_by = signer, revoke_reason_hash = reason_hash, and AttestationRevoked is emitted. A second revocation is a no-op. signed_sequence is unchanged.

clear_attestation_revocation()

  • Signer: admin or arbitration (Unauthorized). The record must be revoked (AttestationNotRevoked).
  • Effects: clears the three revocation fields and sets expires_at = now, so the record is expired at once. The sequence keeps the value revocation consumed, and a node returns to service only through a new committee record at the next sequence.
  • Event: AttestationRevocationCleared.

A worked sequence: a record at sequence 10 has signed_sequence = 10. Revocation moves sequence to 11. A renewal signed for 11 before the revocation fails with AttestationRevoked while the latch is set, and with AttestationReplay after a clear, because the next acceptable sequence is then 12. Arbitration invalidates every attestation of an operator at once by advancing Operator.attestation_epoch: existing records carry the old epoch, the key broker and callers treat an epoch mismatch as revocation, and new records must commit to the new epoch.

7.3.5 Runtimes

allow_runtime(measurement, image_name, image_version, source_commit)

  • Signer: admin, who pays rent. Accounts: runtime created if needed at ["rt", SHA-256(measurement)].
  • Checks: admin check; image name 1 to 64 bytes, version at most 32 bytes and a source commit of exactly 40 lowercase hexadecimal characters (BoundsExceeded); the Runtime is new or revoked (RuntimeAlreadyAllowed).
  • Effects: writes the metadata, proposed_at = now, effective_at = now + 172,800, revoked = false. Re-allowing a revoked runtime starts a full new delay.
  • Event: RuntimeProposed.

revoke_runtime() — Signer: admin. Sets revoked = true and revoked_at = now with immediate effect: no new attestation for that measurement can be recorded. Event: RuntimeRevoked.

7.3.6 Models and splits

create_model(id, manifest_hash, key_broker, key_broker_url, allowed_runtimes, min_tier, min_bond, operator_allowlist, price, derivative_royalty_bps, base_model, split)

  • Signer: owner, who pays rent. Accounts: model created at ["model", owner, id]; base_model optional; one remaining account for each split recipient ATA, including a derivative's royalty recipient.
  • Running check (Paused).
  • Split checks: 1 to 8 entries, each with positive weight and a distinct, non-default recipient ATA, summing to 10,000 bps (InvalidSplit); exactly one entry with role Operator (InvalidOperatorPayout); that entry is not locked (OperatorLockUnsupported); every recipient's account is present, owned by SPL Token, of the Config mint, owned by recipient_owner, and is that owner's canonical ATA, with a non-default owner (InvalidRecipients).
  • Policy checks: 1 to 8 distinct allowed runtimes and at most 16 distinct, non-default allowlisted operators (BoundsExceeded); min_tier from 1 to 3 and royalty ≤ 10,000 bps (InvalidBps); non-default key broker key and a valid HTTPS broker URL of at most 128 bytes (InvalidEndpoint); positive per_call or max_per_call (InvalidAmount).
  • Base model: when base_model is named, the matching Model account must be supplied (InvalidBaseModel); if its derivative_royalty_bps is positive, the split must contain a locked BaseModel entry with at least that weight, paid to the base owner's canonical ATA (RoyaltyRequired). With no base model, no base account may be supplied (InvalidBaseModel).
  • Effects: writes the Model with split_revision = 1 and no pending split.
  • Event: ModelCreated.

The Operator-role entry declares a weight only. At distribution its destination is always the serving operator's payout_ata with the operator authority as beneficiary, whatever address the model stored, so one model served by several operators pays each operator on its own lanes.

propose_split(entries)

  • Signer: owner, with has_one = owner on the Model; locked-entry beneficiaries co-sign as remaining accounts when needed.
  • Checks: running; the split and recipient-account checks of create_model; no pending split (PendingSplitExists). Every locked entry of the current split must either survive unchanged in recipient, owner and role, still locked and with weight not lower, or its recipient_owner must sign the transaction (LockedSplit).
  • Effects: pending_split = { revision: split_revision + 1, proposed_at: now, effective_at: now + 604,800, entries }.
  • Event: SplitProposed.

cancel_split() — Signer: owner. Removes the pending split (NoPendingSplit). Available while paused. Event: SplitCancelled.

apply_split() — No privileged signer. Running check; a pending split exists (NoPendingSplit) and its effective_at has passed (TimelockPending). Sets split_revision and split from the proposal. Event: SplitApplied. Lanes adopt the new revision one at a time at their next settlement boundary (7.3.7), so revenue already in a vault settles under the split it accrued under.

7.3.7 Lanes and distribution

open_lane()

  • Signer: any payer. Accounts: Model and Node at their seeds, Node with has_one = operator, Operator at its seeds; lane created at ["lane", model, node]; vault created if needed as the canonical ATA of the Lane PDA for USDC, which also accepts a vault created in advance by another party.
  • Checks: running.
  • Effects: a Lane with zero counters and a snapshot of the model's current split and revision. Opening a lane grants no key eligibility; the key broker applies the model's policy separately.
  • Event: LaneOpened.

distribute()

  • No privileged signer. Accounts: Config, Model, Node (has_one = operator), Operator; Lane with has_one = model, node and vault_ata == vault; vault, the Lane's canonical ATA; fee_treasury, whose address must equal Config.fee_treasury_ata and differ from the vault; fee_claim, the Claim PDA for the treasury; then one pair of remaining accounts (recipient ATA, Claim PDA) per split entry, in split order.

The handler proceeds as follows:

  1. Running (Paused) and claim policy set (ConfigurationUnset).
  2. Resolve the lane's active split, replacing the Operator-role entry's destination with Operator.payout_ata and Operator.authority.
  3. Compute available = vault.amount − reserved_amount (InvalidAmount if negative).
  4. If available = 0, the call is a settlement boundary: when the model's revision differs from the lane's and the vault holds only reserved funds, copy the model split into the lane and emit LaneSplitActivated; then return.
  5. Require two remaining accounts per entry (InvalidRecipients), authenticate the fee Claim (InvalidClaim), and read the fee rate and treasury owner (ConfigurationUnset).
  6. Compute the allocation (formula below).
  7. Before any transfer, validate every pair: the ATA equals the resolved recipient, is writable and is not the vault (InvalidRecipients); the Claim slot is either an authenticated Claim for this lane, recipient and beneficiary or an empty System-owned placeholder at the right PDA (InvalidClaim); a positive share whose account cannot receive it requires an initialized Claim (InvalidClaim). The fee is held to the same rule.
  8. Pay the fee and each share. An account is payable when it is an SPL Token account of the Config mint, owned by the beneficiary and in the Initialized state. A payable recipient receives a transfer_checked signed by the Lane PDA. An unpayable share (frozen, closed or owner-changed account) is added to its Claim and to reserved_amount, and the other payouts proceed.
  9. Assert that paid plus deferred equals available (Overflow).
  10. Update the lane: total_allocated += available, total_distributed += paid, total_in = total_allocated, reserved_amount += deferred, last_distributed = now, and add the fee to eligible_protocol_fees_paid when it was paid directly to a treasury whose owner is neither the model owner nor the operator authority.
  11. Emit LaneDistributed with the fee paid, allocated and deferred, the paid and deferred totals, the fee rate, the treasury, the hash of the active split, its revision and one payout record per entry.
  12. Reload the vault and run the boundary step of 4. Because the lane and its vault are write-locked by the instruction, a payment lands wholly before or wholly after the boundary.
available = vault.amount - lane.reserved_amount fee = floor(available * protocol_fee_bps / 10000) net = available - fee share_i = floor(net * bps_i / 10000) for every entry except the last share_last = net - (sum of all other shares) fee + sum(share_i) = available

A worked example at the launch fee of 500 bps, with an illustrative split of 7,000 bps to the owner, 2,000 to the operator and 1,000 to a contributor listed last: an available balance of 1,234,567 base units gives a fee of floor(61,728.35) = 61,728 and a net of 1,172,839. The owner receives floor(820,987.3) = 820,987 and the operator floor(234,567.8) = 234,567. The contributor's floor is 117,283, and as the final entry it also receives the 2 base units of rounding remainder, for 117,285. Fee and shares sum to exactly 1,234,567. With no fee and eight equal entries of 1,250 bps, 11 base units divide as 1, 1, 1, 1, 1, 1, 1 and 4.

rescue_unopened_lane() — Retained in the interface. It always fails with RescueDisabled, whoever signs, and never moves or closes a vault. Funds sent to a lane vault before its lane exists are settled by open_lane and distribute like any other revenue, under the model's split and fee.

7.3.8 Claims

prepare_claim(recipient_ata, recipient_owner)

  • Signer: any payer. Accounts: Model, Lane (has_one = model, node), Node (has_one = operator), Operator; claim created if needed at ["claim", lane, recipient_ata].
  • Checks: claim policy set (ConfigurationUnset); the pair is an entry of the lane's resolved split or the configured treasury and treasury owner (InvalidClaim); an existing Claim must match lane, recipient and beneficiary (InvalidClaim).
  • Effects: a zero-amount Claim bound to the beneficiary; an existing matching Claim is left unchanged. The keeper prepares Claims only for recipients whose positive share cannot be paid directly. No event.

claim_deferred()

  • Signer: the Claim's beneficiary. Accounts: Lane; Claim at ["claim", lane, claim.recipient_ata] with has_one = lane, beneficiary; vault at lane.vault_ata with token authority the Lane PDA; destination, a USDC account owned by the beneficiary and different from the vault.
  • Checks: the destination is owned by the beneficiary and of the mint, and under CanonicalAta it is the beneficiary's canonical ATA (InvalidClaimDestination); claim policy set (ConfigurationUnset); reserved_amount ≥ amount (InvalidClaim). A zero amount returns without effect.
  • Effects: transfers the amount, signed by the Lane PDA; adds the Claim's eligible fee portion to eligible_protocol_fees_paid; zeroes the Claim; reduces reserved_amount; increases total_distributed. Claims are not gated by the pause flag.
  • Event: DeferredClaimPaid.

7.3.9 Disputes and arbitration

open_dispute(sequence, evidence_hash)

  • Signer: the claimant, who must own the model (model derives from the claimant's key and model.owner == claimant, else Unauthorized) and pays rent.
  • Accounts: Operator at its seeds; Node with has_one = operator; Lane with has_one = model, node; claimant_disputes created if needed at ["claimant_disputes", claimant, operator]; dispute created at ["dispute", operator, sequence]; source, a USDC account of the claimant; deposit_vault created if needed as the Dispute PDA's ATA.
  • Checks: deposit, arbitration and deposit policy configured and the deposit positive (ConfigurationUnset); sequence == Operator.next_dispute_sequence (AttestationReplay); policy ForfeitOnDismissal (ConfigurationUnset); the lane's unused credit covers the deposit (InsufficientPaidUsage); the per-claimant cap is configured (ConfigurationUnset); the operator is below max_open_per_operator and the claimant below its cap (DisputeLimitReached).
credit = lane.eligible_protocol_fees_paid - lane.admission_credit_consumed admitted only if credit >= config.dispute_deposit lane.admission_credit_consumed += config.dispute_deposit
  • Effects: consumes the credit, increments the claimant counter, transfers the deposit into the deposit vault, increments next_dispute_sequence and open_dispute_count, and writes an Open Dispute that snapshots the deposit, the lane and node, the forfeiture destination and beneficiary, and admission_credit_used = admission_required_amount = deposit. Consumed credit never returns, whatever the ruling. At the launch fee of 500 bps, the eligible fees behind one deposit correspond to directly paid lane revenue of 20 times the deposit.
  • Events: DisputeAdmission (credit used and remaining), then DisputeOpened.

rule_dispute(for_claimant, slash_amount, freeze, revoke_attestations, ruling_hash)

  • Signer: Config.arbitration (Unauthorized). A proposed but unapplied arbitration key has no authority.
  • Accounts: Operator with has_one = bond_vault; Dispute with has_one = operator, deposit_vault; ClaimantDisputes matching the Dispute's claimant and operator; the bond vault and deposit vault.
  • Checks: the dispute is Open (DisputeAlreadyRuled); slash_amount ≤ bond_amount, and a dismissal carries no slash, freeze or revocation (InvalidAmount).
  • Effects: bond_amount −= slash_amount, pending_slash_amount += slash_amount; frozen = true if ordered; attestation_epoch += 1 if ordered; open_dispute_count and the claimant counter each decrease by one. A claimant ruling sets claimable_amount = deposit; a dismissal sets forfeited_amount = deposit. slash_pending_amount records the slash. The ruling performs no token transfer, so a frozen token account cannot block it.
  • Event: DisputeRuled, which records the ruling authority, the deposit returned or forfeited, the forfeiture destination, the freeze state and the new attestation epoch.

arbitrate_operator(sequence, evidence_hash, slash_amount, freeze, revoke_attestations, ruling_hash)

  • Signer: Config.arbitration (Unauthorized), which pays rent.
  • Accounts: Operator; claimant, a named wallet; optional model, node and lane; ClaimantDisputes and Dispute created as in open_dispute; the deposit vault created if needed.
  • Relationship: either all three of model, node and lane are supplied, or none. When supplied, each must be the canonical PDA for its own fields, the node must belong to this operator, the lane must link that model and node, and the model's owner must be the claimant (Unauthorized on any mismatch or a partial set).
  • Admission: a zero slash needs nothing more. A positive slash requires a configured, positive deposit (ConfigurationUnset) and a lane whose lifetime eligible_protocol_fees_paid is at least that deposit (InsufficientPaidUsage); no credit is consumed.
  • Checks: sequence == next_dispute_sequence (AttestationReplay); slash_amount ≤ bond_amount (InvalidAmount).
  • Effects: advances the sequence; reserves the slash as in rule_dispute; applies freeze and revocation; writes a Dispute already RuledForClaimant with zero deposit, arbitration_initiated = true, admission_credit_used = 0 and admission_required_amount equal to the threshold applied. It takes no open-dispute slot, so a security action is available even when every slot is occupied.
  • Event: ArbitrationAction.

settle_slash()

  • Signer: any; the account in the arbitration position is not compared with Config.arbitration, so settlement is permissionless. Accounts as in rule_dispute.
  • Checks: the dispute is no longer Open (DisputeAlreadyRuled). A zero pending slash returns without effect.
  • Effects: transfers slash_pending_amount from the bond vault to the deposit vault, signed by the Operator PDA; claimable_amount += amount; pending_slash_amount −= amount; slash_pending_amount = 0.
  • Event: SlashSettled.

claim_dispute() — Signer: the claimant (has_one = claimant, deposit_vault). The destination must satisfy the destination policy (InvalidClaimDestination) and the dispute must be ruled (DisputeAlreadyRuled). Transfers claimable_amount from the deposit vault, signed by the Dispute PDA, and zeroes it. Event: DisputeClaimPaid.

claim_forfeited_deposit() — Signer: the Dispute's forfeiture_beneficiary (Unauthorized). The dispute must be Dismissed (DisputeAlreadyRuled) and the destination must satisfy the policy (InvalidClaimDestination). Transfers forfeited_amount to the beneficiary and zeroes it. Event: DisputeDepositForfeited.

The dispute amounts move through these states:

Stateclaimable_amountforfeited_amountslash_pending_amount
Open000
Ruled for claimant by rule_disputedeposit0slash
Dismissed by rule_dispute0deposit0
Recorded by arbitrate_operator00slash
After settle_slashprevious + slashunchanged0

7.3.10 Events

EventEmitted by
ConfigInitializedinitialize (program log)
OperatorRegistered, OperatorTierSet, OperatorFrozenregister_operator, set_tier, freeze_operator
BondDeposited, UnbondRequested, UnbondCancelled, BondWithdrawndeposit_bond, request_unbond, cancel_unbond, withdraw_bond
PlatformRegistered, NodeRegisteredregister_platform, register_node
RuntimeProposed, RuntimeRevokedallow_runtime, revoke_runtime
AttestationRecorded, AttestationRevoked, AttestationRevocationClearedrecord_attestation, revoke_attestation, clear_attestation_revocation
ModelCreated, SplitProposed, SplitCancelled, SplitAppliedcreate_model, propose_split, cancel_split, apply_split
LaneOpened, LaneDistributed, LaneSplitActivatedopen_lane, distribute
DeferredClaimPaidclaim_deferred
FeeProposed, FeeAppliedpropose_fee, apply_fee
DisputeConfigProposed, DisputeConfigAppliedpropose_dispute_config, apply_dispute_config
DisputeAdmission, DisputeOpenedopen_dispute
DisputeRuled, ArbitrationAction, SlashSettledrule_dispute, arbitrate_operator, settle_slash
DisputeClaimPaid, DisputeDepositForfeitedclaim_dispute, claim_forfeited_deposit
UnopenedLaneRescuedRetained in the IDL; no instruction emits it

set_paused, set_settlement_policy, cancel_fee, cancel_dispute_config and prepare_claim emit no event; their effects are read from the accounts they change.

7.4 Timelocks and governance actions

Four durations are compiled into the program as constants and copied into Config for readers:

ConstantSecondsDurationApplies to
ATTESTATION_TTL86,40024 hoursMaximum lifetime of an attestation record
RUNTIME_TIMELOCK172,80048 hoursallow_runtime, propose_fee, propose_dispute_config
SPLIT_DELAY604,8007 dayspropose_split
UNBONDING_PERIOD2,592,00030 daysrequest_unbond

A proposal records effective_at = now + delay using the cluster Clock's Unix timestamp, and its application succeeds in any later transaction whose Clock timestamp is at least effective_at. A fee proposed at Unix time T can be applied from T + 172,800 onward; a split proposed at T can be applied from T + 604,800; an unbond requested at T can be withdrawn from T + 2,592,000.

ChangeProposed byDelayApplied byCancelled byPending slots
Protocol fee and treasuryAdmin, propose_fee48 hoursAnyone, apply_feeAdmin, cancel_feeOne
Arbitration, deposit, caps, forfeiture recipientAdmin, propose_dispute_config48 hoursAnyone, apply_dispute_configAdmin, cancel_dispute_configOne
Runtime allowlist entryAdmin, allow_runtime48 hours before attestations may use itTakes effect at effective_atAdmin, revoke_runtimeOne per measurement
Model splitOwner, propose_split7 daysAnyone, apply_split, then each lane at its boundaryOwner, cancel_splitOne per model
Bond withdrawalOperator, request_unbond30 daysOperator, withdraw_bondOperator, cancel_unbondOne per operator

The arbitration authority changes only through the dispute-configuration path, so a newly proposed arbitrator waits 48 hours, is visible on chain throughout, and cannot rule until the change applies. Combined with the separation checks, the admin can neither appoint itself arbitrator nor pair a settings change with an immediate ruling.

These actions take effect in the transaction that carries them:

ActionAuthorityEffect
revoke_runtimeAdminNo new attestation for the measurement
revoke_attestationAdmin or active verifierRecord latched revoked, sequence advanced
freeze_operatorAdminOperator blocked from deposits, unbonding, registration and attestation
rule_dispute, arbitrate_operatorArbitrationSlash reserved, operator frozen and attestation epoch advanced as ordered
set_pausedAdminGated instructions stop
set_tier, set_settlement_policyAdminTier or claim policy changes

The pause flag stops the instructions that create registry entries or allocate revenue: register_operator, deposit_bond, register_platform, register_node, record_attestation, create_model, propose_split, apply_split, open_lane and distribute. All other instructions run while paused, including unbonding and withdrawal, cancellations, revocations, freezes, governance proposals and applications, claim preparation and deferred claims, dispute admission, rulings, arbitration actions, slash settlement and dispute claims.

7.5 Errors

The program defines 54 error codes, numbered from 6000 in Anchor's custom-error range:

CodeNameRaised when
6000UnauthorizedSigner is not the required authority (admin; admin or active verifier; admin or arbitration; arbitration; upgrade authority at initialize; model owner as claimant; forfeiture beneficiary), an arbitration relationship is partial or mismatched, a counter belongs to another pair, or a program write targets a non-writable or foreign account
6001PausedA gated instruction runs while Config.paused is set
6002ConfigurationUnsetA required setting is unset: tier minima count, unknown tier, claim policy, fee rate or treasury owner, dispute deposit, arbitration, deposit policy or cap; invalid dispute proposal values
6003OverflowChecked arithmetic overflows or underflows, or distribution conservation fails
6004InvalidBpsFee above 10,000 bps; model min_tier outside 1 to 3 or royalty above 10,000 bps
6005InvalidTierTier minima at initialize are not tiers 1, 2, 3 in order
6006InvalidVerifierSetVerifiers are not five distinct non-default keys at threshold three; attestation epoch differs from the active epoch or the set is retired
6007InvalidMintThe mint at initialize does not have six decimals
6008InvalidAmountZero bond deposit or zero-bond unbond request; vault below recorded bond; vault below reserved amount; zero price; slash above bond or a dismissal with sanctions; credit underflow
6009OperatorFrozenThe operator is frozen
6010InsufficientBondThe bond is below the tier minimum
6011UnbondingA deposit or a second request while unbonding
6012OpenDisputeUnbonding or withdrawal with an open dispute
6013UnbondNotRequestedCancellation or withdrawal without a request
6014TimelockPendingWithdrawal before unbond_at, or an apply before effective_at
6015InvalidGpuSetGPU list empty, above eight, duplicated, off-platform or without matching accounts; malformed GpuPin
6016PlatformNotRegisteredThe platform is not registered
6017InvalidEndpointNode endpoint or key broker URL is not a valid HTTPS URL within 128 bytes, or the key broker key is default
6018BoundsExceededRuntime metadata out of bounds; allowed runtimes not 1 to 8 distinct; allowlist above 16 or not distinct
6019RuntimeAlreadyAllowedallow_runtime on a runtime that is allowed or pending
6020RuntimeNotAllowedRuntime revoked or before effective_at
6021NodeDisabledNode status is not Registered
6022AttestationMismatchCommitment measurement or platform differs from the accounts
6023InvalidTcbTCB status not UpToDate or method not Committee
6024AttestationExpiredExpiry not in the future or beyond 24 hours
6025AttestationReplayAttestation sequence or operator epoch wrong; dispute sequence not the next one
6026InvalidRecentSlotSlot and hash pair absent from SlotHashes
6027InvalidSignatureInstructionMalformed Ed25519 precompile instruction or offsets
6028WrongAttestationMessageA verifier signed bytes other than the canonical message
6029UnknownVerifierSigning key is not a member of the set
6030DuplicateVerifierA member's signature appears more than once
6031InsufficientSignaturesFewer than three distinct members signed
6032InvalidSplitSplit not 1 to 8 entries with positive weights, distinct non-default ATAs and a 10,000-bps total
6033InvalidOperatorPayoutSplit does not contain exactly one Operator-role entry
6034InvalidBaseModelBase model account missing, mismatched or supplied without a reference
6035RoyaltyRequiredDerivative split lacks the locked royalty to the base owner's canonical ATA
6036PendingSplitExistsA split, fee or dispute-configuration proposal is already pending
6037LockedSplitA locked entry is reduced, removed or unlocked without its beneficiary's signature
6038NoPendingSplitNo split, fee or dispute-configuration proposal to cancel or apply
6039InvalidRecipientsRecipient account missing, of the wrong program, mint or owner, or not canonical; distribution accounts wrong in count, order or writability; treasury or forfeiture account not canonical
6040AuthoritiesMustDifferArbitration equals the admin or the default key, or the forfeiture owner equals arbitration
6041GovernanceTimelockRequiredset_dispute_config is called
6042DisputeLimitReachedThe operator-wide or per-claimant open-dispute cap is reached
6043RescueDisabledrescue_unopened_lane is called
6044InvalidClaimDestinationClaim destination not owned by the beneficiary, of another mint, or not canonical under CanonicalAta
6045AttestationRevokedRecording while the revocation latch is set
6046AttestationNotRevokedClearing a record that is not revoked
6047GpuAlreadyClaimedA GpuPin already exists, or the GPU belongs to another platform or node
6048InvalidClaimClaim PDA or contents mismatch; recipient not in the lane split or treasury; unpayable positive share without a Claim; reserve below claim
6049DisputeAlreadyRuledRuling a dispute that is not open; settling or claiming one that is open; claiming a forfeiture of one not dismissed
6050AttestationBeyondUnbondExpiry later than the operator's unbond deadline
6051OperatorLockUnsupportedAn Operator-role split entry is marked locked
6052LaneSplitBalanceNotEmptySplit activation while unallocated revenue remains in the vault
6053InsufficientPaidUsageUnused eligible paid fees on the lane are below the dispute deposit

7.6 Upgrade authority and program deployment model

The program is deployed through Solana's upgradeable BPF loader. Its executable lives in a ProgramData account that records the upgrade authority, the only key that can replace the executable.

At launch the upgrade authority is the deployer, and the program binds its own initialization to that key. The initialize accounts require program to be the sealroom program whose ProgramData address is the supplied program_data, and program_data.upgrade_authority_address to equal the admin signer. Because Config sits at the fixed seed config and is created with init, initialization happens exactly once, and only the holder of the upgrade authority can perform it. A party watching the deployment cannot initialize first with its own mint, treasury or verifier set. The launch transaction writes the configuration described in 7.3.2: the USDC mint, the genesis hash, the protocol fee of 500 bps and its canonical treasury ATA, the three tier bond minima, the five-member epoch-0 committee at threshold three, the optional arbitration key, and the signer as Config.admin.

After launch the upgrade authority passes to governance: the loader's authority-transfer instruction moves it from the deployer to the governance multisig, a Squads multisig operated with timelocks, and from then on a new executable can be written only with that multisig's approval. A Squads authority signs program instructions through the Squads program, so the same signing path serves every governance role. Arbitration is a separate named multisig, supplied at initialization or installed through the 48-hour dispute-configuration path, and each role's powers inside the program are exactly those listed in 7.3 and 7.4.

Upgrades keep the wire format. The upgrade procedure compares the instruction arguments, the account fields and their order, and the signer requirements of the new build with the deployed interface, and refuses a build that changes them. Retired behavior stays in the interface with fixed results instead of being removed: set_dispute_config and rescue_unopened_lane remain callable and always fail with their own error codes, rescue_policy and split_settlement_policy remain in Config and always hold None, and the UnopenedLaneRescued event type remains decodable. Every client, indexer and historical transaction therefore keeps decoding against the current IDL. Each account carries a version byte, 1 for every account in this release, as the marker for any future layout.

The program identity is part of what verifiers sign. Bytes 16 to 47 of the canonical attestation message carry the program id and bytes 48 to 79 the genesis hash stored in Config, so committee signatures produced for this deployment are valid nowhere else.

7.7 Transaction sizes and compute

Legacy and v0 Solana transactions are limited to 1,232 bytes on the wire, and every transaction to 1.4 million compute units. Two of the program's transactions carry enough data that their size is a design input: record_attestation, with its committee signatures, and distribute, with its per-recipient accounts.

Attestation. A committee attestation is one transaction with two instructions. The first is a single native Ed25519 instruction carrying all three signatures over one shared copy of the 200-byte message; the second is record_attestation:

ComponentBytes
Ed25519 header (count and padding)2
Three offsets entries of 14 bytes42
Three public keys and signatures of 96 bytes288
Shared canonical message200
Ed25519 instruction data532
record_attestation discriminator8
AttestationCommitment194
Epoch, sequence and recent slot24
Recent slot hash32
record_attestation instruction data258

record_attestation names thirteen accounts, and the transaction also references the Ed25519 program. In a legacy message every one of those addresses costs 32 bytes, and the complete transaction is 1,361 bytes, over the 1,232-byte limit. The coordinator therefore sends it as a v0 transaction with an address lookup table that holds the instruction's fixed accounts: Config, the VerifierSet, the Operator, Platform, Node, Runtime and Attestation accounts, the Instructions and SlotHashes sysvars and the System Program. Each looked-up account costs a one-byte index. The fee payer and the invoked programs stay in the static key list.

Distribution. distribute takes no arguments, so its instruction data is the eight-byte discriminator, but it names twelve accounts plus two per split entry: 28 accounts for eight recipients. It performs one transfer_checked CPI for the fee and one for each payable share, plus the self-CPIs that record its events. Healthy payouts and zero-amount shares use empty placeholder addresses in their Claim slots, so a healthy distribution allocates no Claim rent. With the distribution accounts in a lookup table, the eight-recipient transaction is 268 bytes.

Measured on the compiled program:

TransactionFormatBytesCompute units
record_attestation, three signaturesv0 + lookup table1,05649,314
record_attestation as sent by the coordinatorv0 + lookup table1,08746,465 to 46,505
record_attestation, three signaturesLegacy1,361Exceeds the 1,232-byte limit
distribute, fee and eight recipients, all paidv0 + lookup table268165,610
distribute, fee and eight recipients, final share frozen and reservedv0 + lookup table268166,615
distribute, two recipients, no Claim rentv0 + lookup table46174,150

Compute varies slightly with the addresses involved, because each PDA derivation searches for its bump; the coordinator figure is the range observed across submissions. Reserving a frozen recipient's share in place of transferring it adds about a thousand units, and the costliest measured transaction, distribution to the fee and eight recipients, stays far below the 1.4-million-unit transaction limit.

Transaction builders follow the same budget. They place idempotent ATA-creation instructions in the same transaction as create_model or propose_split when the packet allows. Splits whose eight recipient ATAs are all new use a configured lookup table or a separate account-setup transaction, and a preview that would exceed 1,232 bytes is returned to the wallet as blocked instead of as a transaction it cannot submit. The keeper and the API load the deployment's configured lookup tables before building, so the attestation and distribution transactions above are the forms the service submits.

8. Payments over x402

A call on Sealroom is an HTTPS request to a node's registered endpoint, paid in USDC over x402 version 2. The caller needs no account with the operator, the owner or Sealroom; a Solana wallet holding USDC is enough, so AI agents can pay. Every payment for a model served by a given node lands in one token account, the vault of the lane for that (model, node) pair. No Sealroom instruction runs per call: the split between protocol, operator and contributors happens later, in one distribute per lane (chapter 9).

The caller signs a USDC transfer for the quoted amount, filling only its own signature slot. The node runtime quotes the price, has the payment verified, runs inference inside the enclave and has the payment settled; it holds no facilitator or channel custody key. The facilitator issues the requirements, verifies the signed transaction, pays the network fee as fee payer, broadcasts, confirms and settles usage vouchers.

8.1 Parties and endpoints

PartyEndpointPurpose
Node runtimePOST /v1/chat/completions at Node.endpoint (HTTPS, at most 128 bytes)Answers 402 without payment; serves and settles with payment
FacilitatorPOST /requirementsCreates a payment session bound to the request and returns the x402 requirements
FacilitatorPOST /verifyChecks the signed payment against the session without moving funds
FacilitatorPOST /settleAdds the fee-payer signature, broadcasts, confirms and checks the token balance change
FacilitatorPOST /settle-voucherSettles an upto session against an enclave-signed usage voucher and refunds the unused ceiling
FacilitatorPOST /reconcileRefunds expired upto ceilings and reconciles ambiguous payment intents; the keeper calls it

All write endpoints are rate-limited per client address and answer 429 with Retry-After when the limit is reached. Browser callers are admitted only from exact configured origins.

8.2 Reading the attestation before the call

Before it sends a prompt, the caller establishes from Solana that the node is attested and that the TLS session it is about to open terminates inside the attested enclave. The node's runtime generated three key pairs inside the CVM at boot: an HPKE key pair (X-Wing: ML-KEM-768 plus X25519), a TLS key pair and an Ed25519 voucher key pair. The hash of their public keys is the enclave_key_hash that three verifiers signed into the node's Attestation account.

transcript = hpke_public_key (1216 bytes) || tls_public_key (32 bytes) || voucher_public_key (32 bytes) enclave_key_hash = SHA-256(transcript) tls_key_hash = SHA-256(tls_public_key) report_data = SHA-256(transcript || recent_slot_hash)

The caller's SDK performs these steps:

  1. Derive the accounts: Model at ["model", owner, id:u64], Node at ["node", platform, index:u16], Attestation at ["att", node], Lane at ["lane", model, node].
  2. Read the Attestation and check that it is current: revoked_at is empty, expires_at is in the future (the program never accepts an expiry more than 86,400 seconds after recording), tcb_status is UpToDate, and operator_epoch equals the operator's current attestation_epoch.
  3. Check the model's policy against the node: the attestation's 48-byte runtime_measurement is on the model's allowed_runtimes and its Runtime account is active; the platform is registered to the node's operator; the operator is not frozen, its tier is at least min_tier, its bond is at least the larger of min_bond and its tier minimum, and it is on the model's operator_allowlist when that list is non-empty; the node is not disabled; the protocol is not paused. The registry API serves the same rules at /v1/eligibility?modelId=…&nodeId=… with one reason code per failed check, such as attestation_revoked, runtime_not_allowed or operator_frozen.
  4. Obtain the node's public-key transcript, recompute enclave_key_hash and compare it with the 32 bytes stored in the Attestation account. The registry publishes tlsPublicKeyHash and voucherPublicKey for an attestation only after this recomputation succeeds against the chain value.
  5. Open TLS to Node.endpoint, hash the server's 32-byte TLS public key and compare it with tls_key_hash. On a mismatch the caller aborts before sending the prompt.
  6. Keep the voucher public key. For upto calls it is the only key whose usage vouchers the caller accepts.
  7. Read the model's price and the lane's vault_ata.

The on-chain price is a Borsh enum in the Model account:

VariantFieldsValidation at create_model
Exactper_call: u64per_call > 0
Uptoinput_per_million_tokens: u64, output_per_million_tokens: u64, max_per_call: u64max_per_call > 0

All amounts are USDC base units with 6 decimals, so 1,000,000 base units are 1 USDC.

8.3 The request and the 402 challenge

The caller posts the call to the node with a caller-chosen requestId (a UUID that serves as idempotency key), the modelId, nodeId and laneId read in 8.2, the paying wallet as caller, the scheme (exact or upto), for upto a ceiling in base units, and the prompt.

The node hashes the prompt, bodyHash = SHA-256(prompt), and forwards everything except the prompt, together with bodyHash, to the facilitator's /requirements. The facilitator then:

  1. Confirms that its RPC endpoint reports the configured genesis hash.

  2. Reads the registry accounts from chain and runs the eligibility rules. An ineligible node fails with NODE_INELIGIBLE: followed by the reason codes.

  3. Reads the Lane and checks that lane.model and lane.node are the requested model and node (LANE_IDENTITY_MISMATCH).

  4. Computes the amount from the scheme and the on-chain price:

    exact: amount = per_call upto, Upto-priced model: 1 <= ceiling <= max_per_call and ceiling <= facilitator_max_ceiling upto, Exact-priced model: per_call <= ceiling <= facilitator_max_ceiling

    A metered (Upto) model cannot be bought with exact; the request fails with METERED_MODEL_REQUIRES_UPTO. A ceiling outside its bounds fails with PAYMENT_CEILING_INVALID.

  5. Derives the session key that makes the call idempotent:

    request_id_hash = SHA-256(JSON([genesis_hash, requestId, modelId, nodeId, caller]))

    If a session with this hash exists, the facilitator returns its stored requirements unchanged, provided the scheme, amount, bodyHash and lane are the same; otherwise it answers IDEMPOTENCY_CONFLICT. A retried request therefore gets the same invoice and can never be charged twice.

  6. Sets the session expiry to the chain clock plus 120 seconds and takes a recent blockhash.

  7. Stores the session with status offered, the requirements, the attestation account and its signed revision, and, for upto, the voucher public key read from the attested evidence.

The requirements object is the x402 PaymentRequirements record:

FieldValue
schemeexact or upto
networksolana: followed by the first 32 characters of the cluster's genesis hash
assetThe USDC mint from Config.usdc_mint
payToThe lane vault: Lane.vault_ata, the USDC token account owned by the lane PDA
amountBase units: per_call for exact, the ceiling for upto
maxTimeoutSeconds120
extra.feePayerThe facilitator's fee-payer public key
extra.memosealroom:<sessionId>
extra.recentBlockhash, extra.lastValidBlockHeightThe blockhash the payment must use and its validity bound
extra.sessionIdUUID of the payment session
extra.laneIdThe Lane account
extra.channelId, extra.escrowupto only: the payment channel and its escrow token account

payTo is the vault token account itself. A wallet must send to it directly and must not treat it as an owner address from which to derive another associated token account.

When the request carries no PAYMENT-SIGNATURE header, the node answers 402 Payment Required. The body and the PAYMENT-REQUIRED header carry the same JSON, the header as base64:

{
  "x402Version": 2,
  "resource": { "url": "https://<node endpoint>/v1/chat/completions", "mimeType": "application/json" },
  "accepts": [{
    "scheme": "exact",
    "network": "solana:<genesis hash prefix>",
    "asset": "<USDC mint>",
    "payTo": "<lane vault token account>",
    "amount": "<per_call>",
    "maxTimeoutSeconds": 120,
    "extra": {
      "feePayer": "<facilitator fee payer>",
      "memo": "sealroom:<sessionId>",
      "recentBlockhash": "<blockhash>",
      "lastValidBlockHeight": "<height>",
      "sessionId": "<sessionId>",
      "laneId": "<lane>"
    }
  }]
}

8.4 The payment transaction

The caller builds exactly one transaction shape. The facilitator rejects anything else, which keeps it compatible with the fixed instruction layout that SVM x402 facilitators validate around a single TransferChecked. The SDK function buildX402PaymentTransaction(requirements, caller) produces it.

#ProgramInstructionDataAccounts
0ComputeBudget111111111111111111111111111111SetComputeUnitLimit5 bytes: tag 0x02, u32 LE 100000none
1ComputeBudget111111111111111111111111111111SetComputeUnitPrice9 bytes: tag 0x03, u64 LE 0none
2TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DATransferChecked10 bytes: tag 0x0c, u64 LE amount, u8 6caller's USDC associated token account (writable), USDC mint, destination (writable), caller (signer)
3MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHrMemoUTF-8 sealroom:<sessionId> (45 bytes)none

The destination of instruction 2 is payTo (the lane vault) for exact and extra.escrow for upto. The message is a v0 message whose fee payer is extra.feePayer, so the caller needs no SOL. It uses extra.recentBlockhash, no address lookup tables and exactly two signatures, the facilitator's in slot 0 and the caller's in slot 1, and it serializes to between 100 and 1,232 bytes. The caller's USDC associated token account must already exist; the transaction contains no account-creation instruction. The wallet signs with signTransaction, filling only the caller's slot. It never uses sign-and-send: the facilitator broadcasts after its own checks.

The caller wraps the signed transaction in an x402 payment payload and retries the identical request with the header PAYMENT-SIGNATURE: base64(JSON(payload)). The node accepts a header of at most 8,192 characters.

{
  "x402Version": 2,
  "resource": { "url": "https://<node endpoint>/v1/chat/completions" },
  "accepted": { "...": "the requirements object from the 402, unchanged" },
  "payload": { "transaction": "<base64 of the partially signed v0 transaction>" }
}

The facilitator accepts the transaction only when every check below passes. Each failure has its own error code.

CheckRuleError
Wire100 to 1,232 bytes, canonical base64, no lookup tablesPAYMENT_WIRE_INVALID
Sponsor policyPayer is the facilitator, two required signatures, four instructions, quoted blockhashSPONSOR_POLICY_REJECTED
Fee-payer isolationNo instruction names the fee payer as program or accountFEE_PAYER_NOT_ISOLATED
Compute budgetLimit exactly 100,000 units, price exactly 0 micro-lamportsCOMPUTE_POLICY_REJECTED
MemoMemo program, no accounts, data equals extra.memoPAYMENT_MEMO_MISMATCH
TransferAmount equals the session amount, 6 decimals, USDC mint, destination as quoted, owner is the caller, source is the caller's associated token account, exactly four accountsPAYMENT_TRANSFER_MISMATCH
Caller signatureCaller is one of the two signers and its Ed25519 signature verifies over the message bytesCALLER_SIGNATURE_INVALID

Fee-payer isolation means the facilitator's signature can only pay the network fee. Nothing in the transaction can move the facilitator's own lamports or tokens.

8.5 Verify, serve, settle

Callerwallet signsNodeattested TLSFacilitatorfee payerLane vaultUSDC1 POST /chat/completions2 402 · price · payTo3 PAYMENT-SIGNATURE4 verify5 answer6 settleTransferChecked → lane vault · memo binds the request

Figure: an x402 call from the attestation read through the 402 challenge, verification, inference and settlement into the lane vault.

The node orders the paid call so that the caller is charged only for a completion that an eligible enclave has already produced, and so that a completion that has been paid for is always delivered.

  1. Challenge. The caller sends the request without a payment header and receives the 402 described in 8.3.
  2. Signed retry. The caller signs the payment transaction and repeats the identical request with PAYMENT-SIGNATURE.
  3. Binding. The node fetches the requirements for this request again (the facilitator returns the stored session) and requires the payload's accepted object to equal them field for field (PAYMENT_REQUEST_BINDING_MISMATCH). A payment signed for another request, lane or amount is rejected here.
  4. Verify. The node posts the payload to /verify. The facilitator checks x402Version is 2, the resource URL is the session's, accepted equals the stored requirements, the transaction matches any earlier submission for the session, the session is in a payable state and unexpired, the node is still eligible, and the wire passes every check in 8.4. It returns { isValid: true, payer, sessionId }. It signs nothing and moves no funds.
  5. Serving check. The node reads chain state and requires that it is still eligible and that the attestation account and signed revision are the ones its data-key release was bound to (NODE_KEY_RELEASE_STALE otherwise).
  6. Inference. The runtime executes the call inside the enclave.
  7. Second serving check. The node repeats step 5 with fresh chain state.
  8. Settle. The node posts the same payload to /settle. The facilitator validates it again, adds its fee-payer signature, and stores the signed bytes and their hash with status processing before it broadcasts. It then broadcasts, waits for confirmed commitment, and compares the destination account's pre- and post-transaction token balances in the confirmed transaction: the increase must equal the session amount exactly (PAYMENT_POST_BALANCE_MISMATCH). The session moves to settled for exact and to funded for upto.
  9. Voucher (upto only). The enclave signs a usage voucher and the node posts it to /settle-voucher (8.7).
  10. Response. The node returns 200 with the completion and a PAYMENT-RESPONSE header carrying the base64 JSON receipt { success, transaction, network, payer, sessionId }. For upto the body also carries the voucher and its signature, so the caller can check the charge against the voucher key it read in 8.2.

If the node loses eligibility at step 5 or step 7, the call ends with an error before step 8, and nothing has been broadcast. Once step 8 has confirmed, the node returns the completion it already holds, even if the attestation is renewed or revoked in the meantime.

The facilitator records each session in one of these states:

StatusMeaning
offeredRequirements issued; no payment received
processingSigned payment stored; broadcast or confirmation in progress
fundedupto: ceiling confirmed in escrow, voucher pending
settlingupto: voucher settlement transaction stored, confirmation in progress
settledexact: transfer confirmed. upto: usage paid to the lane vault and remainder refunded
refundingupto: expiry refund transaction stored, confirmation in progress
refundedupto: full ceiling returned to the caller
expiredThe session passed its expiry and no transfer landed

Every state change is a compare-and-set on the previous status and payload, and signed bytes are stored before any broadcast. A retry first looks the stored signature up in transaction history and rebroadcasts the same bytes only while their blockhash is valid. A settled session returns its stored receipt; a refunded or expired one is never accepted as proof of payment.

8.6 The exact scheme

exact is a fixed price per call. The caller's TransferChecked moves per_call straight into the lane vault, and settlement is the confirmation that the vault balance rose by exactly that amount. The vault is the USDC associated token account of the lane PDA, so only the sealroom program, signing for the lane through distribute and claim_deferred, can move funds out of it. Whatever the model's split, each call stays one transfer to one recipient; an eight-recipient split costs nothing extra per call and is paid for once per distribute.

8.7 The upto scheme

upto is metered billing under a ceiling. The caller escrows the ceiling in a payment channel of the Solana Foundation payment-channels program (CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX). After serving, the enclave signs a voucher for the actual charge, the facilitator settles that amount to the lane vault and returns the rest to the caller. The lane vault is the channel's only payee; the split stays in sealroom.

The requirements for an upto session add two fields. extra.escrow is the channel's escrow token account, and it is the destination of the caller's TransferChecked. extra.channelId identifies the channel for this session:

channel_id = SHA-256(request_id_hash_hex || session_id) (32 bytes, base58)

The caller signs the same four-instruction transaction with amount set to its ceiling. Settling it moves the ceiling into escrow, and the session becomes funded.

Usage price

The enclave counts the call's input and output tokens and prices them with integer arithmetic. The rates are USDC base units per million tokens.

raw = input_tokens * input_per_million_tokens + output_tokens * output_per_million_tokens amount = floor((raw + 999999) / 1000000) require amount <= max_per_call refund = ceiling - amount

The division rounds the whole call up once, so the charge exceeds the exact fractional price by less than one base unit. Token counts, rates and the result are checked against the u64 range, and a charge above max_per_call is refused (PAYMENT_USAGE_EXCEEDS_MODEL_MAX).

Worked example, with a sample price of 3,500,000 base units per million input tokens (3.50 USDC) and 10,000,000 per million output tokens (10 USDC), and a sample ceiling of 20,000 base units:

raw = 1235 * 3500000 + 401 * 10000000 = 4322500000 + 4010000000 = 8332500000 amount = floor((8332500000 + 999999) / 1000000) = floor(8333.499999) = 8333 refund = 20000 - 8333 = 11667

The lane vault receives 8,333 base units (0.008333 USDC) and the caller gets 11,667 back.

The usage voucher

The voucher is a JSON object signed by the enclave's Ed25519 voucher key. That key was generated inside the CVM, and its public key is one of the three inputs to the enclave_key_hash that the verifier committee signed. The operator never holds the private key, so it cannot produce a voucher for a larger charge.

FieldTypeContent
domainstringsealroom:usage:v1
laneIdpublic keyThe lane being paid
channelIdpublic keyThe session's channel
requestId32-byte hexrequest_id_hash of the session
nonceu64 string1: one cumulative voucher per session
cumulativeAmountu64 stringThe charge in base units
expiresAtISO timestampThe session expiry
attestationIdpublic keyThe node's Attestation account
attestationRevisionu64 stringThe attestation's signed_sequence at serving time
signerpublic keyThe enclave voucher public key

The signed message is the UTF-8 JSON serialization of these fields in table order, with genesisHash (the cluster genesis hash) inserted after domain. The 64-byte Ed25519 signature travels as base64.

/settle-voucher accepts the voucher only when all of these hold; otherwise it answers VOUCHER_INVALID:

  1. The session is upto, funded, and has no refund in progress.
  2. laneId, channelId and requestId equal the session's lane, channel and request hash, and nonce is 1.
  3. attestationId and attestationRevision equal the node's current attestation on chain and the revision stored when the session was offered.
  4. signer equals the voucher key in the attested evidence. The facilitator loads the evidence archived under the on-chain evidence_hash, recomputes both evidence_hash and enclave_key_hash, and requires both to match the Attestation account and the evidence's node to be this node.
  5. expiresAt is not in the past and not later than the session expiry.
  6. cumulativeAmount is at most the escrowed ceiling.
  7. The Ed25519 signature verifies over the message for signer.

The facilitator then settles in one transaction. A TransferChecked from escrow to the lane vault pays cumulativeAmount when it is positive; a second TransferChecked from escrow to the caller's USDC associated token account returns ceiling - cumulativeAmount when it is positive; a memo sealroom:settle:<sessionId> closes the instruction list. Payment and refund land together or not at all. The session stores the voucher hash: the same voucher submitted again returns the same receipt, and a different voucher for a settled session fails with VOUCHER_REPLAY.

A funded session whose expiry passes without a voucher is refunded in full. The keeper calls /reconcile, which sends the whole ceiling from escrow back to the caller with the memo sealroom:expire:<sessionId> and marks the session refunded. The same pass finishes interrupted payment and settlement transactions and marks a processing session expired when its transfer never landed.

8.8 Header reference

HeaderDirectionEncodingContent
PAYMENT-REQUIREDNode to caller, with 402base64 of JSON{ x402Version: 2, resource, accepts: [requirements] }
PAYMENT-SIGNATURECaller to node, on the retrybase64 of JSON{ x402Version: 2, resource: { url }, accepted, payload: { transaction } }
PAYMENT-RESPONSENode to caller, with 200base64 of JSON{ success, transaction, network, payer, sessionId }

For browser callers the node exposes PAYMENT-REQUIRED, PAYMENT-RESPONSE and Retry-After to scripts and accepts PAYMENT-SIGNATURE as a request header. A rejected call returns { success: false, errorReason } carrying one of the codes named in this chapter.

9. Settlement and distribution

Payments accumulate in lane vaults. Distribution turns a vault balance into payouts: the protocol fee to the treasury, the operator's share to the operator that runs the node, and every other split entry to its recipient. One permissionless instruction, distribute, does this for one lane in one transaction, and either every transfer in it succeeds or none does.

9.1 Lanes

A lane exists for each (model, node) pair that serves traffic. Anyone can open one with open_lane; the payer signs and pays rent, and the protocol must not be paused. The instruction creates the Lane account at ["lane", model, node] and the vault, the USDC associated token account whose authority is the lane PDA. If someone created that token account in advance, open_lane adopts it. The lane copies the model's current split and split revision into active_split and active_split_revision. Opening a lane gives the node no right to a data key; key release depends only on the attestation and the model's policy.

FieldTypeBytesMeaning
discriminator[u8; 8]8Anchor account type
version, bumpu8, u82Layout version and PDA bump
modelPubkey32The Model account
nodePubkey32The Node account
vault_ataPubkey32USDC token account owned by the lane PDA
total_inu648Revenue taken into allocation; equals total_allocated after every distribution
total_distributedu648USDC actually transferred out, including later claims
last_distributedOption<i64>9Time of the last distribution that allocated revenue
total_allocatedu648Revenue assigned to recipients, paid or deferred
reserved_amountu648Deferred entitlements still held in the vault
active_split_revisionu648Model split revision this lane currently pays under
active_splitVec<SplitEntry>4 + 8 × 68The split this lane currently pays under
eligible_protocol_fees_paidu648Protocol fees that reached an independent treasury (chapter 10)
admission_credit_consumedu648Part of that amount already used to open disputes

The account occupies 719 bytes. Its counters obey fixed relations at all times:

available = vault.amount - reserved_amount total_allocated = total_distributed + reserved_amount payments_received = total_distributed + vault.amount

available is revenue that has arrived but has not yet been assigned. reserved_amount is revenue assigned to recipients who could not be paid directly; it stays in the vault until they claim it and is never allocated a second time.

9.2 Splits

A model's split lists up to eight recipients. Each entry is 68 bytes:

SplitEntry (68 bytes)
  recipient_ata    Pubkey  32   canonical USDC associated token account of the recipient
  recipient_owner  Pubkey  32   the recipient wallet (beneficiary)
  bps              u16      2   weight in basis points
  locked           bool     1   reduction or removal needs the recipient's signature
  role             u8       1   0 Owner, 1 Operator, 2 BaseModel, 3 Contributor

The program accepts a split only when all of these hold:

  1. It has between one and eight entries.
  2. Every bps is positive and the weights sum to exactly 10,000.
  3. No two entries share a recipient_ata, and no recipient is the default key.
  4. Exactly one entry has the Operator role, and that entry is not locked.
  5. Each recipient_ata is passed in and checked on chain: a token account of the USDC mint, owned by recipient_owner, at the canonical associated-token address for that owner.

The Operator entry carries a weight, not a destination. At distribution the program replaces its address with the payout account of the operator that runs the lane's node: Operator.payout_ata as destination and Operator.authority as beneficiary. One model served by three operators therefore pays each operator from its own lanes, and the address the owner typed into the entry is never paid. Since that address cannot consent on behalf of every future operator, the role cannot be locked.

A locked entry protects a recipient against later changes. A new split proposal must either keep each locked entry, with the same account, owner, role, lock and at least the same bps, or carry the signature of that entry's recipient_owner. When a model is a derivative of a base model whose derivative_royalty_bps is positive, create_model requires a locked BaseModel entry paying at least that royalty to the base owner's canonical USDC account. The base owner's key broker can then release its key only to lanes whose split carries the royalty.

9.3 The distribute instruction

Lane vaultgross since last distributedistributepermissionlessProtocol treasuryprotocol_fee_bpsOperatorlane operator payoutOwnersplit entryBase modelsplit entryContributorssplit entriesrecipients that can't be paid→ Claim account → claim

Figure: one distribute transaction splits a lane's available balance into the protocol fee and the split shares, paying usable accounts and reserving the rest as claims.

distribute takes no arguments and needs no particular signer; whoever submits it pays the network fee. Its accounts are:

#AccountConstraint
0config["config"]
1model["model", owner, id]
2node["node", platform, index], has_one = operator
3operator["op", authority]
4lane (writable)["lane", model, node], has_one = model, node, vault_ata = vault
5usdc_mintConfig.usdc_mint
6vault (writable)USDC associated token account with authority lane
7fee_treasury (writable)Config.fee_treasury_ata, not the vault
8fee_claim (writable)Claim PDA ["claim", lane, fee_treasury]
9token_programSPL Token
10, 11event_authority, programSelf-CPI event emission

After them come two remaining accounts per split entry, in split order: the entry's resolved recipient token account (writable) and its Claim PDA ["claim", lane, recipient_ata]. A Claim PDA that has never been initialized is accepted as a placeholder when its recipient can be paid directly.

The instruction runs these steps:

  1. Require the protocol to be unpaused and a claim policy to be configured.
  2. Resolve the lane's active_split against the node's operator, replacing the Operator entry's destination with Operator.payout_ata and its beneficiary with Operator.authority.
  3. Compute available = vault.amount - reserved_amount. If it is zero, try to activate a newer split (9.8) and return without moving tokens or touching the counters.
  4. Require exactly two remaining accounts per entry.
  5. Compute the fee and every share from available, Config.protocol_fee_bps and the lane's active_split (9.4).
  6. Before any transfer, check every pair: the token account is the resolved destination, writable and not the vault; an initialized Claim belongs to this lane, recipient and beneficiary; and every positive share is either payable directly or backed by an initialized Claim. Check the treasury the same way.
  7. Pay the protocol fee to the treasury, or add it to the treasury's Claim if the treasury account cannot receive it.
  8. For each entry in order, transfer its share from the vault, signed by the lane PDA, or add it to the entry's Claim.
  9. Require paid + deferred = available.
  10. Update the lane: total_allocated += available, total_distributed += paid, total_in = total_allocated, reserved_amount += deferred, last_distributed = now, and eligible_protocol_fees_paid += fee when the fee was paid directly to an independent treasury.
  11. Emit LaneDistributed with the gross amount, the fee and payouts split into paid and deferred parts, the fee rate, the hash and revision of the active split, and one record per entry.
  12. Reload the vault and activate a newer model split if the lane's revision is behind (9.8).

Any unexpected token-program error aborts the whole transaction, and the lane is left exactly as before.

9.4 Distribution arithmetic

All arithmetic is integer: products are formed in u128 and every counter update is a checked u64 operation.

A = vault.amount - reserved_amount fee = floor(A * protocol_fee_bps / 10000) net = A - fee share_i = floor(net * bps_i / 10000) for i = 1 .. n share_n = share_n + (net - (share_1 + ... + share_n)) fee + share_1 + ... + share_n = A

The protocol fee rounds down, so its remainder stays with the recipients. Each split share rounds down, and the last entry in split order receives every base unit the floors left over. Because the weights sum to 10,000, the leftover is a whole number below n: at most 7 base units for an eight-entry split. The shares are computed from the split's own weights; resolving the Operator destination changes where a share goes, never its size. Fee and shares together always equal A exactly, which distribute enforces before it writes the lane.

The fee rate is read from Config when distribute runs. The launch parameter is 500 bps, 5% of each distributed amount.

9.5 Worked example: a four-entry split

A lane has a vault balance of 123,456,789 base units (123.456789 USDC), nothing reserved, the launch fee of 500 bps, and this sample split in this order: owner 6,500 bps, operator 2,000 bps, base-model royalty 1,000 bps (locked), contributor 500 bps.

A = 123456789 - 0 = 123456789 fee = floor(123456789 * 500 / 10000) = floor(6172839.45) = 6172839 net = 123456789 - 6172839 = 117283950 owner = floor(117283950 * 6500 / 10000) = floor(76234567.5) = 76234567 operator = floor(117283950 * 2000 / 10000) = 23456790 base model = floor(117283950 * 1000 / 10000) = 11728395 contributor = floor(117283950 * 500 / 10000) = floor(5864197.5) = 5864197 leftover = 117283950 - (76234567 + 23456790 + 11728395 + 5864197) = 1 contributor = 5864197 + 1 = 5864198

The treasury receives 6.172839 USDC, the owner 76.234567, the lane operator's payout_ata 23.456790, the base-model owner 11.728395 and the contributor 5.864198; together they are exactly 123.456789 USDC. With all five accounts healthy, the lane afterwards has total_allocated = total_distributed = 123,456,789, reserved_amount = 0 and an empty vault. If the treasury's owner is neither the model owner nor the operator's authority, eligible_protocol_fees_paid rises by 6,172,839.

9.6 Deferred payouts and Claim accounts

A share is paid directly when its destination is payable: an SPL Token account of the USDC mint, owned by the expected beneficiary, in the Initialized state. A frozen, closed or re-owned account is not payable. Its share is still allocated but deferred: it is added to a Claim account and stays in the vault as reserved revenue, while the other recipients are paid in the same transaction.

Claim (122 bytes)  PDA ["claim", lane, recipient_ata]
  discriminator                   8
  version, bump                   u8, u8
  lane                            Pubkey   lane the entitlement belongs to
  recipient_ata                   Pubkey   the destination that could not be paid
  beneficiary                     Pubkey   the wallet entitled to claim
  amount                          u64      unpaid entitlement
  eligible_protocol_fee_amount    u64      protocol-fee part that earns dispute credit when paid

prepare_claim(recipient_ata, recipient_owner) creates a Claim. Anyone may call it and pay the rent, but only for a recipient in the lane's resolved split or for the configured treasury with its recorded owner. Only a positive share that cannot be paid directly needs one; healthy recipients and zero shares use the placeholder and cost no rent. An existing Claim is reused, and its lane, recipient and beneficiary never change. A fee the treasury cannot receive accrues to the treasury Claim under Config.fee_treasury_owner, never to whoever later controls the token account.

claim_deferred pays a Claim out. The beneficiary signs; there is no other authority over the money.

  1. Check the destination against the configured destination policy. Under CanonicalAta, the default, it must be the beneficiary's canonical USDC associated token account. Under AnyOwnedUsdc, which an admin must select explicitly, it may be any USDC account the beneficiary owns.
  2. If the Claim holds nothing, return.
  3. Require lane.reserved_amount >= claim.amount.
  4. Transfer claim.amount from the vault, signed by the lane PDA.
  5. Add claim.eligible_protocol_fee_amount to lane.eligible_protocol_fees_paid, zero both Claim amounts, subtract the amount from reserved_amount and add it to total_distributed.
  6. Emit DeferredClaimPaid.

The configured claim policy is BeneficiaryOnlyNoExpiry: only the beneficiary can claim, and an entitlement never expires. claim_deferred does not check the pause flag, so beneficiaries can claim while the protocol is paused.

9.7 Worked example: eight recipients, one deferred

A lane uses an eight-entry split with 1,250 bps per entry: the owner, the operator, a locked base-model royalty and five contributors. The vault holds 40,000,005 base units, nothing is reserved and the fee is 500 bps. The fifth entry's USDC account has been frozen by the token issuer.

A = 40000005 fee = floor(40000005 * 500 / 10000) = floor(2000000.25) = 2000000 net = 40000005 - 2000000 = 38000005 share_1..7 = floor(38000005 * 1250 / 10000) = floor(4750000.625) = 4750000 leftover = 38000005 - 8 * 4750000 = 5 share_8 = 4750000 + 5 = 4750005
EntriesAllocated eachPaidDeferred
Treasury (fee)2,000,0002,000,0000
1–4 and 6–7: owner, operator, base model, three contributors4,750,00028,500,0000
5: contributor with frozen account4,750,00004,750,000
8: last contributor, with the leftover4,750,0054,750,0050
Total35,250,0054,750,000

Afterwards total_allocated = 40,000,005, total_distributed = 35,250,005, reserved_amount = 4,750,000 and the vault holds 4,750,000, so total_allocated = total_distributed + reserved_amount holds. When calls add 1,000,000 base units, the vault holds 5,750,000 but available is 1,000,000, and the next distribute allocates only that million. Once the contributor's account is usable again, the contributor signs claim_deferred: the vault pays 4,750,000, reserved_amount falls by that amount and total_distributed rises by it.

A healthy distribution of the fee plus eight split recipients was measured on the compiled program at 165,610 compute units in a 268-byte v0 transaction using an address lookup table, well inside a single transaction's limits.

9.8 Split revisions and per-lane activation

An owner changes a split by proposal, and the change waits seven days.

  1. Propose. The owner signs propose_split(entries) while the protocol is unpaused. The entries must pass the rules in 9.2, no other proposal may be pending, and every locked entry must be preserved or its beneficiary must co-sign. The program stores pending_split = { revision: split_revision + 1, proposed_at: now, effective_at: now + 604800, entries } and emits SplitProposed.
  2. Cancel. The owner may call cancel_split at any time before application, including while the protocol is paused.
  3. Apply. Once the chain clock reaches effective_at, anyone may call apply_split while the protocol is unpaused. The model's split and split_revision take the pending values and SplitApplied is emitted.

Applying a split changes the model's default; it does not reprice money already in a lane. Each lane keeps the split under which its balance arrived and switches only at a balance boundary. After the allocation in distribute, and on the zero-available path, the program compares lane.active_split_revision with model.split_revision. If they differ, it requires vault.amount = reserved_amount, meaning every unreserved base unit has just been allocated under the old split, and only then copies the model's split and revision into the lane and emits LaneSplitActivated.

distribute write-locks the lane and its vault, so a payment lands wholly before the boundary and is paid under the old split, or wholly after it and is paid under the new one. Deferred Claims keep their original beneficiaries. An idle lane activates the new split through a zero-amount distribute that moves no tokens.

Example timeline for a model at split revision 1 with two lanes:

TimeActionModel revisionLane ALane B
Day 0Owner proposes revision 21 (pending 2)rev 1, revenue accruesrev 1, empty
Day 7apply_split2rev 1, revenue availablerev 1, empty
Day 7distribute(A)2Allocates the revenue under rev 1, then activates rev 2rev 1
Day 7distribute(B)2rev 2Nothing available; activates rev 2
AfterCalls continue2Paid under rev 2Paid under rev 2

The registry exposes each lane's active revision next to the model's revision, so a lane still behind is visible.

9.9 The activation keeper

Sealroom runs a keeper that applies due split proposals and moves every lane to the new revision, so owners and recipients need not submit these transactions themselves. All the transactions it sends are permissionless; the keeper has no authority the program does not give everyone.

  1. Discovery. On each pass it enumerates all Model and Lane accounts from chain. A model with a pending split gets a model job due at effective_at; a lane whose active_split_revision is below its model's split_revision gets a lane job.
  2. Model job. When the chain clock has reached effective_at and the protocol is unpaused, the keeper submits apply_split.
  3. Lane job. The keeper submits prepare_claim for any positive share that cannot be paid directly and has no Claim yet, then submits distribute. The job is complete when the lane's active_split_revision reaches the target on chain.
  4. Order and funding. Owners take turns, one lane activation per owner at a time; within an owner, empty lanes go first, then lanes by descending available balance. A failing lane retries on its own schedule. Network fees and Claim rent come from the keeper's fee payer within global and per-owner lamport allowances over a rolling window.

Each lane job runs under a generation-fenced lease, and signed bytes are stored before broadcast, so two keeper workers never send a second financial operation for one boundary. The same keeper schedules attestation-expiry and unbond-maturity events and calls the facilitator's /reconcile (8.7). It never withdraws an operator's bond.

9.10 Fee configuration

Config.protocol_fee_bps and the treasury change only through a timelocked proposal. The admin signs propose_fee(protocol_fee_bps) with a treasury that is a canonical USDC associated token account. The program requires protocol_fee_bps <= 10000, refuses a second pending proposal, records the treasury's current owner and sets effective_at = now + 172800 (48 hours). The admin may cancel with cancel_fee. After the delay anyone may call apply_fee, which installs the rate, the treasury account and its owner and emits FeeApplied. Every distribute after that point uses the new rate on the lane's available balance. The launch parameter is 500 bps.

10. Bonds, disputes and arbitration

An operator is a vetted company that has signed the Operator Agreement and posted a USDC bond. The bond backs the agreement: if an operator leaks weights or attests fraudulently, the arbitration panel can freeze it, invalidate all of its attestations and pay part or all of its bond to the harmed owner. This chapter describes the bond, the dispute process that leads to a ruling, and the arithmetic of slashing.

Bonds, deposits and slashes are denominated in USDC. No token, $SEALROOM included, is ever accepted as bond collateral (14.7).

10.1 Bond accounts and tiers

Each operator has an Operator account at ["op", authority] (193 bytes). Its bond fields are:

FieldTypeBytesMeaning
tieru81Admission tier, 1 to 3
bond_vaultPubkey32USDC associated token account owned by the Operator PDA
bond_amountu648Bond principal: deposited, not withdrawn, not slashed
unbond_atOption<i64>9Time from which the principal may be withdrawn
frozenbool1Set by the admin or by an arbitration ruling
open_dispute_countu324Ordinary disputes open against this operator
next_dispute_sequenceu648Nonce of the next dispute record
attestation_epochu648Revocation epoch that every attestation must match
pending_slash_amountu648Slashes ruled but not yet transferred out of the vault

register_operator, signed by the admin after off-chain vetting, creates the account and the bond vault with a principal of zero. Config.tier_bond_minimums holds exactly three entries, one minimum in USDC base units for each of tiers 1, 2 and 3, and register_operator and set_tier accept only a configured tier. An operator is healthy when it is not frozen and its principal meets its tier's minimum; register_platform, register_node and record_attestation all require a healthy operator. A model can demand more through its policy fields min_tier (1 to 3) and min_bond:

tier_minimum = Config.tier_bond_minimums[tier] healthy = (frozen == false) and (bond_amount >= tier_minimum) bond_eligible_for_model = bond_amount >= max(Model.min_bond, tier_minimum) tier_eligible_for_model = tier >= Model.min_tier

Only bond_amount counts. USDC sent to the bond vault outside deposit_bond does not raise the principal and cannot be withdrawn as principal.

10.2 Depositing, unbonding and withdrawing

deposit_bond(amount). The operator authority signs. The program requires the protocol to be unpaused, amount > 0, the operator not frozen and no unbond request pending. It moves amount with TransferChecked from a USDC account owned by the authority into the bond vault, adds it to bond_amount and emits BondDeposited.

request_unbond(). The operator authority signs. The program requires bond_amount > 0, the operator not frozen, open_dispute_count = 0 and no request already pending. It sets unbond_at = now + 2592000, which is 30 days, and emits UnbondRequested.

cancel_unbond(). The operator authority signs. The program clears unbond_at and emits UnbondCancelled. No funds move, and the operator may deposit again.

withdraw_bond(). The operator authority signs. The program requires the operator not frozen, open_dispute_count = 0, a pending request and now >= unbond_at, and that the vault holds at least bond_amount. It transfers the whole principal, signed by the Operator PDA, to a USDC account owned by the authority, sets bond_amount = 0, clears unbond_at and emits BondWithdrawn. If a full slash has already reduced the principal to zero, the call just clears the matured request.

Unbonding also bounds the node's attestations. While unbond_at is set, record_attestation refuses any attestation whose expires_at falls after it (AttestationBeyondUnbond). No node of an unbonding operator can hold a valid attestation, and therefore cannot receive keys or serve paid calls, beyond the moment its bond can leave.

Operator statedeposit_bondrequest_unbondwithdraw_bond
Healthy, no requestallowedallowed if principal > 0refused: no request
Unbond requested, before unbond_atrefusedrefused: already requestedrefused: timelock
Unbond requested, after unbond_atrefusedrefused: already requestedallowed
Ordinary dispute openallowedrefusedrefused
Frozenrefusedrefusedrefused

The keeper tracks unbond maturity and surfaces it in the registry. It never withdraws a bond.

10.3 Dispute configuration

Disputes run under settings in Config that only a timelocked proposal can change:

FieldMeaning
arbitrationThe arbitration multisig, the only key that may rule
dispute_depositDeposit in base units that a claimant posts to open a dispute
dispute_deposit_policyForfeitOnDismissal
max_open_per_claimant_operatorOpen ordinary disputes allowed per (claimant, operator)
max_open_per_operatorOpen ordinary disputes allowed per operator across all claimants; 8 at initialization
dispute_forfeiture_ata, dispute_forfeiture_ownerWhere a dismissed claimant's deposit goes
pending_dispute_configA proposal waiting for its delay

The admin signs propose_dispute_config(arbitration, dispute_deposit, max_open_per_claimant_operator, max_open_per_operator, deposit_policy) with the forfeiture account. The program requires an arbitration key distinct from the admin and from the default key, a positive deposit and positive caps, the ForfeitOnDismissal policy, a forfeiture account that is a canonical USDC associated token account not owned by the arbitration key, and no other pending proposal.

The proposal takes effect after now + 172800 (48 hours), when anyone calls apply_dispute_config; the program checks the separation of keys again at that moment. The admin may cancel with cancel_dispute_config. The older direct setter set_dispute_config always fails with GovernanceTimelockRequired. The admin therefore cannot appoint itself arbitrator, and a new arbitrator cannot be installed and rule in one step: a proposed arbitrator has no authority until its proposal is applied.

open_dispute refuses to run until the arbitrator, the deposit, the policy, the claimant cap and the forfeiture recipient are all set.

10.4 Who may open a dispute

An ordinary dispute is opened by a model owner against an operator that has served its model under paid traffic. In program terms, the claimant owns a Model (the model PDA is derived from the claimant's key), a Lane exists for that model on a node of the target operator, and that lane holds unused admission credit of at least the configured deposit.

Admission credit measures protocol fees that left this lane for an independent treasury. The program counts them in eligible_protocol_fees_paid:

  • In distribute, when the fee is transferred directly to the treasury and the treasury's recorded owner is neither the model owner nor the authority of the lane's operator, the fee is added.
  • When the fee had to be deferred into the treasury Claim under the same condition, it is recorded in Claim.eligible_protocol_fee_amount and added to the lane only when claim_deferred actually pays it.
  • Gross payments into the vault, the owner's and operator's shares, other split shares and fees still reserved are never counted.
independent = (treasury_owner != model.owner) and (treasury_owner != operator.authority) paid directly and independent: lane.eligible_protocol_fees_paid += fee deferred and independent: claim.eligible_protocol_fee_amount += fee (moved to the lane by claim_deferred) credit = lane.eligible_protocol_fees_paid - lane.admission_credit_consumed admitted = credit >= D on admission: lane.admission_credit_consumed += D

Here D is Config.dispute_deposit. Admission consumes exactly D of credit, and the credit does not come back when the dispute is dismissed or upheld, or when governance later changes the deposit. A second dispute needs another D of fresh credit and another deposit.

Under the launch fee of 500 bps, credit accrues at one twentieth of the revenue distributed through the lane:

fee_k = floor(A_k * 500 / 10000) admitted when fee_1 + fee_2 + ... >= D single distribution: floor(A / 20) >= D <=> A >= 20 * D

For every 1 USDC of configured deposit, the lane must have distributed at least 20 USDC of revenue, with its fee paid to an independent treasury, before its owner can open an ordinary dispute against the operator. Opening it then costs the deposit on top. Credit belongs to one lane: fees paid through the claimant's lanes on other operators admit nothing against this one.

10.5 Opening a dispute

Claimantpaid fees ≥ depositDisputedeposit heldArbitrationmultisigopen_disputeFreezeoperatorRevokeattestationsSlashbond → claimantDismissdeposit policyclaim_dispute

Figure: a dispute from admission through ruling to slash settlement and the claimant's or forfeiture beneficiary's claim.

open_dispute(sequence, evidence_hash) takes the claimant's signature and these accounts: config, the target operator (writable), the claimant's model, a node of that operator, the lane for model and node (writable), the claimant_disputes counter ["claimant_disputes", claimant, operator], the new dispute at ["dispute", operator, sequence:u64], the USDC mint, the claimant's USDC source account and the deposit_vault, the USDC associated token account of the dispute PDA. The evidence_hash is the 32-byte hash of the evidence the claimant submits to the arbitration panel off chain.

The instruction runs these steps:

  1. Require a configured arbitrator, a positive deposit D and the ForfeitOnDismissal policy.
  2. Require sequence = operator.next_dispute_sequence; a stale or replayed nonce fails.
  3. Compute the lane's credit and require credit >= D (InsufficientPaidUsage). Add D to admission_credit_consumed.
  4. Require operator.open_dispute_count < max_open_per_operator and claimant_disputes.open_count < max_open_per_claimant_operator (DisputeLimitReached), creating the claimant counter if needed. Increment the claimant counter.
  5. Transfer D from the claimant to the deposit vault.
  6. Increment operator.next_dispute_sequence and operator.open_dispute_count.
  7. Write the Dispute account and emit DisputeAdmission (credit used and remaining) and DisputeOpened.

Any failure rolls back the whole instruction: credit, tokens and both counters. The Dispute account is 415 bytes:

FieldTypeMeaning
operator, sequencePubkey, u64PDA seeds
model, lane, node, claimantPubkeyThe paid relationship and the claimant
evidence_hash, ruling_hash[u8; 32], Option<[u8; 32]>Commitments to the evidence and the published ruling
deposit, deposit_vaultu64, PubkeyThe posted deposit and the account holding it
statusenumOpen, RuledForClaimant or Dismissed
slash_amount, slash_pending_amountu64Slash ordered, and the part not yet moved from the bond vault
claimable_amount, forfeited_amountu64Owed to the claimant, or to the forfeiture beneficiary
forfeiture_ata, forfeiture_beneficiary, deposit_policyPubkey, Pubkey, enumForfeiture terms copied at opening
opened_at, ruled_ati64, Option<i64>Timestamps
arbitration_initiated, admission_credit_used, admission_required_amountbool, u64, u64Origin of the record, credit consumed and threshold applied

Because the forfeiture terms are copied when the dispute opens, a later governance change cannot redirect a deposit already at stake.

10.6 Open-dispute caps

Two counters bound the number of ordinary disputes open at the same time:

  • ClaimantDisputes.open_count (78-byte account at ["claimant_disputes", claimant, operator]) counts one claimant's open disputes against one operator, across all of that claimant's models. It may not exceed max_open_per_claimant_operator.
  • Operator.open_dispute_count counts open disputes against the operator from all claimants. It may not exceed max_open_per_operator, which is 8 at initialization and changes only through the 48-hour proposal.

Opening increments both counters together, and a ruling decrements both exactly once. Since every admission also spends paid-fee credit and a forfeitable deposit, fresh wallet keys without a paid lane cannot fill the operator-wide queue. Arbitration actions (10.8) use neither counter, so the panel can act on an operator while its queue is full.

10.7 Ruling a dispute

rule_dispute(for_claimant, slash_amount, freeze, revoke_attestations, ruling_hash) is signed by the current arbitration key, which must equal Config.arbitration. Its accounts are the operator (writable), the dispute, the claimant counter, the USDC mint, the bond vault and the deposit vault.

  1. Require dispute.status = Open (DisputeAlreadyRuled otherwise); each dispute is ruled once.
  2. Require slash_amount <= operator.bond_amount.
  3. A dismissal (for_claimant = false) must have slash_amount = 0, freeze = false and revoke_attestations = false.
  4. Reduce bond_amount by the slash and add it to pending_slash_amount.
  5. Decrement open_dispute_count and the claimant counter.
  6. If freeze, set operator.frozen = true. The operator can no longer register platforms or nodes, record attestations, deposit, unbond or withdraw, and every model's eligibility check fails with operator_frozen.
  7. If revoke_attestations, increment operator.attestation_epoch. Every attestation of every node of the operator committed the old epoch in its signed message, so all of them fail the epoch check immediately, and new ones must be signed against the new epoch.
  8. Record the outcome: status RuledForClaimant or Dismissed, ruling_hash, slash_amount, ruled_at, slash_pending_amount = slash_amount, and either claimable_amount = deposit (upheld) or forfeited_amount = deposit (dismissed).
  9. Emit DisputeRuled with the ruling authority, the slash, the deposit disposition, the frozen flag, the new attestation epoch and the ruling hash.

The ruling performs no token transfer, so freezing and revocation take effect even if the token issuer has frozen the bond vault or the deposit vault; moving the slashed USDC is a separate step (10.9). An upheld claimant gets the deposit back together with the slash; a dismissed claimant's deposit goes to the forfeiture beneficiary recorded in the dispute.

10.8 Arbitration actions without a slot

The panel can also act on an operator directly with arbitrate_operator(sequence, evidence_hash, slash_amount, freeze, revoke_attestations, ruling_hash), signed by the current arbitration key. This path exists for security responses that cannot wait for, or do not come from, an ordinary dispute.

  • A zero-slash action freezes the operator and/or advances its attestation epoch. It needs no model, node or lane, no deposit and no forfeiture configuration; the three relationship accounts are omitted together.
  • A positive slash needs the complete relationship: a Model, a Node of this operator and the Lane for them, all at their canonical addresses, with the model owned by the named claimant. That lane's lifetime eligible_protocol_fees_paid must be at least the configured deposit. Lifetime fees count here, including credit already consumed by ordinary disputes, because the panel itself is the admitting authority.
required = 0 if slash_amount = 0 required = D if slash_amount > 0, requiring D > 0 and lane.eligible_protocol_fees_paid >= D

The action checks the dispute nonce and slash_amount <= bond_amount, advances next_dispute_sequence, moves the slash from bond_amount to pending_slash_amount, applies freeze and revocation, and creates a Dispute record that is already ruled: status RuledForClaimant, deposit 0, arbitration_initiated = true, admission_credit_used = 0, admission_required_amount = required. It creates the claimant counter if needed but does not increment it, does not touch open_dispute_count and consumes no admission credit. It emits ArbitrationAction. The slash then follows the same settlement and claim path as an upheld dispute.

10.9 Slashing arithmetic and claims

A slash first moves from principal to a pending liability and later moves as tokens from the bond vault to the dispute's deposit vault, from which the claimant withdraws.

at ruling: bond_amount' = bond_amount - s, s <= bond_amount pending_slash_amount' = pending_slash_amount + s dispute.slash_pending_amount = s dispute.claimable_amount = D (upheld) or 0 (dismissed or arbitration action) at settle: bond_vault -= s, deposit_vault += s dispute.claimable_amount += s, dispute.slash_pending_amount = 0 pending_slash_amount' = pending_slash_amount - s claimant total: D + s (upheld dispute) or s (arbitration action)

settle_slash is permissionless: any signer may submit it, and the program checks only the account links and the dispute state. It requires a ruled dispute, transfers slash_pending_amount from the bond vault to the deposit vault signed by the Operator PDA, and updates the three counters above. A dispute with nothing pending returns without effect, so the transfer happens once. It emits SlashSettled.

claim_dispute is signed by the claimant. It requires a ruled dispute, checks the destination against the claim destination policy (the claimant's canonical USDC account by default), transfers claimable_amount from the deposit vault signed by the dispute PDA, sets it to zero and emits DisputeClaimPaid. A claimant may claim the returned deposit before the slash is settled and claim again afterwards; each call pays only what is claimable at that moment.

claim_forfeited_deposit is signed by the forfeiture beneficiary recorded in the dispute. It requires status Dismissed, applies the same destination policy, transfers forfeited_amount and sets it to zero. It emits DisputeDepositForfeited.

Walk-through of an upheld dispute with bond principal B, deposit D and slash s <= B:

Stepbond_amountpending_slash_amountBond vaultDeposit vaultclaimable_amount
Before openingB0B00
open_disputeB0BD0
rule_dispute (upheld, slash s)B − ssBDD
settle_slashB − s0B − sD + sD + s
claim_disputeB − s0B − s00

For a dismissal the ruling leaves bond_amount at B, sets forfeited_amount = D, and claim_forfeited_deposit empties the deposit vault to the forfeiture beneficiary.

10.10 Pending slashes and bond withdrawals

The program keeps the bond's principal and the pending slashes apart, so a withdrawal can never take money a ruling has already assigned to a claimant.

bond_vault.amount >= bond_amount + pending_slash_amount withdrawable = bond_amount

A ruling lowers bond_amount immediately and raises pending_slash_amount, but leaves the tokens in the vault. withdraw_bond transfers only bond_amount, so the pending slash stays in the vault for settle_slash, whichever of the two runs first. Take principal B and an arbitration action that slashes s without freezing. After the action, bond_amount = B − s and pending_slash_amount = s while the vault still holds B. If the operator then unbonds and withdraws after 30 days, it receives B − s and the vault keeps s; settle_slash later moves s to the dispute's deposit vault and the claimant claims it.

A frozen operator cannot withdraw at all, and an operator with an ordinary dispute open can neither request unbonding nor withdraw. Each ruling decrements open_dispute_count; once no ordinary dispute is open and the operator is not frozen, the unbonding path in 10.2 is available again for the remaining principal.

11. Services and infrastructure

The sealroom program holds the registry and the money. The work around it runs as off-chain services: projecting chain state into a database, verifying vendor evidence, releasing data keys, serving inference, sponsoring payment transactions and starting time-based transitions that a Solana program cannot start by itself. Each service either reads the chain or submits transactions that the program checks on its own terms. No service holds a user's private key; every wallet action is signed in the user's wallet.

11.1 Service map

ServiceRun byInterfaceSolana roleSecrets held
WebSealroomSite, registry app, intake formNone; wallets signNone
Public APISealroomHTTPS JSON under /v1Reads RPC to build previewsAdmin bearer credential, rate-limit HMAC key, challenge secret
IndexerSealroomNone; writes PostgresReads accounts and historyNone
KeeperSealroomNoneFee payer for split activationOne fee-payer key
CoordinatorSealroomPOST /v1/attestFee payer for record_attestationFee-payer key, verifier endpoint credentials
Verifier (five)Committee members; Sealroom runs at most twoPOST /v1/signSigns the committee messageOne Ed25519 verifier key
FacilitatorSealroom, or any SVM x402 facilitator/requirements, /verify, /settle, /settle-voucher, /reconcileFee payer of payment transactionsSponsor key, payment-channel authority
Key brokerModel ownerPOST /v1/key-requestsReads accountsModel data keys
Node runtimeOperator, inside the CVMPOST /v1/chat/completions over attested TLSReads accountsEnclave HPKE, TLS and voucher keys

The API, coordinator, verifiers, facilitator, broker and node expose GET /health; the indexer and keeper have no listener. The coordinator, verifiers and broker are infrastructure endpoints: they reject requests carrying a browser Origin header and require a service bearer credential compared in constant time. Browsers reach Sealroom through the public API and node endpoints only.

11.2 Public API

The public API is the read model and transaction builder for the web app, SDKs and agents. Registry reads come from the Postgres projection kept by the indexer; transaction previews read the Solana RPC directly.

11.2.1 Envelope and encoding

Every response uses one envelope, { ok: true, data, meta } or { ok: false, error, meta }. meta carries the contract version (1.0.0), a request ID (also sent as X-Request-Id), asOf (server wall time) and index, the freshness record of 11.3.4. Values that are u64 on chain are exact decimal strings; USDC amounts are base units with 6 decimals. Keys and signatures are base58, hashes lowercase hex, runtime measurements 48 bytes. An error carries a code (VALIDATION_ERROR, NOT_FOUND, UNAUTHORIZED, FORBIDDEN, RATE_LIMITED, IDEMPOTENCY_CONFLICT, ABUSE_REJECTED, CONFIGURATION_UNSET, INTERNAL_ERROR and others), a retryable flag and, where relevant, per-field messages or retryAfterSeconds. Responses are no-store, cross-origin requests are accepted from exact configured origins only, and request bodies are JSON of at most 16 KiB.

11.2.2 Routes and resources

MethodPathPurpose
GET/v1/parametersProtocol parameters read from Config (chapter 13)
GET/v1/summaryOperators, attested nodes, models, open lanes, open disputes, total distributed
GET/v1/{resource}, /v1/{resource}/{id}Filtered list; one record
GET/v1/eligibility?modelId=&nodeId=Serving decision for one model on one node
POST/v1/transactions/previewTransaction for a wallet intent
POST/v1/applicationsOperator or model-owner application
ResourceFilters
operatorsstatus (bonded, unbonding, frozen, unbonded), tier, operatorId
platformsoperatorId
nodesstatus (registered, serving, offline, disabled), operatorId, platformId, nodeId
attestationsstatus (valid, expiring_soon, expired, revoked), operatorId, platformId, nodeId, method
runtimesstatus (pending, allowed, revoked)
modelsowner, modelId
lanesstatus, modelId, nodeId, laneId, operatorId
distributionslaneId, modelId, nodeId
disputesstatus (open, ruled_for_claimant, dismissed), operatorId, modelId, claimant
key-releasesoperatorId, nodeId, modelId, outcome (released, denied)
activityoperatorId, nodeId, modelId, kind

Lists take limit from 1 to 100 (default 20), a text query of at most 128 characters and an opaque cursor bound to a digest of the resource and filters, so a cursor from one query cannot page another. Unknown filters and mismatched cursors return 422. An attestation's identity is (account, revision), and its detail returns the latest committee-signed revision.

/v1/eligibility runs the same rule set that the key broker and the node apply before releasing a key or serving a call, returning each check with its actual and required value. The twelve checks are no_valid_attestation, attestation_expired, attestation_revoked (including an outdated operator epoch), runtime_not_allowed (the measurement must be allowed globally and pinned by the model), platform_not_registered, tier_below_min, bond_below_min (the larger of the model's min_bond and the tier minimum), operator_frozen, operator_not_on_allowlist, tcb_not_up_to_date, node_disabled and protocol_paused. Evaluation time is the chain Clock observed by the indexer plus the wall time elapsed since that observation, so expiry follows chain time.

11.2.3 Transaction previews

POST /v1/transactions/preview turns a wallet intent into a transaction the user's wallet signs. Seventeen intents are supported: create_model, open_lane, propose_split, apply_split, cancel_split, distribute, prepare_claim, claim, deposit_bond, request_unbond, cancel_unbond, withdraw_bond, register_platform, register_node, open_dispute, claim_dispute and claim_forfeited_deposit. The response holds the base64 serializedTransaction (unsigned, or carrying co-signatures already collected), requiredSigners, an exact estimatedFeeLamports from the RPC, submittable, machine-readable blockedBy reasons, the instruction list and the resolved intent.

  1. Request a preview and read submittable and blockedBy.
  2. Deserialize the bytes without changing the message, keeping any co-signature present.
  3. Sign with the wallet's signTransaction.
  4. Send the raw bytes to the RPC and poll getSignatureStatuses.

The builder derives every PDA and canonical associated token account, adds idempotent token-account creation for fresh USDC destinations, picks the owner's first free positive model ID when create_model omits one, and refuses intents the program would reject, with codes such as PROTOCOL_PAUSED, GPU_UEIDS_REQUIRED, OPERATOR_LOCK_UNSUPPORTED, CLAIM_PREPARATION_REQUIRED, ADMIN_SIGNATURE_REQUIRED and TRANSACTION_TOO_LARGE (over 1,232 bytes). A register_platform preview names the admin multisig as the remaining co-signer.

11.2.4 Applications intake with a challenge

Operators and model owners apply through POST /v1/applications. Every application carries organisation, contact, explicit consent, an empty honeypot field and a challenge response; operators add regions, GPU fleet, certifications, confidential-compute readiness and an optional onboarding wallet, and model owners add their domain, buyers and weight-protection need.

  1. Strict validation; bad or unknown fields return 422 with a message per field.
  2. Rate limiting through an atomic Postgres bucket keyed by an HMAC-SHA256 of the socket address (forwarded headers are not trusted); the default is 20 public writes per minute, shared with previews.
  3. An Idempotency-Key of 16 to 128 characters is required. An identical resubmission returns the stored receipt before any challenge is consumed; changed content returns 409.
  4. A filled honeypot returns 403 ABUSE_REJECTED.
  5. The server verifies the Cloudflare Turnstile response with Siteverify and accepts it only when the returned hostname and action match configuration. Missing configuration fails closed with 503.
  6. The application is stored and a receipt with one support ID is returned.

Contact data and challenge responses never enter logs. Admission on chain is a separate act: after vetting, the admin multisig signs register_operator with the hash of the signed Operator Agreement and the operator's tier.

11.3 Indexer

The indexer mirrors the program into Postgres and never signs.

11.3.1 A reconciliation pass

A pass runs every 2,000 ms by default at finalized commitment:

  1. Read, in parallel, the genesis hash, all program accounts with their context slot, and the Clock sysvar. A database belongs to one genesis hash and program ID; a mismatch stops the pass.
  2. Decode each account by its 8-byte discriminator; program-owned accounts matching no type are kept as raw audit records.
  3. Advance signature history for the program, every lane vault and every bond vault.
  4. Decode events and lane payments from new transactions.
  5. Project accounts and events, reconcile money, and commit under a checkpoint compare-and-swap.

11.3.2 Signatures, events and payments

Each address has its own durable cursor. getSignaturesForAddress pages of up to 1,000 signatures walk back from a head frozen at the snapshot slot to the last processed signature; a pass processes at most 1,000 transactions per address within a backfill bound of 10,000, and commits per batch, so a large backlog is worked off over successive polls. Before declaring a scan complete the indexer fetches the anchor transaction it is scanning toward; a missing anchor marks that history unavailable.

Program events are authenticated self-CPI instructions (emit_cpi!) signed by the program's event-authority PDA. The indexer decodes them from inner instructions (tag e445a52e51cb9a1d), which keeps them intact past Solana's 10 KB log limit. Lane payments are SPL Token Transfer (3) and TransferChecked (12) instructions, top-level or inner, whose destination is a lane vault. Vault balances come from each processed transaction's post-token balances, so a payment and the balance it produced are read at the same boundary.

11.3.3 Reconciliation and commit

payments_in = total_allocated + vault_balance - reserved_amount sum(LaneDistributed.amount) = total_allocated sum(fee_paid + payouts_paid) + sum(DeferredClaimPaid.amount) = total_distributed

A lane that fails a check or whose history is unavailable keeps its last good values, is marked degraded, and retries alone while the rest of the registry continues. A bond vault must cover bond_amount + pending_slash_amount; USDC sent outside deposit_bond never counts as principal. Attestations are projected per signed revision: a revocation marks the revision current at that time, and an epoch-advancing ruling marks every earlier-epoch revision revoked. When the private evidence archive holds evidence whose key transcript hashes to the on-chain enclave_key_hash, the projection adds the HPKE key hash, TLS key hash and voucher public key.

Commits take one Postgres advisory lock and compare the checkpoint the batch started from (indexed slot, last signature and a strictly increasing timestamp). A batch whose base moved returns committed: false, so an older snapshot never overwrites a newer one. Deterministic record keys (chain:<signature>:<event index>, payment:<signature>:<instruction>:<inner index>) make restarts and repeated passes idempotent.

11.3.4 Freshness and index states

meta.index reports indexedSlot, chainSlot, lagSlots, indexedAt (wall time at which the observation started), chainTime, backfillPending and status:

stale = no_checkpoint or backfillPending or (now - indexedAt > 30000 ms) or (lagSlots > 150)

Age counts from the start of the observation, so a slow reconciliation cannot make old data look fresh.

StateShown inCause
Freshmeta.index.statusObservation under 30 s old, lag within 150 slots, history complete
Stale by laglagSlotsIndexed slot more than 150 slots behind the chain
Stale by ageindexedAtLast observation started more than 30 s ago
Stale by backfillbackfillPendingA cursor is mid-scan or a money mirror is degraded
Lane degradedlanes[].reconciliationThat lane failed conservation; it retries alone
Bond degradedoperators[].bondReconciliationVault does not cover principal plus pending slash

11.4 Keeper

The keeper submits time-driven transitions that any wallet may submit and none is obliged to. It signs only permissionless instructions, never withdraws a bond and never signs for a user. Every 5,000 ms it reconciles its pending transactions, runs one indexer pass, asks the facilitator to /reconcile expired upto ceilings, discovers jobs, and leases up to 32 of them.

11.4.1 Jobs

KindDedupe keyAction
Attestation expiryexpiry:<attestation>:<revision>Marks expiry; the program and eligibility read expires_at directly
Unbond maturityunbond:<operator>:<unbond_at>Marks maturity; withdrawal stays the operator's signed withdraw_bond
Split activation, modelmodel-split:<model>:<revision>:<effective_at>apply_split once mature
Split activation, lanelane-split:<lane>:<revision>prepare_claim as needed, then a boundary distribute

Duplicate inserts are skipped, so several keepers discover the same work once.

11.4.2 Split activation per model revision

Each lane stores its own active_split and active_split_revision; apply_split changes only the model default. A lane adopts a new revision only when its available balance is zero, so revenue that arrived under the old split is paid under the old split.

  1. Model job. Once the chain Clock passes effective_at, re-read the model; if its revision is still the target minus one with the same effective_at, submit apply_split.
  2. Lane jobs. Create one job per Lane account whose revision is below the model's, enumerated from chain accounts so that a lane missing from the index is still found.
  3. Claims. For each recipient whose positive old-split share cannot be paid directly (closed, frozen or owner-changed account), submit prepare_claim for its Claim PDA.
  4. Pre-send check. The model revision is unchanged, no Claim is missing, and the available balance is still zero or still positive.
  5. Boundary crank. distribute allocates every unreserved base unit under the old split, reloads the vault and, when vault.amount == reserved_amount, copies the model split into the lane. An idle lane advances with a zero-amount call that moves no money.
  6. Completion. The lane account's active_split_revision reaches the target; completion is read from the account, not from the transaction status.

Owners take turns by the time they were last served. Within an owner the model job goes first, then empty or reserved-only lanes, then lanes in descending available balance; at most one activation per owner is leased at a time, and a recipient failure affects only its own lane's retry.

11.4.3 SOL budget

Sponsorship requires a global cap, an owner cap and a rolling window, with no defaults. The owner cap must be below the global cap and large enough for one preparation transaction (up to three Claim rents plus its fee); an optional per-model lifetime cap adds a further brake. Every fee and every Claim rent counts against both caps:

max_cost = fee_lamports + claim_rent_lamports * new_claims pending = sum(max_cost of unsettled reservations) used = pending + sum(actual_cost of settlements within the window) admit = (pending + max_cost <= cap) and (used + max_cost <= cap)

Direct canonical payouts need no Claim, and Claims are bound to their beneficiary and reused. Actual cost is taken from the fee payer's balance change, with each Claim's rent attributed once. When pending reservations fill a cap, the lane retries in 60 s; when settled costs fill the window, it retries as they age out; a transaction larger than a cap is terminal. A receipt that costs more than its reservation is recorded OVERDRAWN and halts all sponsorship until a recorded review acknowledges the exact amount. Blocked budgets re-alert every 900 s. The keeper sponsors split activation only; ordinary distributions are paid by whichever wallet sends them.

11.4.4 Leases with generations

A lease records a worker, a generation incremented at each lease, and an expiry on the database clock: 60,000 ms by default, never more than 180,000 ms from acquisition. There is no heartbeat; only confirmed chain progress extends a lease. Every RPC call is bounded by 10,000 ms. The reservation checks owner and generation in the same database transaction; a broadcast requires at least the RPC timeout plus 1,000 ms of lease remaining; signed bytes and their cost breakdown are committed before broadcast, and an ambiguous send is retried with the same bytes and signature, so it cannot become a second operation. Completion and failure updates match the generation, so a superseded lease cannot send. Failed jobs retry after min(300 s, 1 s * 2^min(attempts, 8)).

11.5 Coordinator and verifier services

A node submits evidence in the sealroom-evidence-v1 format (at most 1 MiB): node PDA, a recent slot and its hash, the 1,216-byte X-Wing public key, the 32-byte TLS public key, the 32-byte Ed25519 voucher key, the TDX quote and 1 to 8 GPU evidence blobs. The evidence hash is SHA-256 of a fixed-order serialization; raw evidence is archived privately by that hash.

Each committee member runs one verifier service with one Ed25519 key, its own credential and its own collateral cache. Quotes are checked with Intel DCAP collateral and the NVIDIA local verifier (RIMs and OCSP), with NVIDIA's NRAS as an optional cross-check; the vendor adapter is the only component that parses evidence. On POST /v1/sign a verifier:

  1. Verifies the vendor evidence and extracts the platform ID hash, GPU UEIDs, runtime measurement, RIM hash, REPORTDATA, GPU nonces and TCB status.
  2. Reads Config, Node, Platform, Operator, Attestation, VerifierSet, Runtime, the global pins, Clock and SlotHashes from RPC in one context, and checks the genesis hash.
  3. Checks the registry: not paused, operator not frozen and at its tier's bond minimum, node enabled, platform and GPU pins matching this platform and node, runtime allowed and effective, slot hash present in SlotHashes.
  4. Checks the binding: REPORTDATA and every GPU nonce equal H (12.4); the sequence is the next one, the operator epoch is current, the verifier is a member of the active epoch, and the expiry is within 24 hours.
  5. Archives the evidence and signs the 200-byte canonical message.

The coordinator holds no verifier key, and its fee payer must not be a member. It repeats the vendor and registry checks, sets expiresAt to chain time plus 86,400 s minus 1 s, sends the proposal to all five verifiers in parallel (30 s timeout each), accepts only signatures from each endpoint's configured key over the exact message, keeps three distinct ones, and submits one Ed25519 precompile instruction followed by record_attestation as a v0 transaction with a lookup table. Measured on the compiled program, that transaction is 1,087 bytes and 46,465 to 46,505 compute units.

11.6 Facilitator

The facilitator settles x402 payments. The node never holds facilitator or channel custody keys.

EndpointFunction
/requirementsOpens a session bound to request, caller, lane, scheme, amount and body hash; returns x402 requirements
/verifyChecks the caller's signed payment and its binding, with no transfer
/settleCo-signs as fee payer, broadcasts, confirms, checks the vault's post-token balance
/settle-voucherSettles upto usage from the enclave voucher and refunds the unused ceiling
/reconcileReturns expired, unserved ceilings and finishes ambiguous intents

Requirements name the USDC mint, the lane vault as payTo, the amount, a 120 s timeout and, in extra, the fee payer, a memo sealroom:<sessionId>, a recent blockhash and the session and lane IDs. The payment transaction must have exactly four instructions: compute-unit limit 100,000, compute-unit price 0, one TransferChecked of the exact amount with 6 decimals from the caller's canonical ATA to payTo, and the memo. It must be at most 1,232 bytes, use no lookup tables, require exactly the fee payer's and the caller's signatures, and keep the fee payer out of every instruction. Session state moves by compare-and-set, signed bytes are persisted before broadcast, an ambiguous retry looks up the same signature, and a settled request returns its receipt idempotently.

One TransferChecked to one lane vault per call keeps the layout that SVM x402 facilitators validate; the multi-recipient split happens later in distribute. For upto, the caller escrows a ceiling in the payment-channels program (CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX); after inference the facilitator checks the enclave-signed voucher against the session and the attested voucher key, settles the actual amount to the lane vault and returns the rest to the caller in one transaction.

11.7 Owner key broker

The Model account names its broker: key_broker (public key) and key_broker_url (HTTPS, at most 128 bytes). A node's key client sends POST /v1/key-requests with a request UUID, the model ID and its evidence. The broker reads the chain accounts directly in one RPC context, runs the eligibility rules, checks that the model names this broker and that the evidence hash, enclave key hash, slot and slot hash equal the attested values, re-verifies the vendor evidence, and wraps the model's 32-byte data key to the enclave's HPKE key (12.7). It then reads the chain again and releases only if the attestation, its revision, the broker key and the policy hash are unchanged. An audit record (model, node, operator, attestation and revision, evidence hash, enclave key hash, policy hash, read slot, outcome and reason codes) is persisted before the ciphertext leaves; denials are recorded the same way. These records are public at /v1/key-releases, so the release log can be compared with the chain by anyone. Raw evidence, data keys and ciphertexts never enter the API or logs.

11.8 Operator node runtime

A node is one TDX confidential VM with 1 to 8 H100 or H200 GPUs in confidential-computing mode, running the measured Sealroom runtime, a reproducibly built image on Confidential Containers.

ComponentFunction
Attestation agentGenerates the enclave keys at boot, binds them into REPORTDATA and the GPU nonces, submits evidence, re-attests before expiry
Key clientRequests the data key, unwraps it after checking the release context, decrypts weights into CVM and GPU protected memory
vLLMServes architectures the pinned build supports; no owner-supplied code (trust_remote_code is off)
x402 middlewareReturns 402 for unpaid requests, verifies payment, settles after inference
Voucher signerSigns upto usage vouchers with the enclave Ed25519 key
TLS terminationTerminates TLS inside the CVM with the attested TLS key
Egress allowlistOutbound connections only to the key broker, the RPC and the facilitator

A call proceeds as follows:

  1. The caller connects over attested TLS (12.8) and sends its request with request ID, model, node, lane and scheme.
  2. Without a PAYMENT-SIGNATURE header the node answers 402 with x402 v2 PAYMENT-REQUIRED.
  3. The caller's wallet signs only its own slot of the payment transaction, and the caller retries the identical request.
  4. The node checks that the payload matches the quote and has the facilitator /verify it.
  5. The node checks eligibility and that its key release belongs to the current attestation revision, runs inference, and checks both again.
  6. The node calls /settle (and for upto signs the voucher and calls /settle-voucher), then returns the completion with PAYMENT-RESPONSE.

If eligibility fails before settlement, no payment is taken; once payment succeeds, the completion produced under a valid attestation is returned. The runtime stops serving when its attestation lapses.

11.9 Deployment topology

Sealroom servicewebsite · app · docsapiregistry · previewsfacilitatorx402 settleindexerkeepersplit activationcoordinatorattestationsPostgresverifiersone per committee memberSolanasealroom program · USDCOperators' nodesTDX + H100/H200 CCOwners' key brokersCallerswallets, agentspaid calls

Figure: Sealroom's services over Postgres and a Solana RPC, with owner-run key brokers and operator-run nodes.

Sealroom runs the web app, public API, indexer, keeper, coordinator and facilitator over one Postgres database and a Solana RPC endpoint. The five verifiers run as five separate services, each with only its own key and credential; Sealroom runs at most two, and independent organisations run the others on their own hosts. Model owners run key brokers. Operators run nodes in their vetted datacenters.

ProcessTalks toRecords it owns
Public APIPostgres, RPCApplications, idempotency records, rate buckets
IndexerRPC, PostgresRaw accounts, projections, events, checkpoints, history cursors
KeeperRPC, Postgres, facilitatorJobs, budgets, sponsored transactions, Claim charges
Coordinator and verifiersEach other, RPC, vendor collateral servicesEvidence archive by hash
FacilitatorRPC, PostgresPayment sessions and signed intents
Key brokerRPC, its key storeBroker audit records
NodeCoordinator, broker, facilitator, RPCNone; keys stay in the CVM

Each service runs as its own process with its own health check, and schema migrations apply at service start. Chain state is authoritative for the registry and money; services append their own records and never rewrite chain history.

12. Security design

Sealroom protects model weights and their data keys, callers' prompts, USDC in lane, bond and dispute vaults, and the registry that decides which machines receive keys.

12.1 Threat model

ThreatAdversary capabilityDefense
Impersonate a node with a purchased serverOwns a genuine TDX machine and can extract its attestation keys physicallyPlatform pinning, operator vetting (12.2)
Run software that copies weights or promptsControls the host and boots another imageMeasured runtime, timelocked allowlist, owner pins (12.3)
Replay evidence or borrow GPU evidenceHolds old quotes, controls other GPUsFresh keys bound to a slot hash, UEID pins (12.4)
Corrupt the committeeControls up to two verifiersThree-of-five threshold, broker re-verification (12.5)
Keep serving after a compromise is foundHolds a once-valid node24-hour TTL, immediate revocation, operator epoch (12.6)
Capture the data keyControls network and host OSHPKE to the enclave key (12.7)
Read prompts at the operatorTerminates TLS outside the CVMAttested TLS (12.8)
Overcharge metered callsControls the host's billing pathEnclave-signed vouchers (12.9)
Physically attack a registered serverPhysical access in a vetted facilityOperator Agreement, USDC bond, arbitration (12.10)
Change rules quicklyHolds the admin keyTimelocks, separate multisigs (12.11)
Substitute accountsCrafts instruction account listsProgram account checks (12.12)

12.2 Platform pinning and operator vetting

A genuine Intel platform is not enough to receive a key: release accepts only quotes whose platform identity and GPU device IDs a vetted, bonded operator registered. A machine bought on the open market therefore cannot be presented as a node, whatever attestation keys it holds.

  1. Sealroom vets the operator's legal entity, datacenter certifications and signed Operator Agreement; the admin multisig then calls register_operator.
  2. register_platform, signed by both operator and admin, stores the hash of the Intel PPID/PCK and of 1 to 8 distinct GPU UEIDs, and creates a global PlatformPin (["platform_pin", platform_id_hash]) and a global GpuPin (["gpu_pin", ueid_hash]) per GPU. Pins are created only at empty addresses, so a platform or GPU belongs to one operator and one platform.
  3. register_node claims named UEIDs from its platform; each pin's node field is set once, so a GPU serves one node.
  4. Platform, node and attestation registration require an operator that is not frozen and holds its tier's bond minimum.
  5. Verifiers and brokers require the quoted platform ID and GPU UEIDs to match the pins for this node.

NVIDIA GPU evidence is not bound to a particular CVM; evidence produced on any GPU outside the node's pinned set fails the UEID check.

12.3 Measured runtime and owner-pinned measurements

The runtime is built reproducibly, so its 48-byte TDX measurement can be recomputed from source. Each allowed image has a Runtime account at ["rt", SHA256(measurement)] with image name, version and 40-character source commit. allow_runtime makes a measurement effective 48 hours later; revoke_runtime is immediate. record_attestation requires an effective, unrevoked runtime equal to the attested measurement. Each model pins 1 to 8 measurements, and eligibility requires both lists, so a key goes only to an image that the protocol has allowed for at least 48 hours and that the owner chose.

12.4 Evidence bound to fresh keys and a slot hash

The CVM generates its keys at boot. Their public parts form a 1,280-byte transcript:

transcript = hpke_public_key[1216] || tls_public_key[32] || voucher_public_key[32] enclave_key_hash = SHA256(transcript) H = SHA256(transcript || recent_slot_hash) REPORTDATA[0..32] = H gpu_nonce[i] = H for every GPU i

The slot and its hash must appear in the SlotHashes sysvar (at most 512 entries) when a verifier signs and when record_attestation executes, so evidence is recorded while its slot is recent. The canonical 200-byte message binds the domain sealroom.att.v0, program ID, genesis hash, node PDA, committee epoch, next sequence, the hash of the 194-byte commitment (platform, measurement, RIM hash, evidence hash, enclave key hash, TCB status, expiry, method, operator epoch), slot and slot hash. A signature cannot be reused on another program, deployment, node, sequence or evidence, and the private keys never leave the CVM.

12.5 Three-of-five committee and broker re-verification

The VerifierSet holds exactly five distinct members at threshold three (allocation up to seven). record_attestation reads the preceding Ed25519 precompile instructions through the Instructions sysvar and requires every offset to point inside the same instruction, every message to equal the canonical message, every signer to be a member of the active epoch, and at least three distinct signers, tracked in an 8-bit bitmap. The Solana runtime verifies the signatures. Sealroom runs at most two verifiers, so every quorum includes an independent member, and the coordinator holds no verifier key.

The owner's broker checks independently of the committee: it reads the chain itself, re-verifies the vendor evidence and requires the evidence, keys, slot, measurement and RIM hash to equal the attested values. A committee-signed record built on evidence that fails vendor verification is refused at the broker.

12.6 Short attestation lifetime and immediate revocation

  • TTL. expires_at lies after the current chain time, at most 86,400 s later, and no later than any pending unbond deadline.
  • Revocation. The admin or any current verifier can revoke_attestation, effective in the same transaction. It consumes the next sequence and latches the record, so a renewal signed for that sequence cannot land. clear_attestation_revocation (admin or arbitration) lifts the latch and expires the record, so a fresh quorum is required.
  • Operator epoch. Arbitration can advance an operator's attestation_epoch. Every commitment includes its epoch, and older epochs are treated as revoked, so one ruling invalidates all of that operator's attestations.
  • Freeze and pause. A frozen operator fails platform and node registration, attestation and eligibility. While paused, registration, attestation, model creation, split changes, lane opening and distribution stop and brokers release nothing; revocation, freezing, rulings, withdrawals and claims remain available.
  • Runtime. The node checks eligibility before inference and before settlement, and stops serving when its attestation lapses.

12.7 Key release only through HPKE to the enclave key

Weights are encrypted with a random 256-bit data key (AES-256-GCM, chunked); the ciphertext can be stored anywhere. The data key leaves the broker only as an HPKE ciphertext:

ElementValue
ModeRFC 9180 base mode, one sender context per release
SuiteX-Wing (X25519 + ML-KEM-768), HKDF-SHA256, AES-256-GCM
infosealroom.owner-key-release.v1
AADsealroom.key-release.v1, program, genesis hash, model, node, attestation and signed revision, evidence hash, request ID
RecipientThe HPKE key whose hash is committed in enclave_key_hash

Only the CVM holding the matching private key can open it, and the key client refuses an envelope whose context differs from the one it expects. The broker persists its audit record before the ciphertext leaves and zeroes the data key after every attempt. The default broker is run by the owner, so no new party holds keys.

12.8 Attested TLS

The TLS key is generated in the CVM and is part of the committed transcript; the API exposes its hash as tlsPublicKeyHash. Before sending a prompt, the caller's SDK reads the node's attestation, requires status valid, and requires the presented TLS key to hash to tlsPublicKeyHash and the transcript to enclave_key_hash. The session ends inside the measured CVM, and the operator's host sees only ciphertext. Node endpoints are HTTPS URLs validated on chain.

12.9 Usage vouchers signed by the enclave key

A metered charge is set by a voucher only the enclave can sign. The voucher (domain sealroom:usage:v1) carries lane, channel, request hash, nonce, cumulative amount, expiry, attestation and signed revision, and signer; the signed message also binds the genesis hash. The facilitator accepts it only when the signer is the voucher key in the node's current attestation and every field matches the session. The amount comes from the model's on-chain rates and is capped by max_per_call and the caller's ceiling; altered or replayed vouchers fail.

12.10 Bonds and arbitration

Each operator's USDC bond sits in a vault owned by its Operator PDA and backs the Operator Agreement. Deposits must be positive and are refused while frozen or unbonding. Unbonding takes 30 days and is refused while frozen or under an open dispute, and no attestation may expire after the unbond deadline, so no attested node outlives the locked bond. A model owner with a paid lane opens a dispute with an evidence hash, a deposit and admission credit (14.6), within per-claimant and per-operator caps. The arbitration multisig rules once: for the claimant it can slash up to the bond principal, freeze the operator and advance its epoch; a dismissal cannot slash, freeze or revoke. arbitrate_operator lets arbitration freeze or revoke at any time without a dispute. Slashes are reserved at ruling time and moved by a permissionless settle_slash; security rulings make no token transfer, so a frozen token account cannot block them.

12.11 Timelocked governance

Admin and arbitration are separate Squads multisigs and must differ, which the program enforces at initialization and on every arbitration change. Changes to who is paid or which software receives keys wait before applying: 48 hours for a runtime, the fee and treasury, and the dispute configuration; seven days for a split; 30 days for a bond withdrawal. A pending arbitrator has no authority. Owners' runtime lists, allowlists and minimums apply on top of protocol defaults.

12.12 Program account checks

  • Ownership and type. Typed accounts are checked for program ownership and discriminator; hand-written accounts (GPU pins, Claims) must be program-owned, writable and at their re-derived PDA and bump.
  • PDA seeds. Every account loads at its canonical address and stored bump, e.g. ["op", authority], ["node", platform, index], ["att", node], ["model", owner, id], ["lane", model, node], ["claim", lane, recipient_ata], with parent links enforced by has_one.
  • Signers. Admin and arbitration must equal the keys in Config; operators and owners are bound through their PDA seeds; claimants and beneficiaries must equal the recorded beneficiary. initialize must be signed by the upgrade authority recorded in ProgramData.
  • Tokens. The mint must be Config.usdc_mint with 6 decimals, transfers use TransferChecked, vaults are canonical ATAs of their PDA, and split recipients, treasury and forfeiture recipient must be canonical ATAs of the declared wallet.
  • Splits. 1 to 8 positive entries summing to 10,000 bps, unique recipients, exactly one unlockable Operator entry routed to the lane operator; reductions of locked entries need the beneficiary's signature.
  • Distribution. Recipient and Claim accounts must match the split, be writable and differ from the vault; every Claim is authenticated before the first transfer; arithmetic is checked, and fee plus paid plus deferred must equal the available balance.

Off chain, every chain-reading service pins the expected genesis hash and program ID, and unset security configuration (challenge secret, vendor adapter, wrapping provider, keeper budgets) fails closed.

13. Parameters and governance

13.1 Protocol parameters

Durations and size limits are program constants; economic values live in Config and change only through the instructions listed. /v1/parameters reports each value with its source, and a value not yet set as unset with the operation it blocks.

ParameterValueWhere setWho can change it
Protocol fee500 bps at launch; at most 10,000Config.protocol_fee_bpsAdmin propose_fee; anyone apply_fee after 48 h
Fee treasuryCanonical USDC ATAConfig.fee_treasury_ataWith the fee, same path
Attestation TTL24 h (86,400 s)Constant ATTESTATION_TTLProgram upgrade
Committee5 distinct members, threshold 3VerifierSet, written by initializeFixed per committee epoch
Verifier keys per setUp to 7Constant MAX_VERIFIERSProgram upgrade
Unbonding period30 days (2,592,000 s)Constant UNBONDING_PERIODProgram upgrade
Split delay7 days (604,800 s)Constant SPLIT_DELAYProgram upgrade
Runtime allowlist timelock48 h (172,800 s)Constant RUNTIME_TIMELOCKProgram upgrade
Fee and dispute-config timelock48 hSame constantProgram upgrade
Split entries1 to 8MAX_SPLIT_ENTRIESProgram upgrade
Allowed runtimes per model1 to 8MAX_ALLOWED_RUNTIMESProgram upgrade
Operator allowlist per model0 to 16 (empty admits any eligible operator)MAX_ALLOWED_OPERATORSProgram upgrade
Node endpoint, broker URLHTTPS, at most 128 bytes eachMAX_ENDPOINT_BYTES, MAX_KEY_BROKER_URL_BYTESProgram upgrade
GPUs per platform and node1 to 8; H100 or H200MAX_GPUS, GpuModelProgram upgrade
Runtime measurement48 bytesRuntime.measurementProgram upgrade
Runtime image metadataName at most 64 bytes, version at most 32, commit 40 hexProgram constantsProgram upgrade
Tiers and bond minimumsTiers 1 to 3, one USDC minimum eachConfig.tier_bond_minimumsWritten by initialize; admin set_tier assigns tiers
Model minimum tier and bondTier 1 to 3; any USDC amountModel.min_tier, Model.min_bondOwner, in create_model
Model priceExact { per_call } or Upto { input_per_million_tokens, output_per_million_tokens, max_per_call }Model.priceOwner, in create_model
Derivative royalty0 to 10,000 bpsModel.derivative_royalty_bpsOwner, in create_model
Operator shareWeight of the split's Operator entryModel.splitOwner propose_split; anyone apply_split after 7 days
Default operator shareProtocol default reported by the APIConfig.default_operator_share_bpsReserved Config field
Dispute depositPositive USDC amountConfig.dispute_depositAdmin propose_dispute_config; anyone applies after 48 h
Open disputes per claimant and operatorPositive integerConfig.max_open_per_claimant_operatorSame path
Open disputes per operatorDefault 8Config.max_open_per_operatorSame path
Dismissed depositForfeitOnDismissal to a canonical ATA whose owner is not arbitrationConfig.dispute_forfeiture_ataSame path
ArbitrationSquads multisig, not the adminConfig.arbitrationinitialize; then the dispute-config path
AdminSquads multisigConfig.admininitialize
Claim policy, claim destinationBeneficiaryOnlyNoExpiry; CanonicalAta (default) or AnyOwnedUsdcConfigAdmin set_settlement_policy
Split settlementLaneBoundaryConfig.settlement_mechanismFixed at initialization
Pausetrue or falseConfig.pausedAdmin set_paused
USDC mintSix-decimal SPL mintConfig.usdc_mintinitialize
Mainnet beta capsTotal bonds and total vaults; the brief's example is $250kReserved Config fieldsAdmin operating policy
Data key, weight cipher256-bit key, AES-256-GCMBroker and runtime formatsRuntime release
Enclave keysHPKE (X25519 + ML-KEM-768), TLS, Ed25519 voucherRuntimeRuntime release
Committee message200 bytes; commitment 194 bytesProgramProgram upgrade
Legacy transaction size1,232 bytesSolanaNetwork limit

13.2 Service parameters

ParameterValue
Index freshness: maximum age; maximum lag30,000 ms; 150 slots
Indexer poll; backfill bound2,000 ms; 10,000 signatures
API page size; public write rate1 to 100, default 20; 20 per minute per address
Keeper poll; jobs per tick5,000 ms; 32
Keeper lease default and maximum; RPC timeout60,000 ms and 180,000 ms; 10,000 ms
Keeper retry backoff; blocked re-alertmin(300 s, 1 s * 2^min(attempts, 8)); 900 s
Keeper global, owner and model caps; windowSet per deployment, no defaults
Coordinator verifier timeout30 s
Payment session timeout; compute limit120 s; 100,000 CU at price 0
Evidence sizeAt most 1 MiB

13.3 Authorities

AuthorityHolderPowers
Upgrade authoritySealroomSigns initialize; deploys upgrades
AdminSquads multisigOperator admission, tier and freeze; co-signs register_platform; runtime allowlist; revoke_attestation, clear_attestation_revocation; pause; settlement policy; proposes and cancels fee and dispute-config changes
ArbitrationSeparate Squads multisigrule_dispute, arbitrate_operator, clear_attestation_revocation
Verifier committeeFive members, three neededSigns attestations; any member may revoke_attestation
Model ownerOwner walletcreate_model, propose_split, cancel_split, open_dispute; runs the key broker
OperatorOperator authorityBond lifecycle, co-signs register_platform, register_node
BeneficiariesRecipient walletsConsent to locked-entry reductions; claims
AnyoneAny walletrecord_attestation with a quorum, open_lane, prepare_claim, distribute, apply_split, apply_fee, apply_dispute_config, settle_slash

13.4 The multisig and timelock model

The admin admits operators, curates runtimes and proposes economic changes; the arbitration panel rules on disputes and takes security actions. Both sign through the Squads multisig program, and their keys must differ, so no single authority both changes dispute rules and rules on disputes.

ChangeProposeWaitApplyCancel
Allow a runtimeAdmin allow_runtime48 hAt effective_atAdmin revoke_runtime
Fee and treasuryAdmin propose_fee48 hAnyone apply_feeAdmin cancel_fee
Arbitration, deposit, caps, forfeitureAdmin propose_dispute_config48 hAnyone apply_dispute_configAdmin cancel_dispute_config
Model splitOwner propose_split7 daysAnyone apply_split, then per-lane activationOwner cancel_split
Bond withdrawalOperator request_unbond30 daysOperator withdraw_bondOperator cancel_unbond
  1. One proposal of each kind may be pending.
  2. A fee proposal validates the treasury as a canonical ATA and snapshots its owner.
  3. A dispute-config proposal requires arbitration distinct from the admin and from the forfeiture owner, positive deposit and caps, and ForfeitOnDismissal; applying re-checks the separation.
  4. A pending arbitrator has no authority, and the direct setter set_dispute_config always returns GovernanceTimelockRequired.
  5. apply_split is refused while paused; cancellation stays available.

Protective actions apply immediately: revoke_runtime, revoke_attestation, freeze_operator, set_paused and rulings. Claim beneficiaries, forfeiture recipients and ruling authorities are snapshotted into accounts and events, so later governance cannot reassign entitlements or re-attribute past actions.

14. Economics

All money in Sealroom is USDC, in base units with 6 decimals: callers pay per call into lane vaults, distribute pays the protocol fee, the operator and the split, and bonds and dispute deposits are posted in USDC. The protocol fee, once in the treasury, buys $SEALROOM on the market and burns it (14.7).

14.1 Fee flow

  1. Price. The owner sets a fixed per_call price (exact) or per-million-token input and output rates with a max_per_call ceiling (upto).
  2. Payment. Each call pays over x402 into the vault of its (model, node) lane: the price for exact, or the voucher amount from an escrowed ceiling for upto.
  3. Accrual. The vault accumulates payments; nothing is split per call.
  4. Distribution. Anyone calls distribute(lane). One all-or-nothing transaction pays the fee to the treasury and every split entry, with the Operator entry routed to the lane operator's payout account.
  5. Deferral. A share whose account is closed, frozen or owner-changed is reserved as a Claim for its beneficiary; other payouts proceed.
  6. Buyback. The treasury's fee income buys $SEALROOM on the market and burns it (14.7).

14.2 Distribution arithmetic

G = vault.amount - reserved_amount F = floor(G * protocol_fee_bps / 10000) N = G - F s_i = floor(N * bps_i / 10000) for entries i = 1 .. k-1 s_k = N - (s_1 + ... + s_(k-1)) the final entry receives the rounding dust F + s_1 + ... + s_k = G

Worked example at the launch fee of 500 bps with an eight-entry split of 1,250 bps each:

Available G (base units)Fee FNet NEntries 1 to 7, eachEntry 8Check
1,000,00150,000950,001118,750118,75150,000 + 7 × 118,750 + 118,751 = 1,000,001
10,0035009,5031,1871,194500 + 7 × 1,187 + 1,194 = 10,003

A fee plus eight recipients in one v0 transaction with a lookup table measures 268 bytes and 165,610 compute units on the compiled program.

14.3 Per-call pricing

charge = ceil((input_tokens * input_per_million_tokens + output_tokens * output_per_million_tokens) / 1000000) charge <= max_per_call charge <= caller_ceiling

At the brief's assumed specialist price of $3 to $10 per million output tokens (3,000,000 to 10,000,000 base units per million), one second of generation at the assumed 1,500 output tokens per second costs:

charge_low = ceil(1500 * 3000000 / 1000000) = 4500 base units = 0.0045 USDC charge_high = ceil(1500 * 10000000 / 1000000) = 15000 base units = 0.015 USDC fee_low = floor(4500 * 500 / 10000) = 225 base units fee_high = floor(15000 * 500 / 10000) = 750 base units

14.4 Illustrative unit economics

The brief's illustrative economics use the listed price of a TEE H100 on Phala, $3.08 per GPU-hour, and assume an 8B to 32B model sustains 1,500 output tokens per second on one H100.

tokens_per_gpu_hour = 1500 * 3600 = 5400000 compute_per_million_full = 3.08 / 5.4 = 0.57 USD (full use) compute_per_million_20 = 3.08 / 1.08 = 2.85 USD (20% use) fee_per_million = 0.05 * price_per_million
QuantityAt $3 per million tokensAt $10 per million tokens
Compute cost per million tokens, full use / 20% use$0.57 / $2.85$0.57 / $2.85
Protocol fee per million tokens (5%)$0.15$0.50
Caller payments per GPU-hour, full use (5.4M tokens)$16.20$54.00
Protocol fee per GPU-hour, full use$0.81$2.70
Paid to the split per GPU-hour, full use$15.39$51.30
Caller payments per GPU-hour, 20% use (1.08M tokens)$3.24$10.80

The GPU-hour costs $3.08 whether it serves 5.4 million tokens or 1.08 million, so utilization sets the operator's margin.

14.5 Protocol revenue

protocol_revenue = caller_payments * 500 / 10000 20000000 USD * 0.05 = 1000000 USD
Annual caller paymentsProtocol revenue at 5%Inference tokens at $3 per millionInference tokens at $10 per million
$20M$1M6.67 trillion2 trillion
$50M$2.5M16.7 trillion5 trillion

All protocol revenue is the buyback budget for $SEALROOM (14.7). The 5% fee is comparable to OpenRouter's 5 to 5.5% fee on credit purchases. Private lanes (a dedicated operator set, SLA, custom policy and invoicing) are sold as monthly contracts, and operator vetting (onboarding and periodic review) is charged at cost recovery.

14.6 Operator compensation, royalties and admission credit

Owners and operators choose how the operator is paid. Under revenue share, the split's single Operator entry carries a weight set by the owner and pays each lane's own operator, so one model served by several operators pays each from its own lanes; the entry cannot be locked, because a model cannot consent for every operator that may serve it. Under a reservation, the owner pays the operator for reserved capacity off-chain and the split's Operator weight reflects that arrangement.

Contributors are paid through the same split. A derivative of a base model whose derivative_royalty_bps is positive must carry a locked BaseModel entry of at least that weight paid to the base owner's canonical ATA, and the base owner's broker can release the base key only to lanes whose split carries it. A locked entry changes only with its beneficiary's signature, and every split change waits seven days and applies per lane at an empty boundary.

Each tier has a protocol bond minimum and each model may require a higher min_bond. Dispute admission is priced in protocol fees actually paid to a treasury owned by neither the model owner nor the operator; owner and operator shares never count.

credit_available = eligible_protocol_fees_paid - admission_credit_consumed admit = credit_available >= dispute_deposit gross_for_credit = ceil(dispute_deposit * 10000 / protocol_fee_bps) at 500 bps: gross_for_credit = 20 * dispute_deposit

open_dispute consumes credit equal to the deposit and transfers the deposit. A ruling for the claimant returns the deposit and pays the settled slash; a dismissal forfeits it. Consumed credit is never restored.

14.7 $SEALROOM

$SEALROOM is the protocol's token. Its design is deliberately small: a fixed supply, one fair launch, and a single source of demand that comes from paid inference.

PropertyValue
Ticker$SEALROOM
Supply1,000,000,000, fixed
Launch100% to one public launch on equal terms
AllocationsNone: no presale, team, investor, airdrop or reserved allocation
VestingNone
Protocol fee shareAll of it buys $SEALROOM on the market and burns it

Fair launch. The entire supply entered circulation through one public launch, on the same terms for every participant. Nothing is held back for a team, investors or a reserve, and nothing unlocks later, so the supply that exists after the launch is the most that will ever exist; from then on it only falls as tokens are burned.

Buyback and burn. The protocol fee is taken at distribute (14.1 and 14.2), in USDC, before any split entry is paid, and lands in the canonical treasury account. The treasury's fee income has one use: buying $SEALROOM on the market and burning it.

fee(d) = floor(available(d) * protocol_fee_bps / 10000) buyback_usdc = sum of fee(d) over every distribution d supply(t) = 1000000000 - burned(t) at 500 bps: buyback_usdc = 0.05 * distributed caller payments

At the launch fee, every 20 USDC of caller payments that reaches a distribution sends 1 USDC to the buyback. The buyback is funded by paid inference and by nothing else: $SEALROOM pays no emissions, staking rewards or incentives, and operators earn only from caller revenue through the split (14.6). The fee itself is governed like every other Config value: a change is proposed by the admin and applied after 48 hours (Chapter 13).

What stays in USDC. $SEALROOM sits on the fee side of the protocol. Callers and agents pay over x402 in USDC, so a call never needs a conversion. Bonds, dispute deposits and slashes are USDC, so a ruling pays the claimant in the same unit its calls were priced in. Key release depends only on the six release checks, and operator admission remains a vetting decision. No token, $SEALROOM or a model's, is accepted as bond collateral or used to gate key release, and Sealroom issues no tokenized model revenue shares.

Appendix A. Instruction reference

Fee and rent payers are also written; they are listed only when they are the signer of record. "Remaining" marks accounts passed after the named ones.

InstructionSignersAccounts writtenEffect
initializeadmin, as upgrade authorityconfig, verifier_setCreates Config (genesis hash, six-decimal USDC mint, canonical treasury, fee, three tier minimums, optional arbitration distinct from admin, fixed durations) and the epoch-0 set of five verifiers at threshold three
set_pausedadminconfigSets or clears paused
register_operatoradminoperator, bond_vaultAdmits an operator with agreement hash and tier; creates its bond vault; records its payout ATA
set_tieradminoperatorAssigns a tier that has a bond minimum
freeze_operatoradminoperatorSets or clears frozen
deposit_bondoperatoroperator, source, bond_vaultAdds a positive USDC amount to principal
request_unbondoperatoroperatorSets unbond_at 30 days ahead; no freeze or open dispute
cancel_unbondoperatoroperatorClears the request
withdraw_bondoperatoroperator, bond_vault, destinationAfter unbond_at, pays principal to the operator
register_platformoperator, adminplatform, platform_pin, GPU pins (remaining)Registers a platform ID hash and 1 to 8 GPU UEID hashes; creates global pins
register_nodeoperatornode, GPU pins (remaining)Creates a node with key, HTTPS endpoint and GPU model; claims named GPUs exclusively
allow_runtimeadminruntimeAllows a measurement, effective after 48 h
revoke_runtimeadminruntimeRevokes a measurement immediately
record_attestationpayer (any)attestationRecords an attestation signed by three distinct members, after the registry, freshness, expiry, sequence and epoch checks
revoke_attestationadmin or current verifierattestationLatches revocation and consumes the next sequence
clear_attestation_revocationadmin or arbitrationattestationClears the latch and expires the record
create_modelownermodelCreates a model with manifest hash, broker, policy, price, royalty, base model and split at revision 1
propose_splitowner; beneficiaries of reduced locked entriesmodelStores a pending split, effective after 7 days
cancel_splitownermodelRemoves the pending split
apply_splitnonemodelMakes a mature pending split the model default
open_lanepayer (any)lane, vaultCreates the (model, node) lane and vault; snapshots the split
prepare_claimpayer (any)claimCreates the Claim PDA for a lane recipient or the treasury
distributenonelane, vault, fee_treasury, fee_claim, recipient and Claim pairs (remaining)Pays fee and split, defers unpayable shares, activates a newer split at an empty boundary
claim_deferredbeneficiarylane, claim, vault, destinationPays a Claim under the destination policy
set_settlement_policyadminconfigSets claim and claim-destination policies
propose_feeadminconfigRecords a pending fee and treasury
cancel_feeadminconfigRemoves the pending fee
apply_feenoneconfigApplies the fee after 48 h
propose_dispute_configadminconfigRecords pending arbitration, deposit, caps and forfeiture recipient
cancel_dispute_configadminconfigRemoves the pending configuration
apply_dispute_confignoneconfigApplies it after 48 h
set_dispute_configadminnoneAlways returns GovernanceTimelockRequired
open_disputeclaimant (model owner)operator, lane, claimant_disputes, dispute, source, deposit_vaultAdmits a dispute on paid-fee credit, sequence and caps; takes the deposit
rule_disputearbitrationoperator, dispute, claimant_disputes, bond_vault, deposit_vaultRules once: slash, freeze and epoch advance for the claimant, or dismissal
arbitrate_operatorarbitrationoperator, claimant_disputes, dispute, deposit_vaultRecords a ruled security action; a positive slash needs a paid lane
settle_slashany signeroperator, dispute, claimant_disputes, bond_vault, deposit_vaultMoves a reserved slash into the dispute vault
claim_disputeclaimantdispute, deposit_vault, destinationPays returned deposit and settled slash
claim_forfeited_depositforfeiture beneficiarydispute, deposit_vault, destinationPays a dismissed dispute's deposit
rescue_unopened_laneadmin, owner, operatornoneAlways returns RescueDisabled; prefunded vaults are opened and distributed normally

Appendix B. Glossary

$SEALROOM. The protocol's token: 1,000,000,000 fixed supply, launched entirely in one public launch; the protocol fee buys it on the market and burns it (14.7).

Admission credit. Protocol fees a lane has paid to an independent treasury, minus credit already used to open disputes.

Arbitration panel. A Squads multisig, separate from the admin, that rules on disputes and can freeze operators, revoke attestations and slash bonds.

Attestation. The on-chain record that a verifier quorum checked a node's evidence: measurement, RIM hash, evidence hash, enclave key hash, TCB status, slot, expiry and signers.

Attestation epoch. A per-operator counter committed into every attestation; advancing it invalidates all of that operator's attestations.

Attested TLS. TLS whose server key is committed in the node's attestation, so the session ends inside the CVM.

Base model. A model referenced by a derivative, whose royalty the derivative's split must carry as a locked entry.

Bond. USDC an operator posts as collateral, slashable by arbitration and withdrawable 30 days after an unbond request.

Caller. An app, enterprise or agent that pays per call over x402.

Claim. A per-lane, per-recipient account holding a share that could not be paid directly, payable only to its beneficiary.

Committee message. The 200-byte message verifiers sign for one attestation.

CVM. A confidential virtual machine: an Intel TDX guest, with GPUs in confidential-computing mode.

Data key. The random 256-bit key that encrypts a model's weights.

Dispute. A model owner's complaint against an operator, ruled once by arbitration.

Distribute. The permissionless instruction that pays a lane's balance to the fee, the operator and the split.

Enclave key hash. SHA-256 of a node's HPKE, TLS and voucher public keys.

Evidence. A node's TDX quote, GPU reports, public keys and recent slot hash.

Exact. The fixed-price x402 scheme.

Facilitator. The service that validates, sponsors and settles x402 payments.

HPKE. Hybrid Public Key Encryption; Sealroom wraps data keys with X-Wing (X25519 + ML-KEM-768).

Keeper. The service that sponsors time-driven permissionless transactions such as split activation.

Key broker. The owner-run service that wraps a model's data key to an attested enclave.

Lane. The account for one (model, node) pair, with its own USDC vault and active split.

Locked entry. A split entry that changes only with its beneficiary's signature.

Measurement. The 48-byte TDX measurement of the runtime image.

Model. The on-chain listing of an encrypted model: manifest hash, broker, policy, price and split.

Node. One CVM with 1 to 8 GPUs running the measured runtime.

Operator. A vetted datacenter company that posts a bond and registers platforms and nodes.

Platform. A physical server identified by its Intel PPID/PCK hash and its GPU UEIDs.

Pin. A global account tying a platform to one operator, or a GPU to one platform and one node.

Protocol fee. The basis points paid to the treasury in distribute; 500 bps at launch.

REPORTDATA. The TDX quote field carrying the key-and-slot binding.

Runtime. The reproducibly built CVM image that serves inference.

Sequence. An attestation's replay counter; each signed revision uses the next value.

Slot hash. A recent Solana slot's hash, used to prove evidence freshness.

Split. Up to eight weighted recipients of a model's net revenue, summing to 10,000 bps.

TCB status. The vendor's verdict on firmware and microcode; only UpToDate is accepted.

Tier. An operator class from 1 to 3 with a bond minimum.

UEID. A GPU's device identity, pinned to one platform and node.

Upto. The metered x402 scheme, settled by an enclave-signed voucher.

Vault. A program-owned USDC account for lane payments, bonds or dispute funds.

Verifier. A committee member that checks evidence and signs the committee message.

Voucher. An enclave-signed statement of metered usage.

x402. The HTTP 402 protocol through which callers pay per call.