402 PAYMENT REQUIRED · DOCUMENTATION

The Stellar Bazaar, explained

Step-by-step guides for the machine-payments marketplace on Stellar: pay for APIs from code or an agent, list your own service, or run the whole stack yourself.

Everything here runs on Stellar testnet. Accounts, USDC, and every payment use free test funds — no real money moves anywhere in these guides.

01What is this?

Three ideas, one page.

x402 turns the HTTP status code 402 Payment Required into a real payment flow. A client calls an API; the API answers "402 — this costs 0.01 USDC, here are the terms"; the client's wallet signs a payment authorization and retries; a facilitator verifies the signature and settles it on the Stellar blockchain; the API returns the data. No account. No API key. No subscription.

The Bazaar is the discovery layer on top: a searchable index of every service that has taken a payment through the facilitator. Sellers describe themselves in machine-readable metadata; the index catalogs them automatically at first settled payment — there is no submission form, so there is no spam and no gatekeeper.

Agents are the customers this was built for. An AI agent can search the index in natural language, read a service's input schema, pay per call from its own wallet, and use the result — with no pre-built integration.

agent ──GET──▶ api            402 + terms + metadata
agent ──signs, retries──▶ api  payment authorization attached
api ──/verify, /settle──▶ facilitator ──▶ Stellar  on-chain, ~5s, fee sponsored
api ──200──▶ agent             …and the api is now in the Bazaar index

Why Stellar: a settlement costs a fraction of a cent, USDC is native, and buyers need zero XLM — the facilitator sponsors network fees (areFeesSponsored: true). The wallet only ever holds the payment asset.

02Try it in 5 minutes

No installation — the index itself is the demo.

1

Browse the index

Open the Bazaar. Each card is a live, paid service. Click one to see its parameters, example output, and payment terms. Flip the HUMAN / WIRE switch to see the exact JSON an agent receives.

2

Search in natural language

Type what you need, not an endpoint name — try "how warm is it outside". The chip next to the box shows how retrieval ran: hybrid means keyword and semantic search were fused.

3

Pay for a call with your own wallet

Expand any entry and press “Pay for one call — with your own wallet”. Sign with Freighter or a pasted testnet key (get one below). You'll watch the real sequence: sign → verify → settle → an on-chain transaction you can open in the explorer.

4

Query it like a machine

curl "https://72-60-179-250.sslip.io/discovery/search?query=weather"
curl "https://72-60-179-250.sslip.io/supported"

03Wallets & test funds

Two minutes from nothing to a funded testnet wallet holding USDC.

Option A — Freighter (browser extension)

1

Install Freighter, create a wallet, and switch the network to Testnet in settings.

2

Fund it: open Stellar Lab → Fund account, paste your address, press Fund (friendbot gives free test XLM).

3

Add a USDC trustline (an account must opt in to an asset before holding it): in Freighter choose Manage assets → Add asset and add USDC issued by GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5.

4

Get test USDC from Circle's faucet (select Stellar), or buy some on the testnet DEX with your free XLM — the repo script below does this automatically.

Option B — a throwaway key from the repo (fully scripted)

git clone https://github.com/utkucy/x402-stellar-bazaar && cd x402-stellar-bazaar && pnpm install
# creates the account if needed, funds it, adds the USDC trustline,
# and buys 20 USDC on the testnet DEX — no faucet, no clicking:
pnpm seed-testnet <your-secret-S…> 20
Why no XLM is needed for payments: the facilitator submits your transaction and pays the network fee itself. Your wallet only spends the payment asset. XLM only matters for the one-time account setup, and friendbot gives that for free.

04Pay from code

The stock x402 client handles the whole 402 dance — you write three lines.

npm i @x402/fetch @x402/core @x402/stellar
import { wrapFetchWithPayment } from "@x402/fetch";
import { x402Client } from "@x402/core/client";
import { ExactStellarScheme } from "@x402/stellar/exact/client";
import { createEd25519Signer } from "@x402/stellar";

const signer = createEd25519Signer(process.env.STELLAR_SECRET, "stellar:testnet");
const client = new x402Client().register("stellar:*", new ExactStellarScheme(signer));
const payFetch = wrapFetchWithPayment(fetch, client);

// any x402-protected endpoint, discovered from the Bazaar or known upfront:
const res = await payFetch("https://weather.example.com/weather?city=Istanbul");
console.log(await res.json());
// the settlement receipt (tx hash, payer, network) is in the
// base64 PAYMENT-RESPONSE header of the reply

What happened underneath: the endpoint answered 402 with terms → the library built a Soroban transfer invocation → your key signed only an authorization entry (never a transaction) → the endpoint's facilitator verified and settled on-chain → the request was retried and served.

Discovering services from code

import { HTTPFacilitatorClient } from "@x402/core/server";
import { withBazaar } from "@x402/extensions/bazaar";

const bazaar = withBazaar(new HTTPFacilitatorClient({ url: FACILITATOR_URL }));
const { resources } = await bazaar.extensions.bazaar.search({ query: "weather for a city" });

Or use the budgeted agent SDK

@x402-stellar-bazaar/agent wraps discovery + payment behind hard spending limits (checked before anything is signed) and keeps an audit log:

import { StellarBazaarAgent } from "@x402-stellar-bazaar/agent";

const agent = new StellarBazaarAgent({
  facilitatorUrl: FACILITATOR_URL,
  privateKey: process.env.STELLAR_SECRET,
  maxAmountPerCall: 1_000_000n,          // 0.1 USDC ceiling per call
  sessionBudget: 10_000_000n,            // 1 USDC total
});

const { resources } = await agent.search({ query: "weather for a city" });
const result = await agent.paidCall({
  url: resources[0].resource,
  query: { city: "Lisbon" },
  expectedPriceAtomic: 100_000n,
});
console.log(agent.spendReport());        // every payment + its on-chain tx hash

05Agents & MCP

Give any MCP-capable agent (Claude, Cursor…) the ability to find and buy capabilities at runtime.

{
  "mcpServers": {
    "stellar-bazaar": {
      "command": "npx", "args": ["-y", "@x402-stellar-bazaar/mcp"],
      "env": {
        "FACILITATOR_URL": "https://72-60-179-250.sslip.io",
        "STELLAR_PRIVATE_KEY": "S…your testnet key…",
        "MAX_AMOUNT_PER_CALL": "1000000",   // 0.1 USDC hard ceiling per call
        "SESSION_BUDGET": "10000000"        // 1 USDC total per session
      }
    }
  }
}
ToolWhat the agent gets
search_resourcesnatural-language search over the index, ranked, with prices
get_resource_detailsfull payment terms + input schema with per-parameter descriptions
paid_callpays and calls an HTTP service; the ceiling is enforced before anything is signed
paid_mcp_callpays and calls a paid MCP tool (payment rides in _meta)
spend_reportaudit log — every payment with amount and on-chain tx hash

Every failure returns a machine-readable {code, message}budget_exceeded, no_trustline, payment_rejected:<reason> — so the agent can branch on failures instead of parsing prose. Budgets are hard limits: a call priced above the ceiling is refused before signature.

06List your API

From an ordinary Express route to a paid, discoverable endpoint — target: under an hour.

1

Get a receiving account

Any funded testnet account with a USDC trustline (see Walletspnpm seed-testnet <secret> 0 sets up the trustline only).

2

Protect the route and declare what it does

import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { ExactStellarScheme } from "@x402/stellar/exact/server";
import { bazaarResourceServerExtension } from "@x402/extensions/bazaar";
import { discoverableRoute } from "@x402-stellar-bazaar/seller";

const server = new x402ResourceServer(new HTTPFacilitatorClient({ url: FACILITATOR_URL }))
  .register("stellar:*", new ExactStellarScheme())
  .registerExtension(bazaarResourceServerExtension);

app.use(paymentMiddleware({
  "GET /forecast": discoverableRoute({
    payTo: "G…YOU",
    network: "stellar:testnet",
    price: { asset: USDC_TESTNET, amount: "100000" },  // 0.01 USDC / call
    description: "7-day weather forecast for any city, updated hourly",
    serviceName: "Acme Weather",
    tags: ["weather", "forecast"],
    params: { city: { description: "City name to forecast" } },
    exampleOutput: { city: "Istanbul", days: [ { tempC: 24 } ] },
  }),
}, server));

discoverableRoute fails at startup with a plain-language message if anything would be rejected or soft-dropped by the index (a parameter without a description, a 40-character service name…). Descriptions matter: search ranks natural language, and a realistic exampleOutput measurably improves your ranking.

3

Check yourself before anyone pays

npx x402-stellar-bazaar-validate https://api.yours.dev/forecast
# walks your live 402 and reports exactly what will be cataloged,
# soft-dropped, or rejected — and why
4

Go live

Two doors, same destination:

  • Front door: do nothing. Your first paying customer's settlement writes you into the index automatically.
  • Side door: paste your URL into “Stage your URL early”. The facilitator probes your 402 server-side and stages the listing; then activate it yourself with the pay-with-your-wallet button — you're both first customer and seller, on test funds.

07How listing works

The index is adversarial by design — here is the exact contract.

  • Listing price is one settled payment. Nothing appears publicly until a real payment for that resource settles. Spam costs money; silence is free.
  • Listings are wallet-bound. An entry can only be updated by payments to the same payTo. Nobody can overwrite your name, description, or pricing.
  • Metadata is validated, invalid fields are dropped — not the listing. Service names over 32 characters, icon URLs pointing at private addresses, malformed route templates: each is silently dropped per the x402 spec's soft-drop rules, the rest of your listing survives.
  • You always learn the outcome. Every verify/settle response carries an EXTENSION-RESPONSES header: {"bazaar":{"status":"success" | "processing" | "rejected", "rejectedReason": "…"}}.
  • Stale listings age out. No settlement for 30 days → hidden from search until the next payment.
  • MCP tools are first-class. A paid MCP tool lists as type: "mcp", identified by the pair (server URL, tool name).

08Payment schemes: exact & upto

Two ways money can move.

exact — spot payments

The price is known upfront; the buyer authorizes exactly that amount for exactly that recipient. The signature covers one specific transfer, can be used once (replay is impossible), and expires in about a minute. This is what a per-request API uses.

upto — metered payments

For services priced by usage (per token, per second) where the cost is only known after the work: the buyer authorizes a cap, the service settles the actual usage, and the difference refunds in the same on-chain transaction.

authorize:  up to 1.00 USDC  (cap is signed by the buyer)
settle:     0.23 USDC → seller, 0.77 USDC → back to buyer  (one transaction)

On Stellar this runs through a tiny open-source Soroban contract (~60 lines, no admin, no storage) that enforces actual ≤ cap on-chain. Your worst case is exactly the cap you signed — a smart-account budget policy can treat an upto authorization like an exact payment of the cap.

How it works under the hood

The whole trick is what the buyer's signature covers — and what it deliberately leaves out. The router contract exposes one function:

settle(from, token, to, max, salt, actual)

Inside, it calls require_auth_for_args(from, (token, to, max, salt)) — so the buyer's signature binds the asset, the recipient, the cap, and a one-time salt. actual is outside the signed tuple: it can be filled in later without breaking the signature, and the contract enforces actual ≤ max on-chain regardless of who fills it.

FieldSigned by the buyer?Meaning
tokenyeswhich asset may move (e.g. USDC)
toyesthe only recipient that can be paid
maxyesthe cap — the absolute most that can leave the wallet
saltyesrandom 32 bytes making the authorization single-use
actualnothe metered amount, decided after the work; must be ≤ max

The payment then flows in two phases with different amount semantics:

  1. 402 → terms. The endpoint's accepts carries an upto entry: amount is the cap, and extra.uptoContract is the router's address.
  2. Authorize. The buyer builds the router's settle call with a random salt and actual as a placeholder, and signs the Soroban authorization entry — never a full transaction, and never more than the cap.
  3. Verify (amount = max). The facilitator decodes the signed entry and checks the ground truth from the signature itself: right contract, right token, right recipient, cap matches the requirements, salt unused, expiration sane. The service can now safely do the work.
  4. Settle (amount = actual). The service reports metered usage; the facilitator swaps the actual argument to that value, wraps the call in a fee-bump (so the buyer pays no XLM), and submits. The signature stays valid because actual was never signed.
  5. Three transfers, one transaction. Atomically: buyer → router (max), router → seller (actual), router → buyer (max − actual). No custody: the router holds nothing after the transaction, and there is no state to admin.

Security properties. Worst case is exactly the signed cap. Replay is impossible — the salt-keyed authorization nonce is consumed on first use, so the same signature is rejected a second time. Over-cap settles are rejected by the contract itself, not by policy. The router is stateless, has no admin key, no upgrade path and no pause switch — there is nothing to rug. And fees are sponsored: the facilitator's fee-bump pays the network fee, so a buyer holding only USDC can pay.

Deployed on testnet. The upto router this site uses is live at CDZHYGQTCJQCB7MOWGH4OKOZZB34WPJ6L54D4QBBV6VC3ZQT3I4W7Q6S (view on stellar.expert ↗) — also exported as UPTO_ROUTER_TESTNET from @x402-stellar-bazaar/upto. Contract source: contracts/upto; the full wire-level spec lives at specs/scheme_upto_stellar.md.

Using it from code

import { UptoStellarScheme, UptoStellarFacilitatorScheme, UptoStellarServerScheme,
         UPTO_ROUTER_TESTNET } from "@x402-stellar-bazaar/upto";

// buyer: register beside the exact scheme — the cap is requirements.amount
client.register("stellar:*", new UptoStellarScheme(signer));

// facilitator: verifies against the signed cap, settles the metered actual
facilitator.register("stellar:testnet",
  new UptoStellarFacilitatorScheme(signers, { uptoContract: UPTO_ROUTER_TESTNET }));

// seller (express): offer both — a per-call price and a metered cap
server.register("stellar:*", new UptoStellarServerScheme());
routes["GET /weather"].accepts = [
  { scheme: "exact", network: "stellar:testnet", payTo, price: { asset: USDC, amount: "100000" } },
  { scheme: "upto",  network: "stellar:testnet", payTo, price: { asset: USDC, amount: "500000" } },
];

Try it live

The hosted Acme Weather listing offers both schemes. Open it in the explorer and press “Metered — authorize up to 0.05 USDC (upto)”: you sign the cap, pick how much to charge, and watch the refund land in the same transaction (example: 0.05 cap, 0.03 charged, 0.02 refunded ↗).

09Security model

Who can do what to whom — in five sentences.

  • The facilitator never holds funds. Payments move payer → payee directly on-chain under the payer's own signature; the facilitator only verifies, submits, and pays the network fee.
  • Tampering breaks the signature. Amount, recipient, and asset are inside the signed authorization; changing any of them invalidates it. Rejections always carry a machine-readable reason.
  • Replay is impossible. Soroban consumes each authorization's nonce on first use, and every authorization expires after ~a minute.
  • The catalog treats all input as hostile. Metadata arrives via clients, so it is schema-validated, sanitized against URL/path tricks, and wallet-bound (see listing rules).
  • Everything is auditable. Every settlement is a public Stellar transaction; the full stack is Apache-2.0 open source with a published threat model.

10Self-host the stack

The whole system — facilitator, index, search, MCP server — runs from one repo with zero external services: SQLite for the catalog, a local model for semantic search, no API keys.

git clone https://github.com/utkucy/x402-stellar-bazaar
cd x402-stellar-bazaar
pnpm install && pnpm build
cp .env.example .env        # put a friendbot-funded secret in FACILITATOR_STELLAR_PRIVATE_KEY
pnpm --filter @x402-stellar-bazaar/facilitator start
curl :4022/supported         # exact + upto on stellar:testnet

Or with Docker: docker compose -f ops/hosted/docker-compose.yml up -d (facilitator + MCP server + example services + TLS proxy).

The knobs that matter

EnvMeaningDefault
AUTH_MODEnone (frictionless) or apikey (hashed bearer keys)none
RATE_LIMIT_PER_MINper caller on verify/settle120
FACILITATOR_STELLAR_CHANNEL_SECRETSchannel accounts for parallel settlement (pnpm generate-channel-accounts)
SETTLE_FEE_MODELfree / flat / bps — usage metering for your business modelfree
EMBEDDINGS_PROVIDERlocal (no keys) or none (keyword-only search)local
BAZAAR_DB_PATHSQLite catalog locationdata/bazaar.db

One-command demo of the full flow (creates and funds throwaway accounts, boots everything, makes two real payments, shows the catalog): pnpm demo.

Embed it in your own server (self-facilitation)

The facilitator and the Bazaar are libraries, not just a binary — a resource server can run the whole thing in-process and depend on nobody:

import { createFacilitatorApp } from "@x402-stellar-bazaar/facilitator";
import { createBazaar } from "@x402-stellar-bazaar/bazaar";

const { bazaar, discoveryRouter } = await createBazaar({ dbPath: "data/bazaar.db", log });
const { app } = createFacilitatorApp({ config, log, bazaar, discoveryRouter });
// mount `app` (verify/settle/supported/discovery) inside your own Express server

11HTTP API reference

Everything the explorer does, you can curl.

EndpointWhat it does
GET /supportedschemes, networks, signer addresses, extra.areFeesSponsored, extra.uptoContract
POST /verifyvalidates a payment payload against requirements — {isValid, invalidReason?, payer}
POST /settlesubmits on-chain — {success, transaction, network, payer} + EXTENSION-RESPONSES header
GET /discovery/resourcesthe catalog; filters type, payTo, scheme, network, extensions, limit, offset
GET /discovery/searchnatural-language search; query required; returns resources, partialResults, searchMethod, cursor pagination
POST /discovery/registerbody {"url": …} — probes your 402 server-side, stages the listing, returns the staged terms
GET /health{"status":"ok"}

Wire formats follow the x402 v2 specification and the Bazaar extension — x402-foundation/x402.

12Packages & SDKs

Everything ships from one Apache-2.0 monorepo under the @x402-stellar-bazaar scope. Each package below is running in production on this very site's testnet stack.

PackageForWhat it does
@x402-stellar-bazaar/sellerAPI sellersdiscoverableRoute() makes a route paid + discoverable in one call, with startup-time validation; ships the x402-stellar-bazaar-validate CLI (“why isn't my listing live?”)
@x402-stellar-bazaar/agentbuyers / agent devsBazaar queries + budget-capped payments (paidCall, paidMcpToolCall) + spendReport() audit log — limits enforced before signing
@x402-stellar-bazaar/mcpagent runtimesthe MCP discovery server: 5 tools (search, inspect, pay-and-call ×2, spend report) over stdio or streamable HTTP; bin x402-stellar-bazaar-mcp
@x402-stellar-bazaar/bazaaroperatorsthe discovery engine as a library: SQLite catalog, automatic cataloging with anti-poisoning defenses, hybrid BM25+embedding search, the /discovery/* router
@x402-stellar-bazaar/uptometered servicesthe upto scheme: client/server/facilitator classes + the deployed Soroban router address — authorize a cap, settle actual usage, refund in the same tx
@x402-stellar-bazaar/facilitatoroperatorsthe full facilitator app, also importable: createFacilitatorApp() for embedding verify/settle/discovery in your own Express server
@x402-stellar-bazaar/shared(internal)config schemas, the machine-readable error-code vocabulary, logging — a dependency of the others
All packages are live on npm. npm i @x402-stellar-bazaar/seller for sellers, npm i @x402-stellar-bazaar/agent for buyers, npx -y @x402-stellar-bazaar/mcp for agent runtimes — or build everything from source.

13FAQ

Who pays to get a service listed?

Whoever makes the first payment — normally your first customer. If you want to be live immediately, stage your URL and press the activate button: you pay your own price once, from your own wallet, with test funds. The index operator never pays and never charges.

Does the marketplace take a commission?

No. Payments go directly payer → payee on-chain. Browsing and search are free and keyless.

Why does my wallet need no XLM?

The facilitator submits the transaction and sponsors the fee (areFeesSponsored). Your wallet only holds the payment asset (USDC on testnet).

My payment failed with a trustline error.

Your account hasn't opted into USDC yet — add the trustline (Wallets, step 3, or pnpm seed-testnet <secret> 0).

My listing shows "rejected" — what now?

The rejectedReason in the response says exactly why, and npx x402-stellar-bazaar-validate <url> reproduces the index's full validation against your live 402.

Is this mainnet-ready?

The code supports stellar:pubnet behind configuration, but this instance is testnet-only and mainnet is gated on an independent security review. Everything you see here uses free test funds.