Skip to content

feat(bazaar): add facilitator discovery service#2290

Draft
forest-builds wants to merge 1 commit into
x402-foundation:mainfrom
forest-builds:feat/bazaar-discovery-service
Draft

feat(bazaar): add facilitator discovery service#2290
forest-builds wants to merge 1 commit into
x402-foundation:mainfrom
forest-builds:feat/bazaar-discovery-service

Conversation

@forest-builds
Copy link
Copy Markdown

Summary

Implements the bazaar discovery backend that the spec defines and that withBazaar already calls. Today GET /discovery/resources and GET /discovery/search only exist in the e2e test harness; this PR adds a real, swappable catalog and wires it into the x402.org facilitator.

  • BazaarCatalog interface with two implementations — InMemoryBazaarCatalog (tests / local dev) and PostgresBazaarCatalog (Drizzle + tsvector full-text search, keyset cursor pagination, NULLS NOT DISTINCT catalog key on PG 15+).
  • installBazaarFacilitator(facilitator, catalog) registers the bazaar marker and a best-effort onAfterVerify hook. Hook errors route through a configurable onError and never propagate — cataloging must not break verify.
  • Two new routes on the site: GET /facilitator/discovery/resources and GET /facilitator/discovery/search. They parse the spec's query envelope, call the catalog, and mirror the verify/settle error shape.
  • Postgres is opt-in: lives behind subpath export @x402/extensions/bazaar/postgres; pg is an optional peer dep so Neon-HTTP / postgres-js consumers can bring their own driver via new PostgresBazaarCatalog(drizzleDb).
  • Catalog selection: site's getBazaarCatalog() uses Postgres when BAZAAR_DATABASE_URL is set, otherwise the in-memory catalog with a console.warn (mirrors getFacilitator() lazy-init pattern).

Design decisions worth flagging

  • Trigger-based tsvector, not GENERATED ALWAYS AS … STORED. to_tsvector is not provably immutable under some default_text_search_config settings, which trips PG's generated-column immutability check. The BEFORE INSERT/UPDATE trigger is the canonical FTS pattern for that reason.
  • NULLS NOT DISTINCT unique on (resource_url, method, tool_name). HTTP rows have NULL tool_name, MCP rows have NULL method. PG 15's NULLS NOT DISTINCT makes the right key collide as expected.
  • Keyset cursor with explicit predicate. Sorted rank DESC, id ASC — a tuple compare would assume uniform direction. Predicate is rank < cr OR (rank = cr AND id > ci).
  • Accepts merging happens in the catalog. Facilitators only see the one selected PaymentRequirements per verify, so the catalog merges across observations deduped by (scheme, network). Same metadata as the latest payment is propagated so renamed services / updated descriptions converge.

Out of scope / follow-ups

  • No Neon HTTP factory. Consumers can still pass their own Drizzle db to PostgresBazaarCatalog. A createNeonBazaarCatalog helper is an easy next PR if there's interest.
  • No partialResults heuristic in search. Advisory field; spec allows omitting.
  • No site route-level tests. The catalog layer is well covered; the site lacks a route-test harness today. Adding vitest + supertest for the site is a clean follow-up.
  • No stale-entry pruning. last_seen_at is recorded but not yet acted on — leaves room for a periodic job to soft-delete endpoints that haven't been seen in N days.

Test plan

  • pnpm -F @x402/extensions lint:check && pnpm -F @x402/extensions format:check
  • pnpm -F @x402/extensions test436 passed, 11 skipped (db tests gated behind RUN_DB_TESTS=1)
  • RUN_DB_TESTS=1 pnpm -F @x402/extensions test — full 447 passed locally with Testcontainers (Docker + postgres:16-alpine)
  • pnpm -F @x402/extensions build — succeeds; migration SQL lands in both dist/esm/bazaar/postgres/migrations/ and dist/cjs/bazaar/postgres/migrations/ via tsup postbuild copy
  • pnpm -F site lint:check && pnpm -F site format:check && pnpm -F site build — clean; both new routes show up in the Next.js route table
  • E2E oracle (suggested for CI): the repo's e2e/extensions/bazaar.ts already exercises /discovery/resources?limit=1000 and /discovery/search?query=X against test facilitators. Point it at a locally-running site (with BAZAAR_DATABASE_URL set to a throwaway PG) for an end-to-end smoke test.

What's in the diff

Path What it does
extensions/src/bazaar/facilitator-service/catalog.ts BazaarCatalog interface + CatalogUpsertInput
extensions/src/bazaar/facilitator-service/installFacilitator.ts installBazaarFacilitator() factory
extensions/src/bazaar/facilitator-service/memoryCatalog.ts InMemoryBazaarCatalog
extensions/src/bazaar/facilitator-service/postgres/{schema,postgresCatalog,migrate,factory}.ts Drizzle schema + Postgres catalog + migrations runner + pg-backed factory
extensions/src/bazaar/facilitator-service/postgres/migrations/0001_init.sql Table, trigger, indexes
extensions/test/bazaar-facilitator-service.test.ts 17 in-memory + hook tests
extensions/test/bazaar-postgres.test.ts 11 Testcontainers tests
site/app/facilitator/discovery/{catalog.ts,resources/route.ts,search/route.ts} Catalog singleton + two route handlers

Draft for now while I do one more pass on the GitHub diff UI; I'll mark ready once I'm happy.

🤖 Generated with Claude Code

Implements the bazaar discovery backend that the spec defines and
`withBazaar` already calls (`GET /discovery/resources` and `GET
/discovery/search`). Previously these routes existed only in e2e test
harnesses; this PR adds a real, swappable catalog and wires it into the
x402.org facilitator.

Extensions package
- New `BazaarCatalog` interface with two implementations:
  - `InMemoryBazaarCatalog` for tests / local dev.
  - `PostgresBazaarCatalog` (Drizzle + tsvector full-text search, keyset
    cursor pagination, NULLS NOT DISTINCT catalog key on PG 15+).
- `installBazaarFacilitator(facilitator, catalog)` registers the bazaar
  marker and a best-effort `onAfterVerify` hook that catalogs each
  verified payment. Catalog errors are routed through a configurable
  `onError` and never propagate — cataloging must not break verify.
- Postgres backend lives behind a new subpath export
  `@x402/extensions/bazaar/postgres`; `pg` is an optional peer dep, so
  consumers using Neon HTTP / postgres-js can bring their own driver.
- Migration runner ships SQL via a tsup postbuild copy into both
  `dist/esm/bazaar/postgres/migrations` and the cjs equivalent.

Site
- New routes `/facilitator/discovery/resources` and
  `/facilitator/discovery/search` parse the query envelope, call the
  catalog, and mirror the verify/settle error shape.
- A `getBazaarCatalog()` singleton selects Postgres when
  `BAZAAR_DATABASE_URL` is set, otherwise falls back to the in-memory
  catalog with a warning (mirrors the existing `getFacilitator()`
  pattern).
- Hook is installed alongside the existing extension registrations.

Tests
- 17 in-memory tests cover upsert, accepts merging, method/toolName
  keying, filters, offset pagination, search ranking, cursor
  pagination, and dynamic-route canonicalization.
- 11 Postgres tests using `@testcontainers/postgresql` cover the same
  matrix plus tsvector ranking, keyset pagination correctness, and
  migration idempotency. Gated behind `RUN_DB_TESTS=1`.
- All 436 unit tests pass; the 11 db tests are skipped without
  `RUN_DB_TESTS=1`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

@forest-builds is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added typescript sdk Changes to core v2 packages website labels May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk Changes to core v2 packages typescript website

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant