Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions typescript/packages/extensions/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Drizzle Kit config for bazaar Postgres schema introspection / generation.
*
* The schema is shipped along with hand-written SQL migrations in
* `src/bazaar/facilitator-service/postgres/migrations`. Use `pnpm db:generate`
* to materialize new diffs from `schema.ts` (you may need to merge the output
* into `0001_init.sql` if you're adjusting the generated tsvector column).
*/

import { defineConfig } from "drizzle-kit";

export default defineConfig({
schema: "./src/bazaar/facilitator-service/postgres/schema.ts",
out: "./src/bazaar/facilitator-service/postgres/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.BAZAAR_DATABASE_URL ?? "postgres://localhost:5432/postgres",
},
});
34 changes: 32 additions & 2 deletions typescript/packages/extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
"types": "./dist/cjs/index.d.ts",
"scripts": {
"start": "tsx --env-file=.env index.ts",
"build": "tsup",
"build": "tsup && node -e \"const{cpSync}=require('fs');cpSync('src/bazaar/facilitator-service/postgres/migrations','dist/esm/bazaar/postgres/migrations',{recursive:true});cpSync('src/bazaar/facilitator-service/postgres/migrations','dist/cjs/bazaar/postgres/migrations',{recursive:true})\"",
"test": "vitest run",
"test:db": "RUN_DB_TESTS=1 vitest run",
"test:watch": "vitest",
"watch": "tsc --watch",
"format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"",
"format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"",
"lint": "eslint . --ext .ts --fix",
"lint:check": "eslint . --ext .ts"
"lint:check": "eslint . --ext .ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
},
"keywords": [
"x402",
Expand All @@ -27,13 +30,17 @@
"description": "x402 Payment Protocol Extensions",
"devDependencies": {
"@eslint/js": "^9.24.0",
"@testcontainers/postgresql": "^10.16.0",
"@types/node": "^22.13.4",
"@types/pg": "^8.11.10",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"drizzle-kit": "^0.30.0",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-prettier": "^5.2.6",
"pg": "^8.13.1",
"prettier": "3.5.2",
"tsup": "^8.4.0",
"tsx": "^4.19.2",
Expand All @@ -47,12 +54,25 @@
"@scure/base": "^1.2.6",
"@x402/core": "workspace:~",
"ajv": "^8.17.1",
"drizzle-orm": "^0.36.4",
"jose": "^5.9.6",
"@signinwithethereum/siwe": "^4.1.0",
"tweetnacl": "^1.0.3",
"viem": "^2.48.11",
"zod": "^3.24.2"
},
"peerDependencies": {
"pg": "^8.13.0",
"@neondatabase/serverless": "^0.10.0"
},
"peerDependenciesMeta": {
"pg": {
"optional": true
},
"@neondatabase/serverless": {
"optional": true
}
},
"exports": {
".": {
"import": {
Expand All @@ -74,6 +94,16 @@
"default": "./dist/cjs/bazaar/index.js"
}
},
"./bazaar/postgres": {
"import": {
"types": "./dist/esm/bazaar/postgres/index.d.mts",
"default": "./dist/esm/bazaar/postgres/index.mjs"
},
"require": {
"types": "./dist/cjs/bazaar/postgres/index.d.ts",
"default": "./dist/cjs/bazaar/postgres/index.js"
}
},
"./sign-in-with-x": {
"import": {
"types": "./dist/esm/sign-in-with-x/index.d.mts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* BazaarCatalog: storage interface for cataloging discovered x402 resources.
*
* Facilitators receive `bazaar` discovery extensions inside payment payloads.
* `installBazaarFacilitator` extracts each discovery via `extractDiscoveryInfo`
* and forwards the result here. The interface is intentionally narrow so it can
* be implemented against an in-memory Map, Postgres, Redis, or anything else.
*
* The list/search response shapes match the spec's facilitator discovery API
* (`GET /discovery/resources`, `GET /discovery/search`); see
* `specs/extensions/bazaar.md` "Optional Discovery Endpoints".
*/

import type { PaymentRequirements } from "@x402/core/types";
import type { DiscoveredResource } from "../facilitator";
import type {
ListDiscoveryResourcesParams,
SearchDiscoveryResourcesParams,
DiscoveryResource,
DiscoveryResourcesResponse,
SearchDiscoveryResourcesResponse,
} from "../facilitatorClient";

export type {
ListDiscoveryResourcesParams,
SearchDiscoveryResourcesParams,
DiscoveryResource,
DiscoveryResourcesResponse,
SearchDiscoveryResourcesResponse,
};

/**
* Input to `BazaarCatalog.upsert`. The catalog is responsible for merging this
* single observation into any existing row for the same canonical resource.
*
* Note: at verify time the facilitator only sees the **one** PaymentRequirements
* the client selected — not the full accepts[] from the original 402. Catalog
* implementations must merge `paymentRequirements` into the row's accepts list
* (deduped by `(scheme, network)`) so the catalog converges on the full set
* across many payments.
*/
export interface CatalogUpsertInput {
/** Extracted discovery resource as produced by `extractDiscoveryInfo`. */
discovered: DiscoveredResource;
/** The single PaymentRequirements selected for this payment. */
paymentRequirements: PaymentRequirements;
/** Optional extra extensions from the payment payload, stored alongside the row. */
extensions?: Record<string, unknown>;
}

export interface BazaarCatalog {
/**
* Insert or update a row for the canonical resource. Must merge accepts
* (deduped by scheme+network) and update lastSeenAt on upsert. Idempotent.
*/
upsert(input: CatalogUpsertInput): Promise<void>;

/** List cataloged resources with optional filters and offset pagination. */
list(params?: ListDiscoveryResourcesParams): Promise<DiscoveryResourcesResponse>;

/** Search cataloged resources by natural-language query. Cursor pagination is optional. */
search(params: SearchDiscoveryResourcesParams): Promise<SearchDiscoveryResourcesResponse>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Bazaar Facilitator Service — server-side helpers for cataloging x402 discoveries.
*
* The SDK exports here let a facilitator host the discovery endpoints required
* by the bazaar spec. The flow is:
*
* 1. Pick or implement a `BazaarCatalog` (see `InMemoryBazaarCatalog` for a
* reference / test implementation, or `./postgres` for a Postgres-backed one).
* 2. Call `installBazaarFacilitator(facilitator, catalog)` to register the
* `bazaar` extension and the post-verify cataloging hook.
* 3. Wire `catalog.list(...)` / `catalog.search(...)` behind your HTTP routes
* at `GET /discovery/resources` and `GET /discovery/search`.
*/

export type {
BazaarCatalog,
CatalogUpsertInput,
DiscoveryResource,
DiscoveryResourcesResponse,
SearchDiscoveryResourcesResponse,
ListDiscoveryResourcesParams,
SearchDiscoveryResourcesParams,
} from "./catalog";

export {
installBazaarFacilitator,
type InstallBazaarFacilitatorOptions,
} from "./installFacilitator";

export { InMemoryBazaarCatalog } from "./memoryCatalog";
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* One-call wiring of the `bazaar` extension into an x402Facilitator.
*
* Registers the `bazaar` marker (so `/supported` advertises it) and installs an
* `onAfterVerify` hook that extracts the discovery info from each verified
* payment and forwards it to the supplied `BazaarCatalog`.
*
* The hook is best-effort: any catalog error is logged but never propagated,
* because cataloging is purely observational and must not break payment
* verification.
*/

import type { x402Facilitator } from "@x402/core/facilitator";
import { BAZAAR } from "../types";
import { extractDiscoveryInfo } from "../facilitator";
import type { BazaarCatalog } from "./catalog";

export interface InstallBazaarFacilitatorOptions {
/**
* Called when the catalog upsert throws. Defaults to `console.warn`.
* Provide your own logger to integrate with the facilitator's existing
* observability stack.
*/
onError?: (error: unknown) => void;
}

/**
* Registers the bazaar extension marker and installs the cataloging hook.
*
* @param facilitator - The x402Facilitator instance to wire into.
* @param catalog - The BazaarCatalog implementation that will receive discoveries.
* @param options - Optional overrides (custom error handler).
* @returns The same facilitator for chaining.
*
* @example
* ```typescript
* import { x402Facilitator } from "@x402/core/facilitator";
* import { installBazaarFacilitator, InMemoryBazaarCatalog } from "@x402/extensions";
*
* const facilitator = new x402Facilitator();
* // ... register schemes ...
* installBazaarFacilitator(facilitator, new InMemoryBazaarCatalog());
* ```
*/
export function installBazaarFacilitator(
facilitator: x402Facilitator,
catalog: BazaarCatalog,
options: InstallBazaarFacilitatorOptions = {},
): x402Facilitator {
const onError =
options.onError ??
((err: unknown) => {
console.warn(`[bazaar] catalog upsert failed: ${err instanceof Error ? err.message : err}`);
});

return facilitator.registerExtension(BAZAAR).onAfterVerify(async context => {
let discovered;
try {
discovered = extractDiscoveryInfo(context.paymentPayload, context.requirements);
} catch (err) {
onError(err);
return;
}
if (!discovered) return;

try {
await catalog.upsert({
discovered,
paymentRequirements: context.requirements,
extensions: context.paymentPayload.extensions as Record<string, unknown> | undefined,
});
} catch (err) {
onError(err);
}
});
}
Loading
Loading