Public API
The public API serves the Sealroom registry, indexed from the chain, and builds the transactions your wallet signs. It never holds keys or submits transactions for you.
Base URL
<api-url>
All routes are under /v1, except /health. Requests and responses are JSON. The contract version is 1.0.0, reported in every response.
| Method | Route | Returns |
|---|---|---|
| GET | /health | Service status |
| GET | /v1/parameters | The deployment's protocol parameters |
| GET | /v1/summary | Registry totals |
| GET | /v1/{resource} | A page of a resource |
| GET | /v1/{resource}/{id} | One record |
| GET | /v1/eligibility?modelId=…&nodeId=… | Whether a node may serve a model, rule by rule |
| POST | /v1/transactions/preview | An unsigned transaction for a wallet intent |
| POST | /v1/applications | An operator or model-owner application |
Response envelope
Every response has the same shape:
{ "ok": true, "data": { }, "meta": { } }
{ "ok": false, "error": { "code": "NOT_FOUND", "message": "Resource not found.", "retryable": false }, "meta": { } }
meta describes the read:
| Field | Meaning |
|---|---|
contractVersion | 1.0.0 |
requestId | The request's id, also sent as the X-Request-Id header |
asOf | Server time of the response |
index.status | fresh while the index is within 30 seconds and 150 slots of the chain, otherwise stale |
index.indexedSlot, index.chainSlot, index.lagSlots | The slot indexed, the chain's slot and the difference |
index.indexedAt | When the index last caught up |
index.chainTime | The chain clock used for expiries and timelocks |
Formats
| Kind | Format |
|---|---|
| Accounts and ids | Base58 public keys (32 bytes) |
| USDC amounts, slots, counters | Decimal strings of integers. Amounts are base units with 6 decimals: "25000" is 0.025 USDC |
| Hashes | 64 lowercase hex characters (SHA-256) |
| Runtime measurements | 96 lowercase hex characters (48 bytes) |
| Shares | Integers in basis points, 0 to 10,000 |
| Times | ISO 8601 UTC timestamps |
| Endpoints and broker URLs | HTTPS, at most 128 bytes |
Use big-integer arithmetic for amounts.
Health
GET /health returns status (ok), mode (live), contractVersion, database and chain (connected or degraded).
Parameters
GET /v1/parameters returns the settings the program and services run with. Single settings have the form { "status": "set", "value": …, "source": "chain" }.
| Field | Meaning |
|---|---|
protocolFeeBps | Protocol fee, 500 bps at launch |
pendingProtocolFee | A proposed fee change and its effectiveAt, or null |
attestationTtlSeconds | Attestation lifetime: 86,400 (24 hours) |
verifierSet | members, threshold (3), expectedMembers (5), epoch |
unbondingPeriodSeconds | 2,592,000 (30 days) |
splitDelaySeconds | 604,800 (7 days) |
runtimeAllowlistTimelockSeconds | 172,800 (48 hours) |
limits | splitEntries 8, allowedRuntimes 8, operatorAllowlist 16, verifierKeys 7, endpointBytes 128, gpusPerNode 8 |
usdcMint, usdcDecimals | The USDC mint and 6 |
programId, cluster | The sealroom program and the Solana cluster |
paused | Whether the protocol is paused |
adminMultisig, arbitrationMultisig | The admin and arbitration authorities |
protocolFeeRecipientAta | The treasury's USDC account |
bondMinimumsByTier | Minimum bond for tiers 1, 2 and 3 |
disputeDeposit | The dispute deposit |
operatorShareDefaultBps | The suggested operator share for new models |
settlementPolicies | claims, splitSettlement, disputeDeposit, claimDestination |
disputeConfiguration | Open-dispute limits, forfeiture beneficiary, pending changes |
payments | Which call schemes the deployment quotes (exact, upto) and the payment-channels program |
Summary
GET /v1/summary returns operators, attestedNodes, models, openLanes, totalDistributed (base units) and openDisputes.
Resources
| Resource | Record | Filters |
|---|---|---|
operators | An operator: authority, tier, status, bondAmount, unbondAt, frozen, payoutAta, platformIds, nodeIds, disputeIds, nextDisputeSequence, openDisputeCount, revenue, uptime | status, tier, operatorId |
platforms | A server: operatorId, platformIdHash, identityKind, gpuUeidHashes, nodeIds | operatorId |
nodes | A node: operatorId, platformId, index, nodeKey, endpoint, gpuModel, gpuCount, status, currentAttestationId, attestationExpiresAt | status, operatorId, platformId, nodeId |
attestations | An attestation: revision, nodeId, runtimeMeasurement, gpuRimHash, evidenceHash, enclaveKeyHash, tlsPublicKeyHash, voucherPublicKey, tcbStatus, attestedAt, expiresAt, method, signingVerifiers, status | status, operatorId, platformId, nodeId, method |
runtimes | A runtime image: measurement, imageName, imageVersion, sourceCommit, status, activatesAt | status |
models | A model: modelId, owner, manifestHash, keyBroker, policy, price, derivativeRoyaltyBps, baseModelId, split, pendingSplit, eligibleNodeIds, revenue | owner, modelId |
lanes | A lane: modelId, nodeId, operatorId, vault, status, vaultBalance, totalIn, totalDistributed, activeSplit, claimable, admission credit | status, modelId, nodeId, laneId, operatorId |
distributions | A distribution: laneId, distributedAt, grossAmount, protocolFeeBps, payouts, splitHash | laneId, modelId, nodeId |
disputes | A dispute: operatorId, modelId, claimant, evidenceHash, deposit, status, ruling | status, operatorId, modelId, claimant |
key-releases | A key broker decision: modelId, nodeId, attestationId, attestationRevision, outcome, reasonCodes, releasedAt | operatorId, nodeId, modelId, outcome |
activity | A registry event: kind, occurredAt, source, entityId, amount, signature, summary | operatorId, nodeId, modelId, kind |
Every record from the chain has an id (its account address) and a chain reference with the slot, transaction signature and observedAt time of its latest change.
On a resource's own list, its id filter matches the record id: GET /v1/models?modelId=<account> returns that model. Filters are exact matches, and a filter a resource does not support returns VALIDATION_ERROR. Each list also accepts q, a case-insensitive search over the record's name, endpoint, image name, summary or id.
Status values:
| Resource | status |
|---|---|
operators | bonded, unbonding, frozen, unbonded |
nodes | registered, serving, offline, disabled |
attestations | valid, expiring_soon, expired, revoked |
runtimes | pending, allowed, revoked |
lanes | open, closed |
disputes | open, ruled_for_claimant, dismissed |
GET /v1/attestations/{id} returns the latest revision of that attestation. activity kinds include model_created, lane_opened, lane_payment, distribution, attestation_recorded, attestation_revoked, attestation_expired, key_released, key_denied, bond_deposited, unbond_requested, split_proposed, split_applied, dispute_opened, dispute_ruled and claim_paid, among others.
Pagination
Lists return a page:
{ "items": [ ], "nextCursor": "eyJvZmZzZXQiOjIw…", "total": 57 }
| Parameter | Meaning |
|---|---|
limit | 1 to 100 items, 20 by default |
cursor | The nextCursor of the previous page |
nextCursor is null on the last page. A cursor belongs to its resource and filters: keep the same query and change only cursor.
curl "<api-url>/v1/lanes?modelId=<model>&status=open&limit=50"
Eligibility
GET /v1/eligibility?modelId=<model>&nodeId=<node> returns eligible, checkedAt, the attestationId and attestationRevision it used, reasons (the codes that failed) and checks, one per rule:
{ "code": "bond_below_min", "passed": true, "message": "Operator bond must meet the model and protocol tier minimums.", "actual": "1000000000", "required": "100000000" }
The rule codes are no_valid_attestation, attestation_expired, attestation_revoked, runtime_not_allowed, platform_not_registered, tier_below_min, bond_below_min, operator_frozen, operator_not_on_allowlist, tcb_not_up_to_date, node_disabled and protocol_paused.
Transaction previews
POST /v1/transactions/preview takes one wallet intent and returns the exact transaction to sign. Every intent has an action and an authority: the wallet that signs and pays the network fee.
Intents
action | Inputs besides authority | Who sends it |
|---|---|---|
create_model | modelId (optional), manifestHash, keyBroker { publicKey, url }, policy { allowedRuntimes, minTier, minBond, operatorAllowlist }, price, derivativeRoyaltyBps, baseModelId (or null), split | Owner |
open_lane | modelId, nodeId | Anyone |
propose_split | modelId, split, consentSigners (optional) | Owner |
apply_split | modelId | Anyone, after 7 days |
cancel_split | modelId | Owner |
distribute | laneId | Anyone |
prepare_claim | laneId, recipientAta, recipientOwner | Anyone |
claim | laneId, recipientAta, destinationAta (optional) | The recipient |
deposit_bond | amount (greater than 0) | Operator |
request_unbond | none | Operator |
cancel_unbond | none | Operator |
withdraw_bond | none | Operator, after 30 days |
register_platform | platformIdHash, gpuUeidHashes (1 to 8, distinct) | Operator, co-signed by the admin multisig |
register_node | platformId, index (0 to 65,535), nodeKey, endpoint, gpuModel (H100 or H200), gpuCount (1 to 8), gpuUeidHashes (exactly gpuCount) | Operator |
open_dispute | operatorId, modelId, evidenceHash, disputeNonce, laneId (optional) | Model owner |
claim_dispute | disputeId, destinationAta (optional) | The claimant |
claim_forfeited_deposit | disputeId, destinationAta (optional) | The forfeiture beneficiary |
price is { "scheme": "exact", "perCall": "25000" } or { "scheme": "upto", "inputPerMillionTokens": "…", "outputPerMillionTokens": "…", "maxPerCall": "…" }. A split is 1 to 8 entries { recipientAta, recipientOwner, bps, locked, role } with distinct canonical USDC accounts, summing to 10,000 bps. role is owner, operator, base_model or contributor. Include recipientOwner for any recipient whose USDC account does not exist yet: the transaction creates it.
curl -X POST "<api-url>/v1/transactions/preview" \
-H "Content-Type: application/json" \
-d '{"action":"distribute","authority":"<your wallet>","laneId":"<lane>"}'
Preview response
| Field | Meaning |
|---|---|
submittable | true when your signature completes the transaction |
intent | The intent as resolved, for example with the modelId the API picked |
serializedTransaction | The transaction, base64. Deserialize it, sign it without changing the message and send it to Solana |
requiredSigners | Every signer the transaction needs |
estimatedFeeLamports | The network fee |
instructions | Each instruction's name, programId and accounts |
blockedBy | Codes that prevent the action; empty when it can go ahead |
message | A sentence explaining the result |
When blockedBy is not empty, serializedTransaction is null and message says what to do.
Preview blockers
| Code | Meaning |
|---|---|
PROTOCOL_PAUSED | The protocol is paused |
MODEL_ALREADY_EXISTS | This owner already used that modelId |
MODEL_OWNER_REQUIRED | Only the model's owner can change its split |
PENDING_SPLIT_EXISTS | Apply or cancel the pending split before proposing another |
NO_PENDING_SPLIT | The model has no pending split |
SPLIT_TIMELOCK_PENDING | The split can be applied after its effectiveAt time |
OPERATOR_LOCK_UNSUPPORTED | Operator entries cannot be locked |
RECIPIENT_OWNER_REQUIRED | Add recipientOwner for a recipient whose USDC account does not exist yet |
CANONICAL_RECIPIENT_REQUIRED | Split recipients use their canonical USDC accounts |
RECIPIENT_CONSENT_SIGNATURE_REQUIRED | Lowering a locked entry needs that recipient's signature |
CLAIM_PREPARATION_REQUIRED | Send prepare_claim for the listed recipients, then distribute |
INVALID_LANE_CLAIM | An existing claim names a different recipient |
CLAIM_BENEFICIARY_REQUIRED | Only the recorded beneficiary can claim these funds |
DESTINATION_ATA_REQUIRED | A USDC account that does not exist yet must be your canonical one |
INVALID_TOKEN_ACCOUNT | Choose a USDC account owned by the recipient |
TOKEN_ACCOUNT_FROZEN | That USDC account is frozen |
OPERATOR_FROZEN | The operator is frozen |
UNBONDING_ACTIVE | Cancel unbonding before adding to the bond |
BOND_REQUIRED | Deposit a bond before requesting unbonding |
UNBOND_NOT_REQUESTED | Request unbonding first |
UNBOND_TIMELOCK_PENDING | The bond can be withdrawn after its unbondAt time |
OPEN_DISPUTE | Wait for the operator's open disputes to be ruled |
ADMIN_SIGNATURE_REQUIRED | The transaction still needs the admin multisig's co-signature |
GPU_UEIDS_REQUIRED | Name the exact registered GPU UEID hashes the node claims |
GPU_NOT_REGISTERED | A selected GPU is not registered on this platform |
GPU_ALREADY_CLAIMED | A selected GPU already belongs to another node |
RELATED_LANE_REQUIRED | Choose a lane of your model served by this operator |
INSUFFICIENT_PAID_USAGE | The lane needs unused paid protocol fees at least equal to the deposit |
DISPUTE_OPEN | Funds can be claimed after the ruling |
DISPUTE_NOT_DISMISSED | Only a dismissed dispute has a forfeited deposit |
TRANSACTION_TOO_LARGE | The transaction exceeds 1,232 bytes, for example a split with many new recipient accounts. Create those accounts first |
FEE_ESTIMATE_UNAVAILABLE, CHAIN_CLOCK_UNAVAILABLE, CHAIN_STATE_UNAVAILABLE, MODEL_ID_UNAVAILABLE | Request the preview again |
Applications
POST /v1/applications receives operator and model-owner applications from the form at https://sealroom.cloud/apply. It needs an Idempotency-Key header (16 to 128 letters, digits, _ or -) and the form's anti-abuse token. Sending the same key and body again returns the same receipt: id, status (received), confirmation and receivedAt.
Errors
| HTTP | code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | The route needs authentication |
| 403 | FORBIDDEN | The request's origin is not allowed |
| 403 | ABUSE_REJECTED | The anti-abuse check failed |
| 404 | NOT_FOUND | No such record or route |
| 405 | METHOD_NOT_ALLOWED | Wrong method; the Allow header lists the right one |
| 409 | IDEMPOTENCY_CONFLICT | The idempotency key was used with a different body |
| 413 | PAYLOAD_TOO_LARGE | The body exceeds 16 KiB |
| 415 | UNSUPPORTED_MEDIA_TYPE | Send Content-Type: application/json |
| 422 | VALIDATION_ERROR | A field is invalid; fieldErrors maps each field to messages |
| 429 | RATE_LIMITED | Wait retryAfterSeconds, also sent as Retry-After |
| 503 | CONFIGURATION_UNSET | A required setting is missing; configurationKeys names it |
| 503 | INTERNAL_ERROR | A backend dependency did not answer; retry |
Every error has retryable, which is true for 429 and 5xx responses. The TypeScript client adds NETWORK_ERROR, TIMEOUT, CANCELLED and INVALID_RESPONSE for failures before a valid response arrives.
Rate limits
POST /v1/transactions/preview and POST /v1/applications share a limit of 20 requests per minute for each client address, counted in one-minute windows. A request over the limit returns 429 RATE_LIMITED with Retry-After. Reads are not rate limited.
Headers
| Header | Direction | Use |
|---|---|---|
Content-Type: application/json | Request | Required on POST |
Idempotency-Key | Request | Required on POST /v1/applications |
Origin | Request | Browser requests are accepted from the Sealroom site's origin. Servers, scripts and agents send none |
X-Request-Id | Response | The request's id, equal to meta.requestId |
Cache-Control: no-store | Response | Every response is fresh |
Retry-After | Response | Seconds to wait after a 429 |
Allow | Response | Permitted methods after a 405 |
Query parameters appear once each; a repeated parameter returns VALIDATION_ERROR.