Call a model

A call goes to one node that serves the model. The node quotes its price with HTTP 402, your wallet signs one USDC payment to the lane vault for that model and node, and the node answers from inside its attested enclave. You can do this in the app or from code.

In the app

Before you start

Connect a wallet that holds USDC on the app's network (Connect a wallet). A paid call needs no SOL: the facilitator pays the network fee for the payment transaction.

Steps

  1. Open Call a model from the top bar, or from the Call a model button on the Registry page. A model's page has a Call this model button, and each lane row on it has a Call link that opens the page with that model and node already chosen.

  2. In Model, pick the model. Each entry shows its price, such as "0.025000 USDC/call".

  3. In Node (lane), pick the node. Each entry names the node and the lane, for example "node 7Qm3…k9Fa · lane 3xTe…v1Lp". The list holds the open lanes for that model.

  4. Write your request in Prompt (up to 2,048 characters).

  5. Check the What you are paying for panel on the right:

    FieldShows
    modelThe model you picked
    priceThe price per call in USDC
    nodeThe node and its GPUs, for example "2 × H100"
    pays_toThe lane vault that receives your payment
  6. Select Pay and call. The status line moves through three steps:

    • "Asking the node for its price…" The app sends your request to the node, which replies 402 with the price.
    • "Sign 0.025000 USDC to the lane vault in your wallet…" Your wallet shows the payment. Approve it.
    • "Paying and waiting for the enclave's answer…" The app sends the request again with your signed payment. The node verifies it, runs the call and settles the payment.
  7. The Answer section shows the model's reply. Under it, the Payment record shows the settlement: success, the Solana transaction signature, the network, the payer (your address), the sessionId, the amount and the pay_to vault.

See the lane vault fill → opens the model's page, where the lane's In the vault and Total in columns include your payment.

The app pays fixed-price models. Models priced per token (shown as "metered" in the model list) are called from code with the upto scheme, described below.

What the 402 price is

A node quotes every call with an x402 version 2 challenge. The response has status 402 Payment Required, a JSON body, and the same JSON base64-encoded in the PAYMENT-REQUIRED header:

{
  "x402Version": 2,
  "resource": { "url": "https://node.example.com/v1/chat/completions", "mimeType": "application/json" },
  "accepts": [
    {
      "scheme": "exact",
      "network": "solana:<genesis hash prefix>",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "payTo": "<lane vault>",
      "amount": "25000",
      "maxTimeoutSeconds": 120,
      "extra": {
        "feePayer": "<facilitator>",
        "memo": "sealroom:<sessionId>",
        "recentBlockhash": "<blockhash>",
        "lastValidBlockHeight": "<height>",
        "sessionId": "<uuid>",
        "laneId": "<lane>"
      }
    }
  ]
}
FieldMeaning
schemeexact for a fixed price per call, upto for a metered call with a ceiling
networkThe Solana network, as solana: followed by the first 32 characters of its genesis hash
assetThe USDC mint
payToThe lane's USDC vault. Pay this token account directly
amountBase units of USDC (6 decimals). For exact, the model's price per call. For upto, your ceiling
maxTimeoutSecondsHow long the quote stays open: 120 seconds
extra.feePayerThe facilitator, which pays the network fee and submits the transaction
extra.memoThe invoice memo that binds the payment to this quote
extra.recentBlockhash, extra.lastValidBlockHeightThe blockhash the payment transaction uses
extra.sessionIdThe payment session for this request
extra.laneIdThe lane the payment is for
extra.paymentChannel, extra.channelIdFor upto: the payment channel that escrows your ceiling

Where the payment goes

Every payment lands in the lane vault, a USDC token account owned by the lane for this (model, node) pair. Nothing is split at call time. The funds wait in the vault until anyone runs distribute on the lane, which in one transaction pays the protocol fee (500 bps at launch), the share of the operator that runs the node, and every other recipient in the model's split. Fees and distribution has the arithmetic.

The node follows a fixed order for each paid request:

  1. Check that the signed payment matches the quote exactly.
  2. Check that the node is still eligible to serve the model: valid attestation, registered platform, allowed runtime, operator in good standing.
  3. Run the call inside the enclave.
  4. Check eligibility again, then settle the payment on Solana through the facilitator.
  5. Return the answer with a PAYMENT-RESPONSE header that carries the settlement receipt.

From code

Callers that are services or agents use the same flow over HTTP. The TypeScript client @sealroom/contract reads the registry and builds the payment; the TypeScript client page covers it in full.

npm install @sealroom/contract

1. Find a model, a lane and the node's endpoint

import { createSealroomClient } from '@sealroom/contract'

const api = createSealroomClient({ baseUrl: '<api-url>' })

const models = await api.list('models', { limit: 20 })
if (!models.ok) throw new Error(models.error.message)
const model = models.data.items[0]

const lanes = await api.list('lanes', { modelId: model.id, status: 'open' })
if (!lanes.ok || !lanes.data.items.length) throw new Error('No open lane for this model')
const lane = lanes.data.items[0]

const node = await api.detail('nodes', lane.nodeId)
if (!node.ok) throw new Error(node.error.message)
const endpoint = `${node.data.endpoint.replace(/\/$/, '')}/chat/completions`

A node's endpoint is the base URL it registered, such as https://node.example.com/v1. Calls go to /chat/completions under it.

2. Check the node's attestation

Before you send a prompt, confirm that the node may serve this model right now. The eligibility endpoint applies the same policy rules the key broker and the node apply:

const check = await api.eligibility(model.id, node.data.id)
if (!check.ok || !check.data.eligible) throw new Error(`Not eligible: ${check.ok ? check.data.reasons.join(', ') : check.error.message}`)

const attestation = await api.detail('attestations', node.data.currentAttestationId!)
if (!attestation.ok) throw new Error(attestation.error.message)
const a = attestation.data
CheckFieldExpect
The attestation is currenta.status, a.expiresAtvalid (or expiring_soon in its last hour), expiry in the future
Three of five verifiers signeda.signingVerifiersAt least 3 keys
The hardware is up to datea.tcbStatusUpToDate
The node runs a runtime the owner allowsa.runtimeMeasurementListed in model.policy.allowedRuntimes
Your TLS session ends inside the enclavea.tlsPublicKeyHashThe SHA-256 of the node's 32-byte TLS public key
Usage vouchers come from the enclavea.voucherPublicKeyEqual to the signer of any usage voucher you receive

The checks array in the eligibility answer lists each rule with passed, actual and required, so you can show exactly why a node qualifies.

3. Ask for the price

Send the request without payment. The body has these fields:

FieldTypeMeaning
requestIdUUIDYour id for this call. Reuse it if you retry
modelIdaddressThe Model account
nodeIdaddressThe Node account
laneIdaddressThe Lane account for this model and node
calleraddressYour wallet
schemeexact or uptoexact for fixed-price models, upto for metered models
ceilingbase unitsFor upto only: the most you will pay, at most the model's maxPerCall
promptstring1 to 2,048 characters
import { Keypair } from '@solana/web3.js'
import { x402RequiredSchema } from '@sealroom/contract'

const wallet = Keypair.fromSecretKey(callerSecretKey) // your agent's key

const body = JSON.stringify({
  requestId: crypto.randomUUID(),
  modelId: model.id,
  nodeId: node.data.id,
  laneId: lane.id,
  caller: wallet.publicKey.toBase58(),
  scheme: 'exact',
  prompt: 'Classify this clause under the contract taxonomy: …',
})

const quote = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body })
if (quote.status !== 402) throw new Error(`Expected a price, got ${quote.status}`)
const challenge = x402RequiredSchema.parse(await quote.json())
const requirements = challenge.accepts[0]

4. Build and sign the payment

buildX402PaymentTransaction builds the exact transaction the facilitator accepts: two compute-budget instructions, the USDC transfer from your canonical USDC account to payTo (or the escrow of your ceiling for upto) and the invoice memo, with the facilitator as fee payer. Sign it with your wallet and wrap it for the header:

import {
  buildX402PaymentTransaction,
  buildX402PaymentPayload,
  encodeX402PaymentSignature,
} from '@sealroom/contract/solana'

const tx = buildX402PaymentTransaction(requirements, wallet.publicKey)
tx.sign([wallet]) // signs the caller's slot only; the facilitator signs its own

const paymentSignature = encodeX402PaymentSignature(
  buildX402PaymentPayload({ requirements, resource: challenge.resource.url, signedTransaction: tx }),
)

Send the transaction exactly as built. Do not add instructions and do not submit it yourself: the facilitator checks the layout, the amount, the destination, the memo and the signers, then adds its fee-payer signature and submits it.

5. Send the request again with PAYMENT-SIGNATURE

Send the identical body with the payment header:

const paid = await fetch(endpoint, {
  method: 'POST',
  headers: { 'content-type': 'application/json', 'PAYMENT-SIGNATURE': paymentSignature },
  body,
})
if (!paid.ok) throw new Error(JSON.stringify(await paid.json()))

const completion = await paid.json()
console.log(completion.choices[0].message.content)

const receipt = JSON.parse(Buffer.from(paid.headers.get('PAYMENT-RESPONSE')!, 'base64').toString('utf8'))
console.log(receipt.transaction) // the settlement's Solana signature

The answer follows the OpenAI chat completion shape:

{
  "id": "<sessionId>",
  "object": "chat.completion",
  "model": "<served model>",
  "choices": [{ "index": 0, "message": { "role": "assistant", "content": "…" }, "finish_reason": "stop" }]
}

The PAYMENT-RESPONSE header decodes to the settlement: success, transaction (the Solana signature), network, payer, sessionId, and for upto the refundTransaction that returned the unused part of your ceiling.

A node that cannot accept a request answers with a JSON body { "success": false, "errorReason": "…" } that names the reason.

Retrying a call

Keep the same requestId and the same body when you retry. The node returns the same quote for them, and a payment settles once.

Metered calls (upto)

A model priced per token has price.scheme of upto, with inputPerMillionTokens, outputPerMillionTokens and maxPerCall. Call it with scheme: 'upto' and a ceiling:

const body = JSON.stringify({ requestId, modelId, nodeId, laneId, caller, scheme: 'upto', ceiling: '50000', prompt })

The payment escrows your ceiling in the Solana payment-channels program (CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX). After the call, the enclave signs a usage voucher for the actual usage with its voucher key. Settlement pays that charge to the lane vault and returns the unused part of the ceiling to you. The charge is:

charge = ceil((input_tokens * inputPerMillionTokens + output_tokens * outputPerMillionTokens) / 1000000) charge <= maxPerCall

The answer carries the voucher (with laneId, channelId, requestId, nonce, cumulativeAmount, expiresAt, attestationId, attestationRevision and signer) and its voucherSignature. The voucher's signer is the voucherPublicKey of the node's attestation, so the operator cannot raise the charge without the enclave's key.

Wallets in the browser

In a browser app, pass a wallet that exposes publicKey and signTransaction to buildX402PaymentSignature, which builds, signs and encodes in one step:

import { buildX402PaymentSignature } from '@sealroom/contract/solana'

const header = await buildX402PaymentSignature({ requirements, resource: challenge.resource.url, wallet })

For a Wallet Standard wallet that returns signed bytes, call buildX402PaymentTransaction, pass tx.serialize() to the wallet's signTransaction, and give the signed bytes to buildX402PaymentPayload as signedTransaction.