feat(bazaar): add facilitator discovery service#2290
Draft
forest-builds wants to merge 1 commit into
Draft
Conversation
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>
|
@forest-builds is attempting to deploy a commit to the Coinbase Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the bazaar discovery backend that the spec defines and that
withBazaaralready calls. TodayGET /discovery/resourcesandGET /discovery/searchonly exist in the e2e test harness; this PR adds a real, swappable catalog and wires it into the x402.org facilitator.BazaarCataloginterface with two implementations —InMemoryBazaarCatalog(tests / local dev) andPostgresBazaarCatalog(Drizzle + tsvector full-text search, keyset cursor pagination,NULLS NOT DISTINCTcatalog key on PG 15+).installBazaarFacilitator(facilitator, catalog)registers the bazaar marker and a best-effortonAfterVerifyhook. Hook errors route through a configurableonErrorand never propagate — cataloging must not break verify.GET /facilitator/discovery/resourcesandGET /facilitator/discovery/search. They parse the spec's query envelope, call the catalog, and mirror the verify/settle error shape.@x402/extensions/bazaar/postgres;pgis an optional peer dep so Neon-HTTP / postgres-js consumers can bring their own driver vianew PostgresBazaarCatalog(drizzleDb).getBazaarCatalog()uses Postgres whenBAZAAR_DATABASE_URLis set, otherwise the in-memory catalog with aconsole.warn(mirrorsgetFacilitator()lazy-init pattern).Design decisions worth flagging
tsvector, notGENERATED ALWAYS AS … STORED.to_tsvectoris not provably immutable under somedefault_text_search_configsettings, which trips PG's generated-column immutability check. The BEFORE INSERT/UPDATE trigger is the canonical FTS pattern for that reason.NULLS NOT DISTINCTunique on(resource_url, method, tool_name). HTTP rows have NULLtool_name, MCP rows have NULLmethod. PG 15'sNULLS NOT DISTINCTmakes the right key collide as expected.rank DESC, id ASC— a tuple compare would assume uniform direction. Predicate isrank < cr OR (rank = cr AND id > ci).PaymentRequirementsper 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
PostgresBazaarCatalog. AcreateNeonBazaarCataloghelper is an easy next PR if there's interest.partialResultsheuristic insearch. Advisory field; spec allows omitting.last_seen_atis 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:checkpnpm -F @x402/extensions test— 436 passed, 11 skipped (db tests gated behindRUN_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 bothdist/esm/bazaar/postgres/migrations/anddist/cjs/bazaar/postgres/migrations/via tsup postbuild copypnpm -F site lint:check && pnpm -F site format:check && pnpm -F site build— clean; both new routes show up in the Next.js route tablee2e/extensions/bazaar.tsalready exercises/discovery/resources?limit=1000and/discovery/search?query=Xagainst test facilitators. Point it at a locally-running site (withBAZAAR_DATABASE_URLset to a throwaway PG) for an end-to-end smoke test.What's in the diff
extensions/src/bazaar/facilitator-service/catalog.tsBazaarCataloginterface +CatalogUpsertInputextensions/src/bazaar/facilitator-service/installFacilitator.tsinstallBazaarFacilitator()factoryextensions/src/bazaar/facilitator-service/memoryCatalog.tsInMemoryBazaarCatalogextensions/src/bazaar/facilitator-service/postgres/{schema,postgresCatalog,migrate,factory}.tspg-backed factoryextensions/src/bazaar/facilitator-service/postgres/migrations/0001_init.sqlextensions/test/bazaar-facilitator-service.test.tsextensions/test/bazaar-postgres.test.tssite/app/facilitator/discovery/{catalog.ts,resources/route.ts,search/route.ts}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