---
title: "Bazaar is settlement-pull, not manifest-crawl"
slug: bazaar-settlement-pull-internals
canonical: https://x402station.io/blog/bazaar-settlement-pull-internals
date: 2026-05-13
tags: [x402, bazaar, agentic-commerce, payments, coinbase, operations]
---

x402station.io is infrastructure for x402 agentic commerce. Our active wedge is Preflight by x402station.io: agents call it before signing `PAYMENT-SIGNATURE` for an unfamiliar x402 endpoint.

This week I learned that my mental model of Coinbase x402 Bazaar discovery was wrong.

I thought Bazaar behaved like a normal agent metadata crawler:

1. Serve a good `/.well-known/x402`.
2. Keep OpenAPI and the agent card current.
3. Wait for the catalog to crawl and refresh.

That is useful for normal web discovery. It was not the behavior that mattered for Bazaar route metadata.

The operational model that matched production was different:

> Bazaar discovery is settlement-pull. A paid route becomes visible or refreshes when a payment settles through the facilitator, and the metadata comes from the route configuration that participates in that x402 handshake.

That distinction sounds small until a bad route template lands in discovery.

## TL;DR

- Bazaar did not refresh our paid route listings just because `/.well-known/x402` changed.
- The listing metadata we observed came from the x402 payment declaration at settle time.
- The `@x402/next` convenience wrapper registered our route under `"*"`.
- The Bazaar extension normalized that wildcard into `":var1"`, which could overwrite the intended literal `routeTemplate`.
- The production-safe fix was a tiny helper around `withX402FromHTTPServer` plus a literal route map.
- Runtime acceptance must decode the actual `payment-required` header. Source-code presence of `routeTemplate` is not enough.

## The myth: "Bazaar crawls my manifest"

Our first pass treated Bazaar like an agent crawler.

We had already done the normal metadata work:

- `/.well-known/x402` listed the product routes.
- `agent-card.json` described the capabilities.
- OpenAPI exposed the endpoints.
- Paid route descriptions followed the current naming discipline: `x402station.io Preflight`, `x402station.io Forensics`, `x402station.io Credits`, and so on.

The source looked correct. The manifest looked correct. The route files even contained explicit Bazaar `routeTemplate` fields.

But public discovery state did not move the way a crawler model predicts. Some routes were listed. Some were missing. Existing listings stayed stale. The missing routes had a pattern: they had not settled through the facilitator since the metadata changed.

That was the turn.

## The reality: settlement refreshes the listing

The model that matched what we saw was:

1. Client calls a paid route.
2. Server returns `402 Payment Required` with a `payment-required` header.
3. Client signs and retries with `PAYMENT-SIGNATURE`.
4. Facilitator verifies and settles.
5. Bazaar receives route declaration metadata attached to that x402 flow.
6. Discovery refreshes after the settlement path has processed.

For an operator, the practical consequence is simple:

> If you change Bazaar-facing metadata, plan one clean settlement per paid route you want refreshed.

Do not spend money blindly. First decode the unsigned 402 challenge and verify the metadata the facilitator will see.

That sentence would have saved a loop.

## The bug: why our routes showed up as `:var1`

We had added explicit literal route templates to the paid route configs:

```ts
extensions: {
  bazaar: {
    ...declareDiscoveryExtension({
      bodyType: "json",
      input: { url: "https://api.venice.ai/api/v1/chat/completions" },
      inputSchema: {
        type: "object",
        required: ["url"],
        properties: { url: { type: "string", format: "uri" } },
      },
      output: { example: { ok: false, warnings: ["zombie"] } },
    }).bazaar,
    routeTemplate: "/api/v1/preflight",
  },
}
```

The source looked correct.

Then we decoded the live 402 challenge. It still said:

```json
{
  "extensions": {
    "bazaar": {
      "routeTemplate": ":var1"
    }
  }
}
```

That was the important failure mode: the code contained the right field, but the runtime artifact still carried the wrong value.

The root cause was lower in the helper stack. The convenient `withX402(handler, routeConfig, server)` path in `@x402/next` effectively builds a one-route HTTP resource server with a wildcard key:

```ts
const routes = { "*": routeConfig };
const httpServer = new x402HTTPResourceServer(server, routes);
return withX402FromHTTPServer(routeHandler, httpServer);
```

The route pattern entering Bazaar enrichment was not the Next.js file path. It was `"*"`.

That wildcard was normalized into a colon parameter. The Bazaar declaration ended up with `:var1`.

## The tempting fix I did not take

The first architectural proposal was to switch to a proxy-style API that accepts a map of literal paths.

That sounds right if the only problem is "I need literal route keys." It was not the right abstraction for our paid API.

For real-money x402 routes, one contract matters a lot:

> Settlement should happen only after the route handler has successfully produced the paid response.

That is the behavior we wanted from the route-level flow. The handler can validate input, run bounded database work, and return a failure before funds settle if the service cannot safely fulfill.

A broad proxy or middleware shape can change where settlement sits relative to the route handler. That might be fine for static resources or simple proxies. It was not a safe default for routes that create state:

- `Credits by x402station.io`
- `Watch by x402station.io`
- `Verified by x402station.io`

So the fix had to preserve route-level settlement semantics while giving Bazaar a literal path.

## The production-safe fix

The API we needed was already public. We just had to use one layer lower.

```ts
import type { NextRequest, NextResponse } from "next/server";
import {
  withX402FromHTTPServer,
  x402HTTPResourceServer,
  type RouteConfig,
} from "@x402/next";

import { resourceServer } from "@/lib/x402-server";

type RouteHandler<T = unknown> = (request: NextRequest) => Promise<NextResponse<T>>;

export function withX402Route<T = unknown>(
  routeHandler: RouteHandler<T>,
  routePath: `/${string}`,
  routeConfig: RouteConfig,
) {
  const httpServer = new x402HTTPResourceServer(resourceServer, {
    [routePath]: routeConfig,
  });

  return withX402FromHTTPServer(routeHandler, httpServer);
}
```

Then each paid route moved from:

```ts
export const POST = withPublicUrl(
  withX402(handler, routeConfig, resourceServer),
);
```

to:

```ts
export const POST = withPublicUrl(
  withX402Route(handler, "/api/v1/preflight", routeConfig),
);
```

This does two things:

1. The x402 flow still uses `withX402FromHTTPServer`, the same lower-level engine used by the convenience helper.
2. The resource server route map now contains the literal public path instead of `"*"`.

After that change, decoding the unsigned 402 challenge showed the acceptance criterion we actually cared about:

```json
{
  "resource": {
    "url": "https://x402station.io/api/v1/preflight",
    "description": "x402station.io Preflight ..."
  },
  "extensions": {
    "bazaar": {
      "routeTemplate": "/api/v1/preflight"
    }
  }
}
```

## The verification script pattern

We added an operator script so this does not depend on memory next time.

The dry-run path makes one unsigned POST to each paid route and decodes the `payment-required` header:

```bash
bun run scripts/bazaar-bootstrap.ts --prod
```

For each route it checks:

- HTTP status is `402`.
- `extensions.bazaar.routeTemplate` equals the literal route path.
- Network matches the expected chain.
- Amount matches the route price.
- Description starts with the product name, such as `x402station.io Preflight`.

Only after that dry-run passes can the script spend money:

```bash
bun run scripts/bazaar-bootstrap.ts --prod --execute --yes \
  --only preflight,preflight-batch,forensics,catalog-decoys,whats-new,alternatives,credits,watch \
  --max-usdc 0.60
```

That command is intentionally verbose. Real money should have ceremony.

The script also computes the total before payment. This caught another mistaken assumption: refreshing all nine routes was not a one-cent operation.

| Route | Cost |
|---|---:|
| `preflight` | `$0.001` |
| `preflight-batch` | `$0.025` |
| `forensics` | `$0.001` |
| `catalog-decoys` | `$0.005` |
| `whats-new` | `$0.001` |
| `alternatives` | `$0.005` |
| `credits` | `$0.50` |
| `watch` | `$0.01` |
| `verified` | `$1.00` |
| **Total** | **`$1.548`** |

That changed the refresh strategy. The sensible first pass was to refresh the routes that were missing or cheap enough to justify immediately, and let the `$1.00` `verified` route refresh on the next organic provider mint.

## What happened after settlement

Pre-settlement state:

| Route | Bazaar state before refresh |
|---|---|
| `preflight` | listed, stale template |
| `preflight-batch` | missing |
| `forensics` | listed, stale template |
| `catalog-decoys` | listed, stale template |
| `whats-new` | listed, stale template |
| `alternatives` | listed, stale template |
| `credits` | missing |
| `watch` | missing |
| `verified` | listed, stale template |

Path B refreshed eight routes:

```txt
preflight
preflight-batch
forensics
catalog-decoys
whats-new
alternatives
credits
watch
```

Expected spend was `$0.548` USDC. Observed recent settlement transfers from the test wallet totaled `$0.546` across 13 transactions because two close settlement batches happened during coordination and one route failed before settlement.

Useful net result:

- `preflight`, `forensics`, `catalog/decoys`, `whats-new`, `alternatives`, `credits`, and `watch` refreshed or became listed with literal route templates.
- `/credits` and `/watch` moved from missing to listed.
- `/verified` was intentionally skipped in the operator-paid refresh and later refreshed organically via a third-party mint.
- `/preflight-batch` initially failed with HTTP 503 before settlement. That was the right money outcome: the response said `payment_settled: false` and `settlement_skipped: true`, and the facilitator did not take funds.

`preflight-batch` needed a separate fix. The first implementation held a dedicated Timescale aggregate query over recent probe history and timed out at roughly 700 ms. The second implementation reused the same single-URL preflight core per deduped target with bounded concurrency three. After that change, a paid retry returned HTTP 200 in about 1.85 seconds and settled `$0.025` on Base mainnet.

After the indexing window, the CDP discovery API showed all nine x402station.io paid routes with literal route templates:

| Resource | Bazaar routeTemplate |
|---|---|
| `/api/v1/preflight` | `/api/v1/preflight` |
| `/api/v1/preflight-batch` | `/api/v1/preflight-batch` |
| `/api/v1/forensics` | `/api/v1/forensics` |
| `/api/v1/catalog/decoys` | `/api/v1/catalog/decoys` |
| `/api/v1/whats-new` | `/api/v1/whats-new` |
| `/api/v1/alternatives` | `/api/v1/alternatives` |
| `/api/v1/credits` | `/api/v1/credits` |
| `/api/v1/watch` | `/api/v1/watch` |
| `/api/v1/verified` | `/api/v1/verified` |

That is a distribution and metadata result, not demand. We do not count operator test settlements as usage traction.

## The 12-hour observable

Within roughly twelve hours of the Path B refresh, a wallet we had not seen before paid `$1.00 USDC` to mint a verified-badge on `https://api.venice.ai/api/v1/chat/completions`, the same endpoint we cite as a canonical zombie example in paid-route 402 challenge bodies.

The wallet also ran four cheap evaluation calls in the same minute: `/forensics`, `/whats-new`, `/catalog/decoys`, and `/alternatives`.

The mint result was `not_verified`. The endpoint failed the audit. The verified-flow returned the same answer the public signal already implied.

For this article, the point is narrow: clean settlement-pull metadata made the product surface discoverable enough for a third party to traverse the paid evaluation path. It is not a claim that our operator refresh spend was customer demand. It was distribution plumbing.

## Checklist for x402 operators

If you publish x402 routes and care about Bazaar discovery, this is the checklist I wish I had started with.

### 1. Decode the 402 challenge

Do not inspect only source code or manifests. Hit the route without payment and decode the header:

```ts
const response = await fetch("https://your-service.example/api/paid-route", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ example: true }),
});

const encoded = response.headers.get("payment-required");
const decoded = JSON.parse(Buffer.from(encoded!, "base64url").toString("utf8"));
console.log(decoded.extensions?.bazaar?.routeTemplate);
```

If the decoded artifact is wrong, do not settle. Settlement will only index the wrong thing faster.

### 2. Check your wrapper abstraction

If you use `@x402/next` and your route template becomes `:var1`, inspect whether your wrapper registered the route as `"*"`.

For a route-level handler where fulfillment must happen before settlement, prefer a literal route map with `withX402FromHTTPServer`:

```ts
const httpServer = new x402HTTPResourceServer(resourceServer, {
  "/api/v1/preflight": routeConfig,
});

export const POST = withX402FromHTTPServer(handler, httpServer);
```

Wrap that in a helper if you have multiple routes.

### 3. Preserve settlement semantics

Do not move to a broader proxy/middleware integration just because it has a nicer route map. First ask where settlement happens relative to:

- input validation
- database writes
- idempotency checks
- paid response construction
- failure responses

If a route creates state after payment, this ordering matters.

### 4. Budget your refresh

Settlement-pull discovery means every refresh has a route-price cost.

If one route costs `$1.00` and another costs `$0.001`, they should not be treated the same operationally. Refresh missing or distribution-critical routes first. Let expensive routes refresh on organic usage when that is acceptable.

### 5. Re-audit after the refresh window

After settlement, wait for the discovery pipeline to catch up and query the public discovery API again:

```bash
curl -s 'https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources?limit=500'
```

Then compare:

- route count
- product names
- descriptions
- route templates
- settlement recency
- any Bazaar extension response status

## Why this matters for agents

This is not just a catalog nicety.

Agents are increasingly using discovery surfaces as ranking inputs. A route with a clean name, useful description, recent settlement, and accurate template is easier for an agent to select and call correctly.

Bad metadata has a real cost:

- A wildcard `:var1` template makes the route look less concrete.
- Missing routes reduce product-surface coverage.
- Stale descriptions hide current capabilities.
- No recent settlement signal can make an otherwise useful route look abandoned.

For x402station.io, Bazaar metadata is directly tied to the job of Preflight by x402station.io: help agents decide what to pay before they sign. If the discovery layer cannot accurately represent the safety layer, the funnel is weaker.

## Related artifacts

- Service: [x402station.io](https://x402station.io)
- Manifest: [`/.well-known/x402`](https://x402station.io/.well-known/x402)
- OpenAPI: [`/api/openapi.json`](https://x402station.io/api/openapi.json)
- A2A agent card: [`/.well-known/agent-card.json`](https://x402station.io/.well-known/agent-card.json)
- Free Preflight trial: [`/api/v1/preflight-trial`](https://x402station.io/api/v1/preflight-trial)
- Paid Preflight: [`/api/v1/preflight`](https://x402station.io/api/v1/preflight)
- Dataset: [Hugging Face preflight-dataset-v0_1](https://huggingface.co/datasets/x402station/preflight-dataset-v0_1)
- Spec: [x402-signals](https://github.com/sF1nX/x402-signals)

If you are building an agent that pays x402 endpoints, wire a pre-payment check before the retry that carries `PAYMENT-SIGNATURE`.

If you are publishing x402 endpoints, decode your own 402 challenge before you trust your discovery metadata.
