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.
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.
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.
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.
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.
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)
Install Freighter, create a wallet, and switch the network to Testnet in settings.
Fund it: open Stellar Lab → Fund account, paste your address, press Fund (friendbot gives free test XLM).
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.
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
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
}
}
}
}
| Tool | What the agent gets |
|---|---|
| search_resources | natural-language search over the index, ranked, with prices |
| get_resource_details | full payment terms + input schema with per-parameter descriptions |
| paid_call | pays and calls an HTTP service; the ceiling is enforced before anything is signed |
| paid_mcp_call | pays and calls a paid MCP tool (payment rides in _meta) |
| spend_report | audit 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.
Get a receiving account
Any funded testnet account with a USDC trustline (see Wallets — pnpm seed-testnet <secret> 0 sets up the trustline only).
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.
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
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-RESPONSESheader:{"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.
| Field | Signed by the buyer? | Meaning |
|---|---|---|
token | yes | which asset may move (e.g. USDC) |
to | yes | the only recipient that can be paid |
max | yes | the cap — the absolute most that can leave the wallet |
salt | yes | random 32 bytes making the authorization single-use |
actual | no | the metered amount, decided after the work; must be ≤ max |
The payment then flows in two phases with different amount semantics:
- 402 → terms. The endpoint's
acceptscarries an upto entry:amountis the cap, andextra.uptoContractis the router's address. - Authorize. The buyer builds the router's
settlecall with a random salt andactualas a placeholder, and signs the Soroban authorization entry — never a full transaction, and never more than the cap. - 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.
- Settle (amount = actual). The service reports metered usage; the facilitator swaps the
actualargument to that value, wraps the call in a fee-bump (so the buyer pays no XLM), and submits. The signature stays valid becauseactualwas never signed. - 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.
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
| Env | Meaning | Default |
|---|---|---|
| AUTH_MODE | none (frictionless) or apikey (hashed bearer keys) | none |
| RATE_LIMIT_PER_MIN | per caller on verify/settle | 120 |
| FACILITATOR_STELLAR_CHANNEL_SECRETS | channel accounts for parallel settlement (pnpm generate-channel-accounts) | — |
| SETTLE_FEE_MODEL | free / flat / bps — usage metering for your business model | free |
| EMBEDDINGS_PROVIDER | local (no keys) or none (keyword-only search) | local |
| BAZAAR_DB_PATH | SQLite catalog location | data/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.
| Endpoint | What it does |
|---|---|
| GET /supported | schemes, networks, signer addresses, extra.areFeesSponsored, extra.uptoContract |
| POST /verify | validates a payment payload against requirements — {isValid, invalidReason?, payer} |
| POST /settle | submits on-chain — {success, transaction, network, payer} + EXTENSION-RESPONSES header |
| GET /discovery/resources | the catalog; filters type, payTo, scheme, network, extensions, limit, offset |
| GET /discovery/search | natural-language search; query required; returns resources, partialResults, searchMethod, cursor pagination |
| POST /discovery/register | body {"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.
| Package | For | What it does |
|---|---|---|
| @x402-stellar-bazaar/seller | API sellers | discoverableRoute() 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/agent | buyers / agent devs | Bazaar queries + budget-capped payments (paidCall, paidMcpToolCall) + spendReport() audit log — limits enforced before signing |
| @x402-stellar-bazaar/mcp | agent runtimes | the 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/bazaar | operators | the discovery engine as a library: SQLite catalog, automatic cataloging with anti-poisoning defenses, hybrid BM25+embedding search, the /discovery/* router |
| @x402-stellar-bazaar/upto | metered services | the 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/facilitator | operators | the 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 |
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.