walras

Sell: charge per request and become discoverable

You have an HTTP API — or an MCP tool server — and want to charge USDC per call on Stellar. This guide takes you from a plain route to a paid endpoint that shows up in the Bazaar discovery catalog, using only stock x402 SDK packages pointed at a walras facilitator.

Other roles: buy-agent.md · operate.md

What you'll have at the end

An endpoint that answers 402 Payment Required with machine-readable terms, gets paid in testnet USDC through walras, and appears in the discovery catalog the moment its first payment settles. This guide contains no registration call because none exists anywhere in walras: settlement is what creates the listing (D-004, D-022). The whole integration is the stock @x402/express middleware plus declareDiscoveryExtension in your route config (F-074).

Prerequisites

Steps

1. Install the stock SDK packages

In your server project (all @x402/* packages at exactly 2.20.0 — upstream versions move fast, so pin them; F-061):

pnpm add express @x402/express @x402/core @x402/stellar @x402/extensions

2. Declare a paid route

This is the complete integration — middleware plus route config. It is the same wiring as demo/seller.ts, reduced to one route:

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

const FACILITATOR_URL = process.env.FACILITATOR_URL ?? "http://127.0.0.1:4021";
const PAY_TO = process.env.SERVER_STELLAR_ADDRESS; // your G... address

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

const app = express();
app.use(
  paymentMiddleware(
    {
      "GET /weather": {
        accepts: { payTo: PAY_TO, scheme: "exact", price: "$0.01", network: "stellar:testnet" },
        description: "Current weather report for a named city",
        mimeType: "application/json",
        serviceName: "Example Weather",
        tags: ["weather"],
        extensions: {
          ...declareDiscoveryExtension({
            method: "GET",
            input: { city: "Zurich", units: "metric" },
            inputSchema: {
              type: "object",
              properties: {
                city: { type: "string", description: "City to report current weather for" },
                units: { type: "string", description: "Unit system: metric or imperial" },
              },
              required: ["city"],
            },
            output: { example: { city: "Zurich", condition: "sunny", temperatureC: 24 } },
          }),
        },
      },
    },
    server,
  ),
);

app.get("/weather", (_req, res) => res.json({ city: "Zurich", condition: "sunny", temperatureC: 24 }));
app.listen(4022);

There is nothing else. The middleware detects the bazaar declaration in route config and auto-registers the resource-server extension itself (F-074). A price of "$0.01" becomes amount: "100000" on the wire — USDC base units at 7 decimals (F-008).

3. Write metadata the search indexer can rank

The indexer ranks listings by service name, description, parameter text, and tags — where parameter text is parameter names plus JSON-Schema description annotations, and example values are deliberately not indexed (D-026). An example city: "Zurich" says nothing about what your resource does; "City to report current weather for" does. So:

4. Take the first payment — settlement is registration

Start your server, then have any stock buyer pay it once (the walras checkout ships one: demo/buyer.ts). Cataloging is triggered by the buyer's client echoing your bazaar declaration back inside the payment payload — if the client omits the echo, no cataloging occurs, and you cannot force a listing (F-032). The listing is created by the settlement itself (D-004); the full loop — pay, then listed, zero registration steps — ran live on stellar:testnet (F-075, S3-3).

5. Read the cataloging outcome

walras reports what happened to your listing in the EXTENSION-RESPONSES header of the settle response — base64-encoded JSON keyed by extension name (F-024). bazaar.status is success, or rejected with a human-readable rejectedReason plus a machine-readable code alongside it (D-014). The stock middleware logs it for you; captured live (S3-3):

[x402] extension responses: {"bazaar":{"status":"success"}}

That log line is stock behavior — the SDK's facilitator client prints status, rejectedReason, and code from the header (F-073).

6. Verify your listing

curl "http://127.0.0.1:4021/discovery/resources?payTo=G...YOURPAYTO"

The response's items array carries your listing (F-025, F-027): the accepts of the settled payment, your metadata, and the full extensions.bazaar block agents build requests from. Your listing is owned by its first settled payTo — a real, settled payment to any other payee cannot touch it, demonstrated live against a hostile client (D-024, S3-4).

7. MCP tool sellers

An MCP server charges per tool call with the stock @x402/mcp wrapper (F-078). The shape, condensed from demo/mcp-seller.ts:

import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { ExactStellarScheme } from "@x402/stellar/exact/server";
import { createPaymentWrapper } from "@x402/mcp";
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";

const resourceServer = new x402ResourceServer([new HTTPFacilitatorClient({ url: FACILITATOR_URL })]);
resourceServer.register("stellar:*", new ExactStellarScheme());
await resourceServer.initialize(); // required before buildPaymentRequirements (F-083)

const accepts = await resourceServer.buildPaymentRequirements({
  scheme: "exact", network: "stellar:testnet", payTo: PAY_TO, price: "$0.02",
});

const paid = createPaymentWrapper(resourceServer, {
  accepts,
  resource: { url: "http://127.0.0.1:4023/mcp", description: "…", serviceName: "…" },
  extensions: declareDiscoveryExtension({
    toolName: "my_tool",
    description: "What the tool does, in one plain sentence",
    inputSchema: { type: "object", properties: { /* … with descriptions … */ } },
    output: { example: { /* … */ } },
  }),
});
// mcp.registerTool("my_tool", {…}, paid(async args => ({ content: [/* … */] })));

Three things the HTTP path hides:

The live example

Troubleshooting

Codes below are from the error registry; on the seller path they arrive inside the EXTENSION-RESPONSES header's bazaar object unless noted. A rejected catalog write never affects the settlement itself (D-015).

Symptom Code What it means What to do
Settle succeeds, header says rejected bazaar_spec_validation_failed Your declared discovery info violates the bazaar protocol invariants Fix the declareDiscoveryExtension arguments — the rejectedReason names the field
Settle succeeds, header says rejected bazaar_schema_validation_failed The info block does not validate against the schema you declared Make the example input match your own inputSchema
Settle succeeds, header says rejected bazaar_resource_url_invalid The resource URL is not an absolute http(s) URL without credentials Serve the route at a plain http(s) URL
Settle succeeds, header says rejected bazaar_extensions_too_large The extensions block exceeds the 64 KiB indexing cap Trim schemas and examples
Your listing never updates bazaar_listing_owned_by_other_payee The URL is already cataloged for a different payTo (D-024) Keep paying with the original payTo, or list under a different URL
No EXTENSION-RESPONSES header at all The buyer's client omitted the echo, so no cataloging occurred (F-032) — or a walras-internal indexer fault, which omits the header rather than blaming you (D-025) Pay once with a stock client; if the header still never appears, report it to the operator
Middleware refuses your route at boot walras_unsupported_kind Your route's (scheme, network) pair is not advertised by the facilitator curl <facilitator>/supported and match your route config to its kinds