Components, trust boundaries, and the atomic sponsored-transfer path.
OSSR separates user intent, relay policy, sponsor signing, and on-chain settlement into narrow components. Each component validates the fields it is responsible for instead of trusting another layer.
The adapter call receives the transfer amount, recipient, sponsor fee, quote ID, expiry height, and optional memo. On success it performs both token movements:
amountSats from the origin to the recipient;
sponsorFee from the origin to the sponsor.
If either transfer fails, the adapter returns an error and the transaction does not partially settle. The user's exact-deny post-condition independently limits total sBTC outflow to amountSats + sponsorFee.
A relay can refuse service, return an unattractive price, or become unavailable. It cannot change the origin-signed call, exceed the post-condition, spend unrelated assets, or obtain the user's private key. Applications should support clear failure handling and, in a future multi-relay network, safe fallback to a newly quoted and newly signed transaction.
Version 1 accepts only Stacks testnet sponsored transactions and one reviewed sBTC adapter profile. Mainnet, arbitrary SIP-010 tokens, arbitrary contract calls, and cross-relay replay coordination are outside the current scope.
Open Stacks Sponsor Relay
A testnet protocol for sponsored sBTC transfers on Stacks.
OSSR lets a user send sBTC without holding STX. The user authorizes one exact token movement, while an independent relay supplies the STX network fee, adds the sponsor authorization, and broadcasts the completed transaction.
OSSR is an application-layer fee abstraction, not a change to Stacks consensus. STX remains the network fee asset. The relay temporarily supplies STX and receives the quoted reimbursement in sBTC inside the same adapter transaction.
Sponsored authorization already exists in Stacks, but every application otherwise needs to operate its own sponsor, fee policy, signing system, monitoring, and transaction validation. OSSR defines a reusable boundary between wallets and independent relay operators:
wallets keep control of user keys and sign only exact intent;
relays compete on policy, price, availability, and capacity;
the adapter makes the recipient payment and relay reimbursement atomic; and
applications integrate one narrow protocol instead of maintaining sponsor infrastructure.
OSSR is a prototype. Do not use mainnet funds, and do not treat a registered relay as trusted without independently validating its metadata, quote, and transaction.
Fetch /v1/info and validate the relay ID, network, sponsor, adapter, sBTC contract, policy limits, and active quote key against trusted configuration.
relay-info.ts
const info = await fetch(`${relayUrl}/v1/info`).then((response) => response.json());if (info.network !== 'testnet') throw new Error('Unsupported network');if (info.adapterContract !== expectedAdapter) throw new Error('Unexpected adapter');if (info.sbtcContract !== expectedSbtcContract) throw new Error('Unexpected sBTC contract');if (!info.quotesEnabled || !info.sponsorshipsEnabled) { throw new Error('Relay is not accepting requests');}
Do not accept an adapter or quote key merely because the relay advertises it. Compare metadata to a configured relay profile, registry record, or another trusted source.
Display the transfer amount, sponsor fee, maximum sBTC outflow, expiry height, and adapter before wallet approval. Verify the quote signature and ensure every signed field matches the request and trusted relay metadata.
Build the exact sponsored-transfer contract call and an exact-deny fungible-token post-condition for amount + sponsorFee. Then request an origin signature without broadcasting:
Poll GET /v1/sponsorships/{txid} until success or a terminal failure. Use bounded backoff rather than rapid polling, and preserve the transaction ID so status can be restored after a page reload.
The operator registry specification is draft. The final Clarity ABI and deployed contract are not live yet. Operators can run and harden testnet relays now, then register after the contract address and parameters are published.
Configure dedicated sponsor and quote-signing keys, the approved adapter and sBTC contracts, a Stacks API, and a Stacks Core simulation endpoint.
Minimum operational inputs include:
Input
Purpose
SPONSOR_PRIVATE_KEY
Adds sponsor authorization and pays STX fees
QUOTE_PRIVATE_KEY
Signs short-lived relay offers
STACKS_API_URL
Balance, fee, nonce, and transaction status access
STACKS_SIMULATION_API_URL
Pre-sign execution simulation
Adapter and sBTC principals
Exact allowlist and asset binding
CORS origins
Explicit browser clients permitted to call the relay
SBTC_LOG_SCALE_SATS
Transfer-size divisor for logarithmic pricing; defaults to 100
SBTC_LOG_GROWTH_SATS
Sats added per doubling; defaults to 2
Unless SBTC_SPONSOR_FEE_SATS sets a fixed override, the relay quotes
2 × ceil(log2(1 + ceil(amount / 100))), then raises that result to the configured
static cost floor (1 sat by default). Testnet uses faucet-funded STX, so live
market cost recovery is opt-in with SBTC_DYNAMIC_PRICING=true. Existing
relays should set it to false and restart to use stable testnet pricing. The active model, curve parameters, and
cost floor are published by /v1/info for accurate wallet previews.
npm run operator:serve
Verify the public identity and readiness surfaces:
Registration is permissionless, but activation requires valid fields, compatible capabilities, sufficient collateral, and current metadata. Registration is discovery—not endorsement—and clients must continue to validate every relay and signed quote.
Record exists but activation requirements are incomplete
active
Yes
Eligible for wallet discovery
inactive
No
Owner deactivated it or heartbeat became stale
withdrawing
No
Collateral withdrawal delay is running
closed
No
Collateral returned and record cannot reactivate
An active operator periodically submits owner-authorized heartbeats. The contract uses the current chain height rather than accepting a caller-supplied timestamp. The recommended initial staleness window is 500 blocks, though deployed parameters are authoritative.
Host metadata over HTTPS or content-addressed storage and register its exact hash. Clients treat it as untrusted display data even after hash verification.
Requesting withdrawal immediately makes the operator non-discoverable. Collateral remains locked through the configured delay—approximately seven days is recommended—before the owner can finalize and permanently close the record. Version 1 does not define slashing.
See the full operator guide for the infrastructure and lifecycle checklist.
Quote lifecycle
How an OSSR offer is created, bound, consumed, renewed, and reconciled.
A quote is a signed, short-lived offer from one relay for one exact origin intent. It is not a bearer voucher for arbitrary sponsorship.
protocol version, network, action, and reimbursement asset;
relay ID, sponsor principal, quote key ID, and policy version;
origin, recipient, amount, sponsor fee, and maximum STX network fee;
adapter contract, function, argument hash, quote ID, and expiry height.
Clients verify the quote key against trusted /v1/info metadata. A valid signature proves that the matching key signed the offer; it does not prove the relay is trustworthy or the price is acceptable.
The wallet must sign while the quote remains valid. When a quote expires before submission, request a fresh quote and rebuild the transaction because the quote ID, expiry, fee, and argument hash can change. Never attach a new quote ID to transaction bytes signed for an older quote.
Quotes are bound to one relay and sponsor. If an application falls back to another relay, it must obtain a new quote, rebuild the adapter call, and ask the wallet to sign a replacement transaction. Do not submit both versions concurrently.
Relay HTTP API
The narrow version 1 interface implemented by OSSR testnet relays.
The API uses JSON over HTTPS. Plain HTTP is acceptable only for loopback or isolated development environments.
The base URL has no trailing slash. Requests and responses use JSON with lower-camel-case fields. Protocol integers are decimal strings so JavaScript clients never lose precision.
Clients should tolerate new optional properties but fail closed on unknown protocol versions, networks, actions, reimbursement assets, or security-sensitive values.
All protocol integers are unsigned decimal strings. Addresses must be canonical testnet standard principals.
The response contains the signed quote, signing hash, signature, and quote public key. Clients resolve the quote's key ID against trusted metadata and validate the exact origin intent before constructing a transaction.
The relay validates the quote, origin signature, post-conditions, adapter arguments, fee policy, and simulation result before it adds the sponsor authorization and broadcasts.
A successful response returns the broadcast status, operator principal, transaction ID, and STX network fee. The completed sponsored transaction bytes are intentionally not returned.
GET /v1/sponsorships/{txid} combines relay state with the latest observed Stacks chain status. Clients should preserve unknown non-terminal fields for diagnostics and treat repeated not_found or replacement observations cautiously before declaring failure.
Relay errors include a stable machine-readable code and message. Dapps should distinguish invalid intent, expired quotes, unavailable operators, simulation failures, wallet incompatibility, and chain execution failures.
error response
{ "error": "QUOTE_EXPIRED", "message": "Quote has expired."}
HTTP status communicates the broad class; the error code determines product behavior. Do not show only “request failed” when the code can tell the user to reconnect a wallet, refresh a quote, adjust a fee cap, or retry later.
Security model
Validation layers, key boundaries, post-conditions, and failure assumptions.
OSSR assumes public relays receive adversarial traffic. Safety comes from overlapping checks at the wallet, client, relay, signer, adapter, and Stacks execution layers.
Separate from sponsor key and independently rotatable
Registry owner key
Manages operator record and collateral
Cold or governance-controlled account
Never log private keys, seed phrases, complete unsigned user intent containing secrets, or authentication tokens. Public keys, principals, quote IDs, transaction IDs, and policy versions are safe diagnostic identifiers.
A malicious relay can censor a user, refuse a valid request, withhold status, advertise stale metadata, or quote an unfavorable fee. Applications mitigate this with trusted metadata, explicit fee display, timeouts, observable errors, and safe relay fallback. Cryptography cannot force a relay to provide service.
The current deployment target is a grant-reviewable testnet proof of concept. Mainnet operation requires a completed security review, finalized registry contract and ABI, hardened key custody, rate limiting, monitoring, incident procedures, and reviewed deployment values.
Testing and verification
Validate contracts, relay policy, state stores, types, and the testnet transaction path.
Run tests from the repository root after installing dependencies.
Start the relay and inspect its public boundaries:
npm run operator:servecurl -fsS http://127.0.0.1:3002/v1/infocurl -fsS http://127.0.0.1:3002/health/livecurl -fsS http://127.0.0.1:3002/health/ready
/health/live proves the HTTP process is alive. /health/ready additionally checks operator dependencies and sponsor balance, so it can return 503 while liveness remains healthy.
With funded testnet accounts, deployed adapter values, quote signing, and simulation configured:
npm run operator:client -- --wait
A successful run prints submission and confirmation events. Verify that the user spends zero STX, the recipient receives the requested sats, the sponsor receives the quoted sats, and the sponsor pays the STX network fee.
Include rejection tests for wrong network, invalid origin signatures, unsupported contracts, mismatched quote arguments, weak post-conditions, excessive fees, expired quotes, duplicate submissions, stale simulations, and insufficient balances. A rejection before sponsor signing must not consume a sponsor nonce unnecessarily.
Troubleshooting
Diagnose relay, wallet, quote, simulation, and chain-status failures.
Common causes are insufficient sponsor STX, an unreachable Stacks API, or upstream HTTP 429 rate limiting. Liveness can remain healthy while readiness returns 503.
SIMULATION_FAILED means the completed sponsored transaction would not return a successful adapter result. Check the user's sBTC balance, recipient, adapter and token principals, amount plus sponsor fee, expiry, and exact post-condition.
SIMULATION_UNAVAILABLE, SIMULATION_STALE, or SIMULATION_INVALID_RESPONSE indicates the relay could not obtain trustworthy simulation evidence. Operators should inspect their Stacks Core RPC version, authorization token, connectivity, and sync height.
For LAN development, bind the relay to 0.0.0.0, expose port 3002, and include the interface origin in OSSR_CORS_ALLOWED_ORIGINS. Public relays must use HTTPS and explicit CORS origins.
x402 payments for agents
Configure a paid relay, connect an agent wallet, authorize sponsorship payments, and recover safely from retries.
Agents can use x402 v2 to pay OSSR relay service fees. The relay sponsors the Stacks transaction and delegates the separate x402 payment to an external facilitator. This guide covers both relay operators and agent developers.
The service fee is additional to the signed quote’s atomic sBTC or USDCx reimbursement. The Stacks transfer currently runs on testnet; x402 uses the payment network configured by the operator. This integration does not add a native Stacks x402 payment scheme.
Complete operator setup: a funded Stacks sponsor, quote-signing key, deployed supported adapter, and transaction simulation.
Choose a facilitator supporting x402 v2, the exact scheme, your payment network and token, and JSON /verify and /settle requests without authentication. The relay does not currently send facilitator API credentials.
Fund the agent’s Stacks account with the transfer asset plus reimbursement. Fund its x402-compatible account with the service-payment asset. These can be separate wallets on different networks.
Approve a trusted relay profile containing its public URL, quote key, adapter, payment network, token, recipient, signing metadata, timeout, and spending limits.
Start with test assets on both networks. The Node.js example below uses an EVM payment signer; other networks require their own compatible x402 scheme implementation.
For an EVM test deployment, eip155:84532 identifies Base Sepolia. Verify the token address and signing metadata with your facilitator. With six token decimals, 10000 base units means 0.01 tokens. The payment network is configured independently of Stacks testnet.
Setting
Meaning
X402_ENABLED
Exactly true enables payment requirements; disabled by default
X402_FACILITATOR_URL
Base URL; the relay appends /verify and /settle
X402_RESOURCE_URL
Canonical public sponsorship URL advertised to agents
X402_NETWORK
Payment network in CAIP-2 form
X402_ASSET
Payment asset identifier supported by the facilitator
X402_PAY_TO
Wallet receiving the service fee
X402_AMOUNT
Positive integer service fee in token base units
X402_EXTRA
Optional JSON signing metadata; omit if your scheme needs none
X402_MAX_TIMEOUT_SECONDS
Authorization timeout advertised in the challenge; defaults to 60
X402_TIMEOUT_MS
Each facilitator request’s timeout; defaults to 15000 milliseconds
X402_STORE_PATH
Durable bindings and receipts; defaults to .ossr/x402-payments.json
Browser agents also need their origin in OSSR_CORS_ALLOWED_ORIGINS. The relay allows PAYMENT-SIGNATURE and exposes PAYMENT-REQUIRED and PAYMENT-RESPONSE to those origins. Server agents do not need CORS.
Start from the repository root:
npm run operator:serve
Check GET /v1/info: its x402 property advertises the resource and requirements. Both POST /v1/sponsorships and the legacy /v1/sponsor alias require payment. Quotes, discovery, health, and status remain accessible without payment.
Follow transaction integration to obtain and verify a quote, build the exact adapter call and deny post-condition, and sign the origin authorization without broadcasting. An autonomous agent uses its approved Stacks signing service in place of browser-wallet approval.
Verify relay identity, quote signature, origin, recipient, amount, adapter, sponsor fee, and block expiry against the approved intent. The quote’s sponsor fee caps reimbursement; the x402 budget separately caps the service payment.
Save the verified request as sponsorship.json, replacing the placeholders:
Keep the exact serialized request throughout payment and retries. Never send origin keys to the relay or broadcast an incomplete sponsored transaction.
An enabled relay returns HTTP 402 with a base64 JSON PAYMENT-REQUIRED header and a matching JSON body. Before authorizing, check protocol version 2, scheme exact, trusted resource URL, network, token, recipient, timeout, signing metadata, and payment amount. Reserve the amount against the agent’s total budget.
The selected accepted requirements must be copied unchanged into the payment payload, including extra. Use a compatible signer to generate the cryptographic authorization; an unsigned object cannot authorize payment. See the x402 HTTP specification for the wire format.
Keep the installed SDK version locked. The official client example demonstrates scheme registration. This example explicitly handles the challenge so it can save a payment authorization for later recovery.
Supply these settings through a secret manager or an untracked agent.env file:
The code expects a previously verified sponsorship.json. Use a dedicated test wallet for local setup. In production, replace local key access with your approved wallet signer, and implement atomic total-budget reservation before signing. Keep secrets out of prompts, browser bundles, source control, and logs.
pay-sponsorship.mjs
import { readFile, writeFile } from 'node:fs/promises';import { isDeepStrictEqual } from 'node:util';import { x402Client } from '@x402/core/client';import { ExactEvmScheme } from '@x402/evm/exact/client';import { privateKeyToAccount } from 'viem/accounts';const required = (name) => { const value = process.env[name]; if (!value) throw new Error(`Missing ${name}`); return value;};const endpoint = `${required('RELAY_URL').replace(/\/$/, '')}/v1/sponsorships`;const body = await readFile('sponsorship.json', 'utf8');const maxUnits = required('PAYMENT_MAX_UNITS');if (!/^[1-9][0-9]*$/.test(maxUnits)) throw new Error('Invalid budget');const decode = (value) => JSON.parse(Buffer.from(value, 'base64').toString());const post = (signature) => fetch(endpoint, { method: 'POST', redirect: 'error', headers: { 'Content-Type': 'application/json', ...(signature ? { 'PAYMENT-SIGNATURE': signature } : {}), }, body, signal: AbortSignal.timeout(60000),});const initial = await post();if (initial.status !== 402) throw new Error(`Expected 402, got ${initial.status}`);const header = initial.headers.get('PAYMENT-REQUIRED');if (!header) throw new Error('Missing challenge');const challenge = decode(header);const r = challenge.accepts?.[0];if ( challenge.x402Version !== 2 || challenge.accepts?.length !== 1 || challenge.resource?.url !== endpoint || r?.scheme !== 'exact' || r.network !== required('PAYMENT_NETWORK') || r.asset !== required('PAYMENT_ASSET') || r.payTo !== required('PAYMENT_RECIPIENT') || !/^[1-9][0-9]*$/.test(r.amount) || BigInt(r.amount) > BigInt(maxUnits) || r.maxTimeoutSeconds !== Number(required('PAYMENT_MAX_TIMEOUT_SECONDS')) || !isDeepStrictEqual(r.extra, process.env.PAYMENT_EXTRA ? JSON.parse(process.env.PAYMENT_EXTRA) : undefined)) throw new Error('Challenge violates approved payment policy');// Reserve r.amount in the agent's durable total budget before signing.const signer = privateKeyToAccount(required('PAYMENT_PRIVATE_KEY'));const client = new x402Client().register(r.network, new ExactEvmScheme(signer));const payment = await client.createPaymentPayload(challenge);const signature = Buffer.from(JSON.stringify(payment)).toString('base64');// Save BEFORE submitting; retain this file if the request times out.await writeFile('payment-request.json', JSON.stringify({ endpoint, body, signature }), { mode: 0o600 });const response = await post(signature);const receipt = response.headers.get('PAYMENT-RESPONSE');const result = await response.json();await writeFile('payment-result.json', JSON.stringify({ httpStatus: response.status, receipt: receipt ? decode(receipt) : null, result,}), { mode: 0o600 });if (!response.ok) throw new Error(`Relay rejected: ${result.error}`);console.log('Stacks transaction:', result.transactionId);
Run node --env-file=agent.env pay-sponsorship.mjs. Ignore agent.env, authorization, and receipt files in source control. Use a separate durable job directory per request to avoid overwriting an unresolved payment attempt.
Run node --env-file=agent.env retry-sponsorship.mjs only after checking whether reconciliation is needed. Use bounded backoff and a maximum retry count.
The relay verifies payment, validates and simulates the Stacks transaction, then settles payment before broadcasting. PAYMENT-RESPONSE contains the service-payment receipt. The response body’s transactionId identifies the Stacks transaction.
BROADCAST is not chain confirmation. Poll GET /v1/sponsorships/{txid} with bounded backoff until a terminal outcome. Persist both transaction IDs and the quote ID in the agent’s job record. Payment success does not prove transfer success.
Operator must restore the store or reconcile capacity
SPONSORSHIP_IN_PROGRESS
Wait for relay reconciliation and inspect transaction status
Network timeout or broadcast error
Keep the existing signature and receipt; reconcile before retrying the same request
Quote expired or transaction changed
Reconcile the old payment first; a new quote requires a newly signed Stacks transaction and separate payment authorization
Settlement and broadcast are separate operations. Payment may settle even if broadcast fails, and there is no automatic refund. Receipt reuse still depends on quote and transaction policy checks; an expired quote can prevent recovery through the original request. Do not spend again just because a tool returned an error.
Expose a narrow agent tool such as sponsorApprovedTransfer, restricted to approved intents and trusted relays. Keep signing in a wallet service. Enforce recipient and token allowlists, per-payment caps, total budgets, and approval thresholds. Include reimbursement in total job cost. Concurrent jobs must reserve budget atomically, and unresolved outcomes must pause further spending for that job.
Store requests, authorizations, receipts, and transaction IDs outside conversation memory. Run one relay process per X402_STORE_PATH; preserve it across restarts and back it up with the quote store. Programmatic API users must supply x402.storePath for persistence. The store has a 10,000-binding limit and fails closed at capacity. Archive only under a retention policy preserving facilitator replay guarantees.
npm run typechecknpm run test:x402npm run test:relay-policynpm run test:quote-store
Exercise one test-wallet payment end to end before enabling autonomous spending: inspect the challenge, settle the service payment, confirm the Stacks transfer, retry the exact authorization, and ensure it cannot fund a different request. Repository tests mock facilitator responses; verify your deployed facilitator separately.