OSSR Docs

OSSR Documentation

Open Stacks Sponsor Relay

Architecture

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.

Components

ComponentResponsibilityMust never receive
DappRelay selection, quote display, transaction construction, status UXSponsor or quote private keys
Browser walletProtect the origin key and approve the exact adapter callSponsor private key
Relay APIQuote policy, transaction validation, simulation, sponsorship, broadcastUser private key or seed phrase
Sponsor signerAdd only the sponsor authorization after validationArbitrary unvalidated transactions
Adapter contractAtomically transfer sBTC to recipient and relayOff-chain secrets
RegistryPublish operator discovery records and capabilitiesUser transactions or wallet credentials

Transaction path

Dapp              Wallet              Relay                 Stacks
  | request quote   |                   |                      |
  |------------------------------------>|                      |
  | signed quote    |                   |                      |
  |<------------------------------------|                      |
  | build exact call|                   |                      |
  |---------------->|                   |                      |
  | origin-signed bytes                 |                      |
  |<----------------|                   |                      |
  | submit quote + transaction          |                      |
  |------------------------------------>| validate + simulate  |
  |                                     | sponsor + broadcast  |
  |                                     |--------------------->|
  | transaction ID                      |                      |
  |<------------------------------------|                      |

The relay broadcasts the completed transaction itself. It does not return completed sponsored bytes for the client to hold or rebroadcast.

Atomic settlement

The adapter call receives the transfer amount, recipient, sponsor fee, quote ID, expiry height, and optional memo. On success it performs both token movements:

  1. amountSats from the origin to the recipient;
  2. 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.

Trust model

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.

Testnet boundary

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.

Watch the OSSR overview

Why OSSR exists

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.

How it works

  1. A dapp loads trusted relay metadata from GET /v1/info.
  2. The dapp requests a signed quote for an exact transfer.
  3. The wallet signs the allowlisted adapter call as the transaction origin.
  4. The quoted relay validates, simulates, sponsors, and broadcasts it.
  5. The dapp polls the relay until the transaction reaches a terminal status.

Version 1 scope

PropertyValue
NetworkStacks testnet
ActionSponsored sBTC transfer
Network feePaid by the relay in STX
Relay reimbursementPaid atomically in sBTC sats
User custodyNon-custodial
Relay selectionApplication or user configured
Transaction targetAllowlisted OSSR adapter only
Quote validityShort-lived and single-use

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.

Choose a path

Follow OSSR

Follow OSSR on Nostr for project updates, testnet progress, and operator and wallet integration work.

Dapp integration

Request a quote, build the sponsored call, ask the wallet to sign, and submit through a relay.

Prerequisites

  • A compatible browser wallet such as Leather or Xverse
  • A trusted OSSR testnet relay base URL
  • @stacks/connect and @stacks/transactions
  • The reviewed OSSR adapter and canonical testnet sBTC contract principals

1. Load relay metadata

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.

2. Request a quote

request-quote.ts
const response = await fetch(`${relayUrl}/v1/quotes`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    origin,
    recipient,
    amountSats: '1000',
    maxSponsorFeeSats: '20',
  }),
});

const signedQuote = await response.json();

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.

3. Ask the wallet to sign

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:

sign.ts
const walletResult = await request('stx_callContract', {
  contract: prepared.contract,
  functionName: prepared.functionName,
  functionArgs: prepared.functionArgs,
  postConditions: prepared.postConditions,
  postConditionMode: 'deny',
  sponsored: true,
  network: 'testnet',
});

Extract the raw origin-signed bytes from the wallet result. The dapp must not broadcast them directly because the sponsor authorization is incomplete.

4. Submit and track

submit.ts
const sponsorship = await fetch(`${relayUrl}/v1/sponsorships`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    quoteId: signedQuote.quote.quoteId,
    transaction: rawTransaction,
    user: origin,
  }),
}).then((response) => response.json());

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.

User experience states

StateRecommended UI
Loading metadata“Checking relay” with no signing action enabled
Quote readyExact amount, fee, maximum outflow, and expiry
Wallet open“Waiting for wallet” with duplicate actions disabled
BroadcastTransaction ID and explorer link
PendingCurrent chain status with background polling
ConfirmedRecipient amount, sponsor fee, and block height
Rejected or failedStable error title, technical code, and a concrete recovery action

Security checklist

  • Never request or transmit wallet private keys or seed phrases.
  • Never sponsor an arbitrary contract call.
  • Verify the signed quote and every quote-bound transaction field.
  • Submit a transaction to only its quoted relay.
  • Treat quote IDs as single-use idempotency boundaries.
  • Rebuild and re-sign whenever a quote is renewed or the relay changes.
  • Keep origin and sponsor keys out of application logs and analytics.

Operator registration

Run a relay today and prepare for permissionless registry activation.

Current status

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.

Run the relay

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:

InputPurpose
SPONSOR_PRIVATE_KEYAdds sponsor authorization and pays STX fees
QUOTE_PRIVATE_KEYSigns short-lived relay offers
STACKS_API_URLBalance, fee, nonce, and transaction status access
STACKS_SIMULATION_API_URLPre-sign execution simulation
Adapter and sBTC principalsExact allowlist and asset binding
CORS originsExplicit browser clients permitted to call the relay
SBTC_LOG_SCALE_SATSTransfer-size divisor for logarithmic pricing; defaults to 100
SBTC_LOG_GROWTH_SATSSats 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:

curl -fsS https://relay.example/v1/info
curl -fsS https://relay.example/health/ready

Monitor readiness, rejected requests, successful broadcasts, confirmation latency, STX spent, and sBTC reimbursed. Treat persistent 429, stale simulation, sponsor nonce ambiguity, or low-balance events as operational incidents.

Registration record

The version 1 registry is designed to record:

  • owner principal and quote public key;
  • locked STX collateral and lifecycle status;
  • metadata URI and verified content hash;
  • supported protocol versions and capabilities;
  • advertised pricing and sponsorship capacity; and
  • latest owner-authorized heartbeat height.

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.

Lifecycle

StateDiscoverableMeaning
pendingNoRecord exists but activation requirements are incomplete
activeYesEligible for wallet discovery
inactiveNoOwner deactivated it or heartbeat became stale
withdrawingNoCollateral withdrawal delay is running
closedNoCollateral 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.

Metadata document

operator-metadata.json
{
  "schema_version": "1",
  "operator_id": "18",
  "name": "Example OSSR Operator",
  "api_url": "https://relay.example",
  "region": "us-east",
  "description": "Independent OSSR relay operator",
  "website": "https://relay.example"
}

Host metadata over HTTPS or content-addressed storage and register its exact hash. Clients treat it as untrusted display data even after hash verification.

Withdrawal

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.

Bound fields

The signed quote binds at least:

  • 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.

State progression

StateMeaning
IssuedQuote is persisted and returned to the client
ReservedMatching transaction bytes atomically claimed the quote ID
ProcessingSponsor signing or broadcast is in progress
CompletedBroadcast result is durably associated with the quote
ExpiredChain height exceeded the signed expiry
AmbiguousBroadcast transport failed and reconciliation is required

Expiry and renewal

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.

Idempotency

The quote ID is the sponsorship idempotency key:

  • the first matching submission reserves it;
  • an exact retry after completion returns the stored result;
  • different transaction bytes receive an idempotency mismatch;
  • simultaneous matching submissions receive an in-progress response; and
  • consumed or expired quotes cannot sponsor a different transaction.

Changing relays

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.

Endpoints

MethodPathPurpose
GET/v1/infoRelay identity, contracts, limits, and quote keys
POST/v1/quotesCreate a signed, expiring quote
POST/v1/sponsorshipsValidate, sponsor, and broadcast
GET/v1/sponsorships/{txid}Return relay and observed chain status
GET/health/liveProcess liveness
GET/health/readyDependency and signer readiness

Relay metadata

GET /v1/info publishes the values a client needs before requesting a quote:

GET /v1/info
{
  "apiVersion": "1",
  "relayId": "example-relay",
  "network": "testnet",
  "sponsorPrincipal": "ST...",
  "supportedActions": ["sbtc-transfer"],
  "adapterContract": "ST....sbtc-sponsored-transfer-v1",
  "sbtcContract": "SN....sbtc-token",
  "quoteKeys": [
    { "keyId": "quote-2026-01", "publicKey": "0x...", "status": "active" }
  ],
  "quotesEnabled": true,
  "sponsorshipsEnabled": true
}

Clients should tolerate new optional properties but fail closed on unknown protocol versions, networks, actions, reimbursement assets, or security-sensitive values.

Quote request

POST /v1/quotes
{
  "origin": "ST...",
  "recipient": "ST...",
  "amountSats": "1000",
  "maxSponsorFeeSats": "20"
}

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.

Sponsorship request

POST /v1/sponsorships
{
  "quoteId": "0x...",
  "transaction": "0x...",
  "user": "ST..."
}

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.

Status response

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.

Transport behavior

  • Public relays use HTTPS with normal certificate validation.
  • Browser relays use explicit CORS allowlists and do not require cookies.
  • Quote, sponsorship, status, and readiness responses use Cache-Control: no-store.
  • Request bodies are bounded; oversized bodies receive 413.
  • Unsupported JSON content types receive 415.
  • Clients should attach a diagnostic request ID but must not use it as an idempotency key.

Retry rules

Safe retry behavior depends on the stage:

OperationRetry guidance
GET /v1/infoRetry with ordinary cache and network policy
POST /v1/quotesA retry may create a new quote; use only the selected response
POST /v1/sponsorshipsRetry the exact same quote ID and transaction bytes
Status lookupRetry with bounded backoff until terminal or application timeout

Errors

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.

Core invariants

  1. The user signs only a sponsored testnet contract call to the configured adapter.
  2. The relay cannot alter the origin authorization after the wallet signs it.
  3. The transaction contains exactly one sBTC post-condition with deny mode and exact outflow.
  4. The relay signs only after quote, payload, fee, origin, simulation, and policy validation pass.
  5. Completed bytes are broadcast by the relay and never returned to clients.
  6. Quote IDs are durable, single-use idempotency keys.

Key separation

KeyPurposeRecommended handling
Origin keyAuthorizes the user's adapter callBrowser wallet only
Sponsor keyPays STX and completes sponsored authorizationIsolated relay signer
Quote keySigns relay offersSeparate from sponsor key and independently rotatable
Registry owner keyManages operator record and collateralCold 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.

Relay validation order

Before sponsor signing, the relay checks:

  • request size, encoding, network, and sponsored authorization type;
  • origin signature and claimed user principal;
  • stored quote signature, expiry, status, and exact intent binding;
  • adapter principal, function name, arguments, and memo bounds;
  • exact-deny sBTC post-condition and maximum total outflow;
  • sponsor STX balance and configured maximum fee;
  • current nonce state and durable quote reservation; and
  • a fresh Stacks Core simulation with matching transaction ID and successful result.

What remains possible

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.

Production warning

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.

Static and contract checks

npm run typecheck
npm run clarinet:check
npm run test:contracts

Relay correctness

npm run test:relay-policy
npm run test:quote-store
npm run test:nonce-store
npm run test:transaction-state
npm run test:simulation-response

These suites cover malformed transactions, origin mismatch, quote policy, replay and idempotency state, sponsor nonce durability, simulation validation, and transaction lifecycle transitions.

Local readiness

Start the relay and inspect its public boundaries:

npm run operator:serve
curl -fsS http://127.0.0.1:3002/v1/info
curl -fsS http://127.0.0.1:3002/health/live
curl -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.

End-to-end testnet transfer

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.

Controlled failures

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.

Operator unavailable

OPERATOR_UNAVAILABLE means the relay intentionally refused to sign because a readiness check failed. Inspect:

curl -fsS http://127.0.0.1:3002/health/live
curl -i http://127.0.0.1:3002/health/ready

Common causes are insufficient sponsor STX, an unreachable Stacks API, or upstream HTTP 429 rate limiting. Liveness can remain healthy while readiness returns 503.

Quote rejected

CodeMeaningAction
QUOTE_EXPIREDChain height passed the signed expiryRequest a fresh quote and rebuild the transaction
QUOTE_ALREADY_USEDQuote ID already completedStart a new transfer
QUOTE_ORIGIN_MISMATCHConnected wallet differs from quote originReconnect the original wallet or request a new quote
SPONSOR_FEE_TOO_HIGHQuote exceeds the user's configured capIncrease the cap deliberately or wait for pricing to change
QUOTE_TRANSACTION_MISMATCHSigned bytes do not match quoted intentRebuild from the exact returned quote

Wallet signing failed

  • Confirm the wallet is on Stacks testnet.
  • Confirm it supports sponsored stx_callContract requests that return raw signed bytes without broadcasting.
  • Reject stale wallet prompts before retrying.
  • Do not change recipient, amount, memo, quote, or post-conditions after requesting approval.

Simulation failed

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.

Chain execution failed

Status or adapter errorLikely cause
abort_by_post_conditionSigned asset constraint did not match actual movement
ERR_FEE_TRANSFER_FAILED (u109)Origin could not reimburse the sponsor
ERR_RECIPIENT_TRANSFER_FAILED (u110)Origin could not pay the recipient
dropped_replace_by_feeTransaction was superseded by another sponsor-nonce transaction
not_foundIndexer has not observed the broadcast or propagation is ambiguous

Treat not_found and replacement observations as provisional until repeated status checks confirm a terminal outcome.

Browser cannot reach relay

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.

Prerequisites

  • 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.

1. Configure the relay

Add these settings to the relay’s .env.local. Replace all placeholders with your deployment’s values; these are not live service addresses.

Relay .env.local
X402_ENABLED=true
X402_FACILITATOR_URL=https://facilitator.example
X402_RESOURCE_URL=https://relay.example/v1/sponsorships
X402_NETWORK=eip155:84532
X402_ASSET=0xYOUR_PAYMENT_TOKEN_ADDRESS
X402_PAY_TO=0xYOUR_PAYMENT_RECIPIENT
X402_AMOUNT=10000
X402_EXTRA={"name":"USDC","version":"2"}
X402_MAX_TIMEOUT_SECONDS=60
X402_TIMEOUT_MS=15000
X402_STORE_PATH=.ossr/x402-payments.json

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.

SettingMeaning
X402_ENABLEDExactly true enables payment requirements; disabled by default
X402_FACILITATOR_URLBase URL; the relay appends /verify and /settle
X402_RESOURCE_URLCanonical public sponsorship URL advertised to agents
X402_NETWORKPayment network in CAIP-2 form
X402_ASSETPayment asset identifier supported by the facilitator
X402_PAY_TOWallet receiving the service fee
X402_AMOUNTPositive integer service fee in token base units
X402_EXTRAOptional JSON signing metadata; omit if your scheme needs none
X402_MAX_TIMEOUT_SECONDSAuthorization timeout advertised in the challenge; defaults to 60
X402_TIMEOUT_MSEach facilitator request’s timeout; defaults to 15000 milliseconds
X402_STORE_PATHDurable 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.

2. Prepare the Stacks request

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:

sponsorship.json
{
  "quoteId": "0xYOUR_QUOTE_ID",
  "transaction": "0xYOUR_ORIGIN_SIGNED_TRANSACTION",
  "user": "ST_YOUR_ORIGIN_ADDRESS"
}

Keep the exact serialized request throughout payment and retries. Never send origin keys to the relay or broadcast an incomplete sponsored transaction.

3. Inspect the challenge

curl -i https://relay.example/v1/sponsorships \
  -H 'Content-Type: application/json' \
  --data-binary @sponsorship.json

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.

4. Set up the agent client

In your agent project, install:

npm install @x402/core @x402/evm viem

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:

Agent environment
RELAY_URL=https://relay.example
PAYMENT_NETWORK=eip155:84532
PAYMENT_ASSET=0xYOUR_PAYMENT_TOKEN_ADDRESS
PAYMENT_RECIPIENT=0xYOUR_PAYMENT_RECIPIENT
PAYMENT_MAX_UNITS=10000
PAYMENT_MAX_TIMEOUT_SECONDS=60
PAYMENT_EXTRA={"name":"USDC","version":"2"}
PAYMENT_PRIVATE_KEY=0xYOUR_DEDICATED_TEST_PAYMENT_KEY

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.

5. Retry without signing again

Do not rerun the payment-creation script for an uncertain attempt. Reload the saved signature and exact request body:

retry-sponsorship.mjs
import { readFile, writeFile } from 'node:fs/promises';
const saved = JSON.parse(await readFile('payment-request.json', 'utf8'));
const trustedEndpoint = `${process.env.RELAY_URL?.replace(/\/$/, '')}/v1/sponsorships`;
if (saved.endpoint !== trustedEndpoint) throw new Error('Untrusted relay');
const response = await fetch(saved.endpoint, {
  method: 'POST', redirect: 'error',
  headers: {
    'Content-Type': 'application/json', 'PAYMENT-SIGNATURE': saved.signature,
  },
  body: saved.body, signal: AbortSignal.timeout(60000),
});
const receipt = response.headers.get('PAYMENT-RESPONSE');
const result = await response.json();
await writeFile('payment-retry-result.json', JSON.stringify({
  httpStatus: response.status, result,
  receipt: receipt ? JSON.parse(Buffer.from(receipt, 'base64').toString()) : null,
}), { mode: 0o600 });
console.log(response.status, result);

Run node --env-file=agent.env retry-sponsorship.mjs only after checking whether reconciliation is needed. Use bounded backoff and a maximum retry count.

6. Track payment and transfer separately

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.

Troubleshooting and recovery

OutcomeAction
Initial HTTP 402Validate the challenge, reserve budget, and authorize once
INVALID_PAYMENT / PAYMENT_INVALIDCheck v2 encoding, exact requirements, signer, funding, and expiry; do not sign repeatedly
PAYMENT_REPLAYStop: this authorization is bound to another request
PAYMENT_SETTLEMENT_FAILEDReconcile with the facilitator before authorizing another payment
PAYMENT_FACILITATOR_UNAVAILABLEA settlement timeout can have an unknown outcome; reconcile and use bounded backoff
PAYMENT_STORE_UNAVAILABLE / PAYMENT_CAPACITY_EXCEEDEDOperator must restore the store or reconcile capacity
SPONSORSHIP_IN_PROGRESSWait for relay reconciliation and inspect transaction status
Network timeout or broadcast errorKeep the existing signature and receipt; reconcile before retrying the same request
Quote expired or transaction changedReconcile 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.

Agent permissions and operator persistence

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.

Verify your setup

From the repository root:

npm run typecheck
npm run test:x402
npm run test:relay-policy
npm 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.