List a model

As a model owner you keep the weights encrypted and the key in your own hands. The registry records which machines may receive the key and who is paid for each call. Listing takes four steps: encrypt the weights, run a key broker, list the model, and open lanes on the nodes that will serve it.

Any wallet can list a model. To talk with Sealroom about onboarding and buyers first, apply as a model owner at https://sealroom.cloud/apply?kind=model_owner (choose A model owner, fill in Model domain, Target buyers, Number of models and Why the weights need protecting, then Send application).

1. Prepare the weights

  1. Generate a random 256-bit data key. It stays in your key broker.
  2. Encrypt the weights in chunks with AES-256-GCM under the data key.
  3. Upload the ciphertext to any storage. Public storage is fine: the ciphertext is useless without the key.
  4. Write a manifest for the encrypted weights, for example the location of each ciphertext chunk.
  5. Compute the SHA-256 of the manifest. The registry stores it as the model's manifest_hash (64 lowercase hex characters), so a node can confirm it loads exactly the weights you listed.

Nodes serve models with vLLM inside the Sealroom runtime, so package the weights in a format vLLM loads.

2. Run your key broker

Your key broker is the service that gives the data key to nodes. It answers at an HTTPS URL of at most 128 bytes and is identified by a public key. Both go on the Model account when you list.

When a node needs your model, it sends POST /v1/key-requests to your broker with its attestation evidence:

FieldMeaning
requestIdUUID of the request
modelIdYour Model account
evidence.nodeIdThe node asking
evidence.recentSlot, evidence.recentSlotHashThe Solana slot the evidence is bound to
evidence.keys.hpkePublicKeyThe enclave's HPKE public key (X25519 + ML-KEM-768, 1,216 bytes, base64)
evidence.keys.tlsPublicKeyThe enclave's TLS public key (32 bytes, base64)
evidence.keys.voucherPublicKeyThe enclave's Ed25519 voucher key
evidence.tdxQuoteThe Intel TDX quote
evidence.gpuEvidenceEvidence from each GPU (1 to 8)

Before it releases anything, the broker reads the Attestation, Platform and Model accounts on-chain and checks:

CheckPasses when
Attestation validSigned by 3 verifiers, not expired, not revoked
Platform registeredThe Intel PPID/PCK hash is on the operator's record
GPUs registeredEvery GPU UEID is pinned to that platform
Runtime allowedThe measurement is on your model's own list
Operator eligibleTier, bond and allowlist meet your model's policy
Evidence matchesThe evidence hash equals the one on-chain, and the evidence verifies again

Then it wraps the data key to the enclave's HPKE public key (suite X-Wing-draft06-HKDF-SHA256-AES256GCM) and returns:

FieldMeaning
auditIdThe id of this release in the public log
attestationId, attestationRevisionThe attestation the release relied on
wrappedKeyversion, suite, enc, ciphertext and context. The associated data binds the deployment, model, node, attestation revision, evidence hash and request

Only that enclave can unwrap the key. The weights are decrypted in the confidential VM's memory and loaded into GPU protected memory.

Every release and every denial is recorded publicly. GET <api-url>/v1/key-releases?modelId=<model> lists them with outcome (released or denied), reasonCodes, the attestation and evidence hashes, the policy hash and the slot the broker read. You can compare your broker's log with the chain at any time.

3. List the model

In the app

Open Your models (from the top bar, or List a model on the Registry page) and connect the wallet that will own the model. Fill in the List a model form:

FieldWhat to enter
Manifest (text or URL of the encrypted weights)Your manifest, or its URL. The app stores its SHA-256
Price per call, USDCThe fixed price a caller pays, up to 6 decimals, for example 0.025
Your share, %Your share of each lane's revenue after the protocol fee. The rest goes to the operator of each lane
Runtime nodes must runThe runtime image (name and version) whose measurement nodes must run
Key broker URLYour broker's HTTPS URL
Key broker public keyYour broker's public key

Select List the model and sign. Below the button, the form shows the current protocol_fee and the split_delay for later split changes.

The app lists a model with:

  • a fixed price per call (exact),
  • the runtime you chose as the only allowed runtime,
  • minimum tier 1 and no minimum bond beyond the protocol's tier minimum,
  • an empty operator allowlist, so any vetted operator's node can qualify,
  • a two-entry split: your share to your canonical USDC account, and the operator share to whichever operator runs each lane.

Your model gets the next free number for your wallet and appears as "Model 1", "Model 2" and so on.

From code: the full policy

The create_model intent accepts every policy option. Send it to POST <api-url>/v1/transactions/preview and sign the transaction it returns (TypeScript client).

FieldRules
modelIdOptional. Omit it and the API picks your next free number (1, 2, 3, …) and returns it in the preview's intent
manifestHash64 lowercase hex characters
keyBroker{ publicKey, url }, the URL HTTPS and at most 128 bytes
policy.allowedRuntimes1 to 8 runtime measurements (96 hex characters each) from the allowed runtimes at GET /v1/runtimes?status=allowed
policy.minTier1, 2 or 3
policy.minBondMinimum operator bond in USDC base units. A node qualifies when its operator's bond meets the higher of this and the protocol's minimum for the operator's tier
policy.operatorAllowlistUp to 16 operator ids. Empty means any vetted operator
price{ scheme: 'exact', perCall } or { scheme: 'upto', inputPerMillionTokens, outputPerMillionTokens, maxPerCall }, all in base units
derivativeRoyaltyBpsThe royalty derivatives of this model must pay you, in bps
baseModelIdThe Model account this model derives from, or null
split1 to 8 entries summing to 10,000 bps
const preview = await api.previewTransaction({
  action: 'create_model',
  authority: owner,
  manifestHash,
  keyBroker: { publicKey: brokerKey, url: 'https://broker.example.com' },
  policy: { allowedRuntimes: [measurement], minTier: 2, minBond: '0', operatorAllowlist: [] },
  price: { scheme: 'upto', inputPerMillionTokens: '3000000', outputPerMillionTokens: '10000000', maxPerCall: '200000' },
  derivativeRoyaltyBps: 0,
  baseModelId: null,
  split: [
    { recipientAta: ownerUsdc, recipientOwner: owner, bps: 7000, locked: false, role: 'owner' },
    { recipientAta: placeholderOperatorUsdc, recipientOwner: placeholderOperator, bps: 2000, locked: false, role: 'operator' },
    { recipientAta: dataProviderUsdc, recipientOwner: dataProvider, bps: 1000, locked: true, role: 'contributor' },
  ],
})

Which nodes qualify

A node is eligible for your model when every rule passes:

RuleReason code when it fails
The node has a current attestationno_valid_attestation
The attestation has not expiredattestation_expired
The attestation has not been revokedattestation_revoked
The runtime is allowed globally and on your listruntime_not_allowed
The node's platform is registered to its operatorplatform_not_registered
The operator's tier meets your minimumtier_below_min
The operator's bond meets your and the protocol's minimumbond_below_min
The operator is not frozenoperator_frozen
The operator is on your allowlist, if you set oneoperator_not_on_allowlist
The TCB status is UpToDatetcb_not_up_to_date
The node is not disablednode_disabled
The protocol is not pausedprotocol_paused

Each model's eligibleNodeIds lists the nodes that pass right now. GET <api-url>/v1/eligibility?modelId=…&nodeId=… shows every rule for one pair.

4. Open lanes on nodes

A lane pairs your model with one node and owns the USDC vault that collects that node's payments for your model. Callers can only pay through an open lane.

In Your models, the table lists each model with Price, Lanes, In vaults (USDC waiting in all its lane vaults) and Open a lane on. That last column offers the eligible nodes that do not have a lane for the model yet, with nodes marked "· serving" first. Pick one and select Open lane.

open_lane creates the Lane account and its vault, and whoever sends it pays their rent. Anyone may open a lane, so an operator can also open one for its own eligible node.

The split

The split lists who receives each lane's revenue after the protocol fee. Each entry has:

FieldMeaning
recipientAtaThe recipient's canonical USDC account (the associated token account of recipientOwner and the USDC mint)
recipientOwnerThe recipient's wallet. Required when the USDC account does not exist yet; the transaction then creates it
bpsThe share in basis points. All entries sum to 10,000
lockedA locked entry can only be lowered or removed with that recipient's signature
roleowner, operator, base_model or contributor

The operator entry is special: at each distribution its share goes to the payout account of the operator whose node serves that lane, whatever account the entry names. Each lane therefore pays its own operator. The entry still names an existing USDC account when you list; the app uses a registered operator's payout account. Operator entries cannot be locked.

Base-model royalties

A model can set derivativeRoyaltyBps. Any model that names it as baseModelId must include a locked base_model entry of at least that many bps paying the base model's owner. The base owner's broker can then release the base model's key only to lanes whose split pays the royalty.

Changing the split

A split change takes effect 7 days after you propose it.

  1. Send propose_split with the new entries (modelId, split, and consentSigners when you lower a locked entry). One proposal can be pending at a time.
  2. The model's page shows "A new split takes effect" with the date.
  3. After 7 days, apply_split makes it the model's split. Sealroom's keeper applies matured splits automatically, and anyone may send apply_split.
  4. Until then, cancel_split withdraws the proposal.

Each lane switches to the new split at its next distribution. Funds already in a lane vault are distributed under the split that was active when they arrived; payments after the switch use the new split. Payouts deferred to a claim stay with their original recipients.

Revenue and distribution

Your model's page (Models, then the model) shows:

  • Model account: model, owner, manifest_hash, key_broker, min_tier, min_bond, runtimes and operator_allowlist ("any vetted operator" when empty).
  • Split: each entry's role, whether it is locked, its percentage and recipient account.
  • Lanes: for each lane, In the vault, Total in and Distributed, with Call and Distribute.
  • Distributions: each distribution's time, lane, gross amount and number of recipients.

Distribute sends distribute for that lane. One transaction pays the protocol fee, the operator's share and every split recipient, all at once or not at all. Anyone can send it, and the sender pays the network fee. You can distribute whenever a vault holds funds.

The API adds totals: revenue on a model gives totalIn, totalDistributed and claimable, and each lane reports vaultBalance, totalIn, totalDistributed, lastDistributedAt and its activeSplit.

Claims

A distribution pays each recipient straight into their canonical USDC account. When a recipient's account cannot receive (it is missing, frozen, or has changed owner), that recipient's share is held in the lane as a claim instead, and everyone else is paid as usual.

  1. If a recipient needs a claim account that does not exist yet, the distribute preview returns CLAIM_PREPARATION_REQUIRED. Send prepare_claim (laneId, recipientAta, recipientOwner) for that recipient first; anyone can. Then distribute again.
  2. The held amount appears in the lane's claimable list with the recipient and amount.
  3. The recipient sends claim with the laneId and recipientAta to receive it, into their canonical USDC account or the destinationAta they name, as the deployment's claim-destination policy allows.

Claims belong to their recipient and do not expire.