diff --git a/bun.lock b/bun.lock
index 2c8ebd05..b2d17e79 100644
--- a/bun.lock
+++ b/bun.lock
@@ -117,6 +117,22 @@
"@tanstack/react-query",
],
},
+ "packages/catalog": {
+ "name": "@lucid-agents/catalog",
+ "version": "0.1.0",
+ "dependencies": {
+ "@lucid-agents/types": "workspace:*",
+ "csv-parse": "^5.6.0",
+ "yaml": "^2.7.1",
+ "zod": "catalog:",
+ },
+ "devDependencies": {
+ "@lucid-agents/core": "workspace:*",
+ "@lucid-agents/http": "workspace:*",
+ "tsup": "catalog:",
+ "typescript": "catalog:",
+ },
+ },
"packages/cli": {
"name": "@lucid-agents/cli",
"version": "2.5.0",
@@ -173,6 +189,7 @@
"@lucid-agents/a2a": "workspace:*",
"@lucid-agents/analytics": "workspace:*",
"@lucid-agents/ap2": "workspace:*",
+ "@lucid-agents/catalog": "workspace:*",
"@lucid-agents/core": "workspace:*",
"@lucid-agents/hono": "workspace:*",
"@lucid-agents/http": "workspace:*",
@@ -702,6 +719,8 @@
"@lucid-agents/api-sdk": ["@lucid-agents/api-sdk@workspace:packages/api-sdk"],
+ "@lucid-agents/catalog": ["@lucid-agents/catalog@workspace:packages/catalog"],
+
"@lucid-agents/cli": ["@lucid-agents/cli@workspace:packages/cli"],
"@lucid-agents/core": ["@lucid-agents/core@workspace:packages/core"],
@@ -1616,6 +1635,8 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+ "csv-parse": ["csv-parse@5.6.0", "", {}, "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q=="],
+
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
diff --git a/lucid-docs/content/docs/packages/catalog.mdx b/lucid-docs/content/docs/packages/catalog.mdx
new file mode 100644
index 00000000..2271b1fd
--- /dev/null
+++ b/lucid-docs/content/docs/packages/catalog.mdx
@@ -0,0 +1,369 @@
+---
+title: '@lucid-agents/catalog'
+description: YAML/CSV catalog-driven route generation for large product catalogs.
+icon: List
+---
+
+The catalog extension generates entrypoint routes from YAML or CSV product catalog files. Instead of writing hundreds of `addEntrypoint()` calls, define your catalog in a single file and let the extension register all routes at build time.
+
+## Installation
+
+```bash
+bun add @lucid-agents/catalog
+```
+
+## Basic usage
+
+Define a product catalog in YAML:
+
+```yaml
+# products.yaml
+products:
+ - key: sentiment
+ name: Sentiment Analysis
+ description: Analyze text sentiment
+ price: "0.05"
+ - key: summarize
+ name: Text Summarizer
+ description: Summarize long-form content
+ price:
+ invoke: "0.50"
+ stream: "0.10"
+ - key: health
+ name: Health Check
+ description: Free health endpoint
+```
+
+Wire it into your agent:
+
+```typescript
+import { createAgent } from '@lucid-agents/core';
+import { http } from '@lucid-agents/http';
+import { catalog } from '@lucid-agents/catalog';
+import { createAgentApp } from '@lucid-agents/hono';
+
+const agent = await createAgent({
+ name: 'store-agent',
+ version: '1.0.0',
+})
+ .use(http())
+ .use(catalog({ file: './products.yaml' }))
+ .build();
+
+const { app } = await createAgentApp(agent);
+```
+
+Each item in the YAML file becomes a discoverable, invocable entrypoint on your agent. Items with a `price` field will require payment when combined with the payments or MPP extensions.
+
+## YAML format
+
+YAML catalogs support a `products` wrapper object or a top-level array:
+
+```yaml
+products:
+ - key: translate
+ name: Quick Translate
+ description: Translate text between languages
+ price: "0.10"
+ network: "eip155:8453"
+ metadata:
+ category: nlp
+ rateLimit: 200
+```
+
+### Supported fields
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `key` | `string` | Yes | Unique route identifier. Becomes the entrypoint key. |
+| `name` | `string` | Yes | Human-readable display name. |
+| `description` | `string` | No | Description shown in the agent manifest. |
+| `price` | `string` or `object` | No | Flat USD price, or `{ invoke, stream }` for per-mode pricing. |
+| `network` | `string` | No | CAIP-2 network identifier (e.g., `eip155:8453`). |
+| `metadata` | `object` | No | Arbitrary key-value pairs attached to the entrypoint. |
+
+### Price variants
+
+Flat price (same for invoke and stream):
+
+```yaml
+- key: summarize
+ name: Summarize
+ price: "0.50"
+```
+
+Separate invoke and stream prices:
+
+```yaml
+- key: generate
+ name: Generate Text
+ price:
+ invoke: "1.00"
+ stream: "0.20"
+```
+
+No price (free entrypoint):
+
+```yaml
+- key: health
+ name: Health Check
+```
+
+## CSV format
+
+CSV catalogs use a header row. Metadata columns use the `meta_` prefix convention:
+
+```csv
+key,name,description,price,network,meta_category,meta_tier
+ocr,OCR Scan,Extract text from images,0.50,,vision,standard
+embed,Text Embedding,Generate vector embeddings,0.02,,ml,basic
+classify,Text Classifier,Classify text,0.10,,nlp,basic
+```
+
+| Column | Required | Description |
+|--------|----------|-------------|
+| `key` | Yes | Unique route identifier. |
+| `name` | Yes | Display name. |
+| `description` | No | Route description. |
+| `price` | No | USD price string. Empty = free. |
+| `network` | No | CAIP-2 network identifier. |
+| `meta_*` | No | Collected into `metadata` with prefix stripped. `meta_category` becomes `metadata.category`. |
+
+
+CSV does not support the `{ invoke, stream }` price object syntax. Use YAML if you need per-mode pricing.
+
+
+## Configuration
+
+### CatalogExtensionOptions
+
+```typescript
+type CatalogExtensionOptions = {
+ file: string; // Path to .yaml, .yml, or .csv file
+ keyPrefix?: string; // Prefix for all item keys
+ network?: string; // Default CAIP-2 network
+ handlerFactory?: HandlerFactory; // Generate handlers from items
+ inputSchema?: z.ZodTypeAny; // Custom input schema for all routes
+};
+```
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `file` | (required) | Path to the catalog file. Supports `.yaml`, `.yml`, and `.csv`. |
+| `keyPrefix` | `undefined` | Prepended to every item key. `"store/"` turns `"widget"` into `"store/widget"`. |
+| `network` | `undefined` | Default network for items that don't specify their own. |
+| `handlerFactory` | `undefined` | Function receiving a `CatalogItem`, returning a handler function. |
+| `inputSchema` | `z.object({ params: z.record(...).optional() })` | Zod schema applied as input to all generated entrypoints. |
+
+### Handler factory
+
+The handler factory generates a route handler for each catalog item. This is where you connect catalog items to actual business logic:
+
+```typescript
+import type { CatalogItem } from '@lucid-agents/catalog';
+
+const handlerFactory = (item: CatalogItem) => {
+ return async (ctx: { input: { params?: Record } }) => {
+ const model = item.metadata?.model as string ?? 'gpt-4o-mini';
+
+ // Route to different backends based on catalog metadata
+ const result = await callModel(model, ctx.input);
+
+ return {
+ output: {
+ product: item.key,
+ result,
+ },
+ };
+ };
+};
+
+agent.use(catalog({
+ file: './products.yaml',
+ handlerFactory,
+}));
+```
+
+### Key prefix
+
+Use `keyPrefix` to namespace routes, especially when loading multiple catalogs:
+
+```typescript
+const agent = await createAgent({ name: 'multi-catalog', version: '1.0.0' })
+ .use(http())
+ .use(catalog({ file: './nlp-services.yaml', keyPrefix: 'nlp/' }))
+ .use(catalog({ file: './vision-services.csv', keyPrefix: 'vision/' }))
+ .build();
+```
+
+This produces routes like `nlp/sentiment`, `nlp/translate`, `vision/classify`.
+
+## Integration with payments
+
+### With x402
+
+Combine with the payments extension for x402 payment gating. Any catalog item with a `price` field will automatically require payment:
+
+```typescript
+import { payments, paymentsFromEnv } from '@lucid-agents/payments';
+import { catalog } from '@lucid-agents/catalog';
+
+const agent = await createAgent({
+ name: 'paid-catalog',
+ version: '1.0.0',
+})
+ .use(http())
+ .use(payments({ config: paymentsFromEnv() }))
+ .use(catalog({
+ file: './products.yaml',
+ handlerFactory: (item) => async ({ input }) => ({
+ output: { product: item.name, data: 'processed' },
+ }),
+ }))
+ .build();
+```
+
+### With MPP
+
+Combine with the MPP extension for multi-method payments (Tempo, Stripe, Lightning):
+
+```typescript
+import { mpp, tempo } from '@lucid-agents/mpp';
+import { catalog } from '@lucid-agents/catalog';
+
+const agent = await createAgent({
+ name: 'mpp-catalog',
+ version: '1.0.0',
+})
+ .use(http())
+ .use(mpp({
+ config: {
+ methods: [tempo.server({ currency: '0x...', recipient: '0x...' })],
+ currency: 'usd',
+ },
+ }))
+ .use(catalog({
+ file: './products.yaml',
+ keyPrefix: 'store/',
+ handlerFactory: (item) => async ({ input }) => ({
+ output: { product: item.key, price: item.price },
+ }),
+ }))
+ .build();
+```
+
+Both x402 and MPP can coexist on the same agent alongside catalog.
+
+## Runtime access
+
+After building, the catalog items are available on the runtime for introspection:
+
+```typescript
+const agent = await createAgent({ ... })
+ .use(catalog({ file: './products.yaml' }))
+ .build();
+
+// Access parsed catalog items
+const items = (agent as any).catalog.items;
+console.log(`Loaded ${items.length} products`);
+
+for (const item of items) {
+ console.log(`${item.key}: ${item.name} — $${item.price ?? 'free'}`);
+}
+```
+
+## API reference
+
+### parseCatalogYaml(content)
+
+Parses a YAML string into validated `CatalogItem[]`. Accepts either a top-level array or `{ products: [...] }` wrapper.
+
+```typescript
+import { parseCatalogYaml } from '@lucid-agents/catalog';
+
+const items = parseCatalogYaml(yamlString);
+```
+
+Throws if any item fails validation against `CatalogItemSchema`.
+
+### parseCatalogCsv(content)
+
+Parses a CSV string into validated `CatalogItem[]`. Requires a `key` column. Columns prefixed with `meta_` are collected into `metadata`.
+
+```typescript
+import { parseCatalogCsv } from '@lucid-agents/catalog';
+
+const items = parseCatalogCsv(csvString);
+```
+
+### generateEntrypoints(items, options?)
+
+Converts `CatalogItem[]` into `EntrypointDef[]` ready for registration:
+
+```typescript
+import { generateEntrypoints } from '@lucid-agents/catalog';
+
+const entrypoints = generateEntrypoints(items, {
+ keyPrefix: 'api/',
+ network: 'eip155:8453',
+ handlerFactory: (item) => async ({ input }) => ({
+ output: { result: item.name },
+ }),
+});
+```
+
+### CatalogItemSchema
+
+Zod schema for validating catalog items. Useful for custom parsing pipelines:
+
+```typescript
+import { CatalogItemSchema } from '@lucid-agents/catalog';
+
+const result = CatalogItemSchema.safeParse({
+ key: 'test',
+ name: 'Test Item',
+ price: '0.01',
+});
+```
+
+## Network support
+
+Network identifiers use [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) format:
+
+| Network | CAIP-2 ID |
+|---------|-----------|
+| Base | `eip155:8453` |
+| Base Sepolia | `eip155:84532` |
+| Ethereum | `eip155:1` |
+| Solana Mainnet | `solana:mainnet` |
+| Solana Devnet | `solana:devnet` |
+
+Set a default via the `network` option, or override per item in your catalog file. Item-level network always takes precedence over the global default.
+
+## Exports
+
+```typescript
+// Extension
+export { catalog } from '@lucid-agents/catalog';
+
+// Parsers
+export { parseCatalogYaml, parseCatalogCsv } from '@lucid-agents/catalog';
+
+// Entrypoint generation
+export { generateEntrypoints } from '@lucid-agents/catalog';
+
+// Schema & types
+export {
+ CatalogItemSchema,
+ type CatalogItem,
+ type CatalogConfig,
+ type CatalogExtensionOptions,
+} from '@lucid-agents/catalog';
+```
+
+## Related
+
+- [Payments Extension](/docs/packages/payments) — x402 payment protocol (USDC)
+- [MPP Extension](/docs/packages/mpp) — Machine Payments Protocol (multi-method)
+- [HTTP Extension](/docs/packages/http) — HTTP request/response handling
+- [Core](/docs/packages/core) — Agent runtime and extension system
diff --git a/lucid-docs/content/docs/packages/index.mdx b/lucid-docs/content/docs/packages/index.mdx
index 8284100c..c2a0d285 100644
--- a/lucid-docs/content/docs/packages/index.mdx
+++ b/lucid-docs/content/docs/packages/index.mdx
@@ -53,6 +53,11 @@ Optional capabilities you can add to your agent:
description="Machine Payments Protocol — multi-method payments via HTTP 402"
href="/docs/packages/mpp"
/>
+
async ({ input }) => {
+ return { output: { product: item.key } };
+ },
+}));
+```
+
+**`CatalogExtensionOptions`**
+
+| Option | Type | Required | Description |
+|--------|------|----------|-------------|
+| `file` | `string` | Yes | Path to the `.yaml`, `.yml`, or `.csv` catalog file. |
+| `keyPrefix` | `string` | No | Prefix prepended to every item key (e.g., `"store/"` turns `"widget"` into `"store/widget"`). |
+| `network` | `string` | No | Default CAIP-2 network for items that do not specify their own. |
+| `handlerFactory` | `HandlerFactory` | No | Function that receives a `CatalogItem` and returns a handler function. |
+| `inputSchema` | `z.ZodTypeAny` | No | Zod schema applied to all generated entrypoints. Defaults to `z.object({ params: z.record(z.string(), z.unknown()).optional() })`. |
+
+### `parseCatalogYaml(content)`
+
+Parses a YAML string into an array of validated `CatalogItem` objects.
+
+```typescript
+import { parseCatalogYaml } from '@lucid-agents/catalog';
+
+const items = parseCatalogYaml(`
+products:
+ - key: weather
+ name: Weather Lookup
+ price: "0.001"
+`);
+// items: CatalogItem[]
+```
+
+Accepts either a top-level array or an object with a `products` key. Throws if any item fails validation.
+
+### `parseCatalogCsv(content)`
+
+Parses a CSV string into an array of validated `CatalogItem` objects.
+
+```typescript
+import { parseCatalogCsv } from '@lucid-agents/catalog';
+
+const items = parseCatalogCsv(`key,name,price
+weather,Weather Lookup,0.001
+translate,Translation,0.002`);
+// items: CatalogItem[]
+```
+
+Requires a `key` column. Columns prefixed with `meta_` are collected into the `metadata` field. Throws if any row fails validation.
+
+### `generateEntrypoints(items, options?)`
+
+Converts an array of `CatalogItem` objects into `EntrypointDef` objects ready for registration.
+
+```typescript
+import { generateEntrypoints } from '@lucid-agents/catalog';
+
+const entrypoints = generateEntrypoints(items, {
+ keyPrefix: 'api/',
+ network: 'eip155:8453',
+ handlerFactory: (item) => async ({ input }) => {
+ return { output: { result: item.name } };
+ },
+});
+
+for (const ep of entrypoints) {
+ runtime.entrypoints.add(ep);
+}
+```
+
+**`GenerateOptions`**
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `keyPrefix` | `string` | Prefix prepended to each item key. |
+| `network` | `string` | Default network for items without one. |
+| `handlerFactory` | `HandlerFactory` | Generates a handler for each item. |
+| `inputSchema` | `z.ZodTypeAny` | Input schema for all entrypoints. |
+
+### `CatalogItemSchema`
+
+Zod schema used to validate catalog items. Useful for custom parsing or validation pipelines.
+
+```typescript
+import { CatalogItemSchema } from '@lucid-agents/catalog';
+
+const result = CatalogItemSchema.safeParse({
+ key: 'test',
+ name: 'Test Item',
+ price: '0.01',
+});
+```
+
+### `CatalogItem`
+
+TypeScript type inferred from `CatalogItemSchema`:
+
+```typescript
+type CatalogItem = {
+ key: string;
+ name: string;
+ description?: string;
+ price?: string | { invoke?: string; stream?: string };
+ network?: string;
+ metadata?: Record;
+};
+```
+
+## Integration Examples
+
+### With x402 Payments
+
+Combine catalog routes with x402 payment gating:
+
+```typescript
+import { createAgent } from '@lucid-agents/core';
+import { http } from '@lucid-agents/http';
+import { payments, paymentsFromEnv } from '@lucid-agents/payments';
+import { catalog } from '@lucid-agents/catalog';
+import { createAgentApp } from '@lucid-agents/hono';
+
+const agent = await createAgent({
+ name: 'paid-catalog-agent',
+ version: '1.0.0',
+ description: 'Catalog agent with x402 payments',
+})
+ .use(http())
+ .use(payments({ config: paymentsFromEnv() }))
+ .use(catalog({
+ file: './products.yaml',
+ handlerFactory: (item) => async ({ input }) => {
+ return { output: { product: item.name, data: 'processed' } };
+ },
+ }))
+ .build();
+
+const { app } = await createAgentApp(agent);
+export default app;
+```
+
+Each catalog item with a `price` field will automatically require x402 payment.
+
+### With MPP (Machine Payments Protocol)
+
+```typescript
+import { createAgent } from '@lucid-agents/core';
+import { http } from '@lucid-agents/http';
+import { mpp, tempo } from '@lucid-agents/mpp';
+import { catalog } from '@lucid-agents/catalog';
+import { createAgentApp } from '@lucid-agents/hono';
+
+const agent = await createAgent({
+ name: 'mpp-catalog-agent',
+ version: '1.0.0',
+ description: 'Catalog agent with MPP payments',
+})
+ .use(http())
+ .use(mpp({
+ config: {
+ methods: [tempo.server({ currency: '0x...', recipient: '0x...' })],
+ currency: 'usd',
+ },
+ }))
+ .use(catalog({
+ file: './services.yaml',
+ network: 'eip155:8453',
+ handlerFactory: (item) => async ({ input }) => {
+ return { output: { service: item.name } };
+ },
+ }))
+ .build();
+
+const { app } = await createAgentApp(agent);
+export default app;
+```
+
+See `packages/examples/src/catalog/catalog-mpp-store.ts` for a full working example.
+
+### Custom Handler Factory
+
+The handler factory receives the full `CatalogItem`, giving you access to metadata for dynamic behavior:
+
+```typescript
+import type { CatalogItem } from '@lucid-agents/catalog';
+
+const handlerFactory = (item: CatalogItem) => async ({ input }: any) => {
+ const model = item.metadata?.model as string ?? 'gpt-4o-mini';
+ const category = item.metadata?.category as string ?? 'general';
+
+ // Route to different backends based on metadata
+ const result = await processWithModel(model, category, input);
+
+ return { output: result };
+};
+
+agent.use(catalog({
+ file: './products.yaml',
+ handlerFactory,
+}));
+```
+
+### Key Prefix for Namespacing
+
+Use `keyPrefix` to namespace catalog routes, avoiding collisions when loading multiple catalogs:
+
+```typescript
+const agent = await createAgent({
+ name: 'multi-catalog-agent',
+ version: '1.0.0',
+})
+ .use(http())
+ .use(catalog({ file: './nlp-services.yaml', keyPrefix: 'nlp/' }))
+ .use(catalog({ file: './vision-services.csv', keyPrefix: 'vision/' }))
+ .build();
+```
+
+This produces routes like `nlp/sentiment`, `nlp/translate`, `vision/classify`, etc.
+
+## Network Support
+
+Network identifiers follow the [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) format. Common values:
+
+| Network | CAIP-2 ID |
+|---------|-----------|
+| Base | `eip155:8453` |
+| Base Sepolia | `eip155:84532` |
+| Ethereum | `eip155:1` |
+| Solana Mainnet | `solana:mainnet` |
+| Solana Devnet | `solana:devnet` |
+
+Set a default network for all items via the `network` option, or override per item in your catalog file.
+
+## Related Packages
+
+| Package | Description |
+|---------|-------------|
+| [`@lucid-agents/core`](../core) | Core runtime with extension system and `createAgent()` |
+| [`@lucid-agents/http`](../http) | HTTP extension for request handling |
+| [`@lucid-agents/payments`](../payments) | x402 payment tracking and policy enforcement |
+| [`@lucid-agents/mpp`](../mpp) | Machine Payments Protocol integration |
+| [`@lucid-agents/hono`](../hono) | Hono adapter for serving agents over HTTP |
diff --git a/packages/catalog/package.json b/packages/catalog/package.json
new file mode 100644
index 00000000..405edfaa
--- /dev/null
+++ b/packages/catalog/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@lucid-agents/catalog",
+ "version": "0.1.0",
+ "description": "YAML/CSV catalog-driven route generation for Lucid Agents",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/daydreamsai/lucid-agents",
+ "directory": "packages/catalog"
+ },
+ "type": "module",
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": ["dist"],
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "build": "tsup",
+ "clean": "rm -rf dist",
+ "type-check": "tsc -p tsconfig.json --noEmit",
+ "test": "bun test"
+ },
+ "dependencies": {
+ "@lucid-agents/types": "workspace:*",
+ "yaml": "^2.7.1",
+ "csv-parse": "^5.6.0",
+ "zod": "catalog:"
+ },
+ "devDependencies": {
+ "tsup": "catalog:",
+ "typescript": "catalog:",
+ "@lucid-agents/core": "workspace:*",
+ "@lucid-agents/http": "workspace:*"
+ }
+}
diff --git a/packages/catalog/src/__tests__/catalog.test.ts b/packages/catalog/src/__tests__/catalog.test.ts
new file mode 100644
index 00000000..6721c976
--- /dev/null
+++ b/packages/catalog/src/__tests__/catalog.test.ts
@@ -0,0 +1,471 @@
+import { describe, expect, it, beforeEach, afterEach } from 'bun:test';
+import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
+import { join } from 'path';
+import { tmpdir } from 'os';
+import { z } from 'zod';
+
+// Types we'll implement
+import {
+ parseCatalogYaml,
+ parseCatalogCsv,
+ type CatalogItem,
+ type CatalogConfig,
+ CatalogItemSchema,
+ generateEntrypoints,
+ catalog,
+} from '../index';
+
+describe('CatalogItemSchema', () => {
+ it('validates a complete catalog item', () => {
+ const item = {
+ key: 'product-1',
+ name: 'Premium API Call',
+ description: 'Get premium data',
+ price: '0.50',
+ };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(true);
+ });
+
+ it('validates item with invoke/stream pricing', () => {
+ const item = {
+ key: 'product-2',
+ name: 'Streaming API',
+ description: 'Stream data',
+ price: { invoke: '1.00', stream: '0.10' },
+ };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(true);
+ });
+
+ it('validates item with metadata', () => {
+ const item = {
+ key: 'product-3',
+ name: 'Custom Product',
+ description: 'A product',
+ price: '2.00',
+ metadata: { category: 'ai', tier: 'premium' },
+ };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(true);
+ });
+
+ it('rejects item without key', () => {
+ const item = { name: 'No Key', price: '1.00' };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(false);
+ });
+
+ it('rejects item without name', () => {
+ const item = { key: 'no-name', price: '1.00' };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(false);
+ });
+
+ it('allows item without price (free)', () => {
+ const item = { key: 'free-item', name: 'Free Thing', description: 'Free' };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(true);
+ });
+
+ it('validates item with network field', () => {
+ const item = {
+ key: 'net-item',
+ name: 'Network Product',
+ price: '1.00',
+ network: 'eip155:84532',
+ };
+ const result = CatalogItemSchema.safeParse(item);
+ expect(result.success).toBe(true);
+ });
+});
+
+describe('parseCatalogYaml', () => {
+ it('parses a simple YAML catalog', () => {
+ const yamlContent = `
+products:
+ - key: widget-a
+ name: Widget A
+ description: A fine widget
+ price: "1.00"
+ - key: widget-b
+ name: Widget B
+ description: Another widget
+ price: "2.50"
+`;
+ const items = parseCatalogYaml(yamlContent);
+ expect(items).toHaveLength(2);
+ expect(items[0].key).toBe('widget-a');
+ expect(items[0].name).toBe('Widget A');
+ expect(items[0].price).toBe('1.00');
+ expect(items[1].key).toBe('widget-b');
+ expect(items[1].price).toBe('2.50');
+ });
+
+ it('parses YAML with invoke/stream pricing', () => {
+ const yamlContent = `
+products:
+ - key: streaming-api
+ name: Streaming API
+ description: Streamed data
+ price:
+ invoke: "5.00"
+ stream: "0.50"
+`;
+ const items = parseCatalogYaml(yamlContent);
+ expect(items).toHaveLength(1);
+ expect(items[0].price).toEqual({ invoke: '5.00', stream: '0.50' });
+ });
+
+ it('parses YAML with metadata', () => {
+ const yamlContent = `
+products:
+ - key: premium
+ name: Premium
+ description: Premium tier
+ price: "10.00"
+ metadata:
+ tier: premium
+ rateLimit: 1000
+`;
+ const items = parseCatalogYaml(yamlContent);
+ expect(items[0].metadata).toEqual({ tier: 'premium', rateLimit: 1000 });
+ });
+
+ it('parses YAML with network field', () => {
+ const yamlContent = `
+products:
+ - key: solana-product
+ name: Solana Product
+ price: "1.00"
+ network: solana:devnet
+`;
+ const items = parseCatalogYaml(yamlContent);
+ expect(items[0].network).toBe('solana:devnet');
+ });
+
+ it('supports flat array format (no products wrapper)', () => {
+ const yamlContent = `
+- key: item-1
+ name: Item 1
+ price: "1.00"
+- key: item-2
+ name: Item 2
+ price: "2.00"
+`;
+ const items = parseCatalogYaml(yamlContent);
+ expect(items).toHaveLength(2);
+ });
+
+ it('throws on invalid YAML structure', () => {
+ expect(() => parseCatalogYaml('not: valid: yaml: [')).toThrow();
+ });
+
+ it('throws on items missing required fields', () => {
+ const yamlContent = `
+products:
+ - description: no key or name
+ price: "1.00"
+`;
+ expect(() => parseCatalogYaml(yamlContent)).toThrow();
+ });
+});
+
+describe('parseCatalogCsv', () => {
+ it('parses a simple CSV catalog', () => {
+ const csvContent = `key,name,description,price
+widget-a,Widget A,A fine widget,1.00
+widget-b,Widget B,Another widget,2.50`;
+ const items = parseCatalogCsv(csvContent);
+ expect(items).toHaveLength(2);
+ expect(items[0].key).toBe('widget-a');
+ expect(items[0].name).toBe('Widget A');
+ expect(items[0].price).toBe('1.00');
+ });
+
+ it('handles quoted fields with commas', () => {
+ const csvContent = `key,name,description,price
+api-1,"Advanced API","AI-powered, real-time data",3.00`;
+ const items = parseCatalogCsv(csvContent);
+ expect(items[0].description).toBe('AI-powered, real-time data');
+ });
+
+ it('handles empty price as free', () => {
+ const csvContent = `key,name,description,price
+free-item,Free Item,No cost,`;
+ const items = parseCatalogCsv(csvContent);
+ expect(items[0].price).toBeUndefined();
+ });
+
+ it('parses CSV with network column', () => {
+ const csvContent = `key,name,description,price,network
+sol-item,Sol Item,On Solana,1.00,solana:devnet`;
+ const items = parseCatalogCsv(csvContent);
+ expect(items[0].network).toBe('solana:devnet');
+ });
+
+ it('parses CSV with metadata columns (prefixed with meta_)', () => {
+ const csvContent = `key,name,description,price,meta_category,meta_tier
+premium,Premium,Top tier,10.00,ai,premium`;
+ const items = parseCatalogCsv(csvContent);
+ expect(items[0].metadata).toEqual({ category: 'ai', tier: 'premium' });
+ });
+
+ it('throws on CSV missing key column', () => {
+ const csvContent = `name,description,price
+Widget,A widget,1.00`;
+ expect(() => parseCatalogCsv(csvContent)).toThrow();
+ });
+});
+
+describe('generateEntrypoints', () => {
+ const sampleItems: CatalogItem[] = [
+ { key: 'widget-a', name: 'Widget A', description: 'A widget', price: '1.00' },
+ { key: 'widget-b', name: 'Widget B', description: 'B widget', price: '2.50' },
+ ];
+
+ it('generates one entrypoint per catalog item', () => {
+ const entrypoints = generateEntrypoints(sampleItems);
+ expect(entrypoints).toHaveLength(2);
+ expect(entrypoints[0].key).toBe('widget-a');
+ expect(entrypoints[1].key).toBe('widget-b');
+ });
+
+ it('sets price on entrypoints', () => {
+ const entrypoints = generateEntrypoints(sampleItems);
+ expect(entrypoints[0].price).toBe('1.00');
+ expect(entrypoints[1].price).toBe('2.50');
+ });
+
+ it('sets description from catalog item', () => {
+ const entrypoints = generateEntrypoints(sampleItems);
+ expect(entrypoints[0].description).toBe('A widget');
+ });
+
+ it('generates entrypoints with invoke/stream pricing', () => {
+ const items: CatalogItem[] = [
+ { key: 'stream-api', name: 'Stream', price: { invoke: '5.00', stream: '0.50' } },
+ ];
+ const entrypoints = generateEntrypoints(items);
+ expect(entrypoints[0].price).toEqual({ invoke: '5.00', stream: '0.50' });
+ });
+
+ it('generates entrypoints without price for free items', () => {
+ const items: CatalogItem[] = [
+ { key: 'free-item', name: 'Free' },
+ ];
+ const entrypoints = generateEntrypoints(items);
+ expect(entrypoints[0].price).toBeUndefined();
+ });
+
+ it('passes metadata through to entrypoint', () => {
+ const items: CatalogItem[] = [
+ { key: 'meta-item', name: 'Meta', price: '1.00', metadata: { tier: 'gold' } },
+ ];
+ const entrypoints = generateEntrypoints(items);
+ expect(entrypoints[0].metadata).toEqual({ tier: 'gold', catalogItem: items[0] });
+ });
+
+ it('applies custom handler factory when provided', () => {
+ const handlerFactory = (item: CatalogItem) => {
+ return async (ctx: any) => ({ output: { product: item.key } });
+ };
+ const entrypoints = generateEntrypoints(sampleItems, { handlerFactory });
+ expect(entrypoints[0].handler).toBeDefined();
+ });
+
+ it('applies key prefix when provided', () => {
+ const entrypoints = generateEntrypoints(sampleItems, { keyPrefix: 'shop/' });
+ expect(entrypoints[0].key).toBe('shop/widget-a');
+ expect(entrypoints[1].key).toBe('shop/widget-b');
+ });
+
+ it('applies network override to all entrypoints', () => {
+ const entrypoints = generateEntrypoints(sampleItems, { network: 'eip155:84532' });
+ expect(entrypoints[0].network).toBe('eip155:84532');
+ expect(entrypoints[1].network).toBe('eip155:84532');
+ });
+
+ it('item-level network overrides global network', () => {
+ const items: CatalogItem[] = [
+ { key: 'item', name: 'Item', price: '1.00', network: 'solana:devnet' },
+ ];
+ const entrypoints = generateEntrypoints(items, { network: 'eip155:84532' });
+ expect(entrypoints[0].network).toBe('solana:devnet');
+ });
+
+ it('sets default input schema with product key', () => {
+ const entrypoints = generateEntrypoints(sampleItems);
+ // Should have a basic input schema
+ expect(entrypoints[0].input).toBeDefined();
+ });
+
+ it('applies custom input schema when provided', () => {
+ const customInput = z.object({ quantity: z.number() });
+ const entrypoints = generateEntrypoints(sampleItems, { inputSchema: customInput });
+ expect(entrypoints[0].input).toBe(customInput);
+ });
+});
+
+describe('catalog extension', () => {
+ let tmpDir: string;
+
+ beforeEach(() => {
+ tmpDir = join(tmpdir(), `catalog-test-${Date.now()}`);
+ mkdirSync(tmpDir, { recursive: true });
+ });
+
+ afterEach(() => {
+ if (existsSync(tmpDir)) {
+ rmSync(tmpDir, { recursive: true, force: true });
+ }
+ });
+
+ it('creates a valid extension object', () => {
+ const yamlPath = join(tmpDir, 'products.yaml');
+ writeFileSync(yamlPath, `
+products:
+ - key: test
+ name: Test
+ price: "1.00"
+`);
+ const ext = catalog({ file: yamlPath });
+ expect(ext.name).toBe('catalog');
+ expect(ext.build).toBeDefined();
+ });
+
+ it('build returns catalog runtime with items', () => {
+ const yamlPath = join(tmpDir, 'products.yaml');
+ writeFileSync(yamlPath, `
+products:
+ - key: test-product
+ name: Test Product
+ price: "1.00"
+ - key: another
+ name: Another
+ price: "2.00"
+`);
+ const ext = catalog({ file: yamlPath });
+ const result = ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} });
+ expect(result.catalog).toBeDefined();
+ expect(result.catalog!.items).toHaveLength(2);
+ expect(result.catalog!.items[0].key).toBe('test-product');
+ });
+
+ it('onBuild registers entrypoints from catalog', async () => {
+ const yamlPath = join(tmpDir, 'products.yaml');
+ writeFileSync(yamlPath, `
+products:
+ - key: alpha
+ name: Alpha
+ description: Alpha product
+ price: "1.00"
+ - key: beta
+ name: Beta
+ description: Beta product
+ price: "2.00"
+`);
+ const ext = catalog({ file: yamlPath });
+ ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} });
+
+ // Simulate runtime with entrypoints.add
+ const added: any[] = [];
+ const mockRuntime = {
+ entrypoints: {
+ add: (def: any) => added.push(def),
+ list: () => [],
+ snapshot: () => [],
+ },
+ } as any;
+
+ await ext.onBuild!(mockRuntime);
+ expect(added).toHaveLength(2);
+ expect(added[0].key).toBe('alpha');
+ expect(added[0].price).toBe('1.00');
+ expect(added[1].key).toBe('beta');
+ });
+
+ it('loads CSV files and populates catalog.items', async () => {
+ const csvPath = join(tmpDir, 'products.csv');
+ writeFileSync(csvPath, `key,name,description,price
+item-1,Item 1,First item,1.00
+item-2,Item 2,Second item,2.00`);
+ const ext = catalog({ file: csvPath });
+ const result = ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} });
+
+ // CSV is now parsed synchronously — catalog.items should be populated immediately
+ expect(result.catalog!.items).toHaveLength(2);
+ expect(result.catalog!.items[0].key).toBe('item-1');
+
+ const added: any[] = [];
+ const mockRuntime = {
+ entrypoints: {
+ add: (def: any) => added.push(def),
+ list: () => [],
+ snapshot: () => [],
+ },
+ } as any;
+
+ await ext.onBuild!(mockRuntime);
+ expect(added).toHaveLength(2);
+ expect(added[0].key).toBe('item-1');
+ });
+
+ it('applies handlerFactory from options', async () => {
+ const yamlPath = join(tmpDir, 'products.yaml');
+ writeFileSync(yamlPath, `
+products:
+ - key: test
+ name: Test
+ price: "1.00"
+`);
+ const handlerFactory = (item: CatalogItem) => {
+ return async () => ({ output: { product: item.key, price: item.price } });
+ };
+ const ext = catalog({ file: yamlPath, handlerFactory });
+ ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} });
+
+ const added: any[] = [];
+ const mockRuntime = {
+ entrypoints: {
+ add: (def: any) => added.push(def),
+ list: () => [],
+ snapshot: () => [],
+ },
+ } as any;
+
+ await ext.onBuild!(mockRuntime);
+ expect(added[0].handler).toBeDefined();
+ const result = await added[0].handler({});
+ expect(result.output.product).toBe('test');
+ });
+
+ it('applies keyPrefix from options', async () => {
+ const yamlPath = join(tmpDir, 'products.yaml');
+ writeFileSync(yamlPath, `
+products:
+ - key: test
+ name: Test
+ price: "1.00"
+`);
+ const ext = catalog({ file: yamlPath, keyPrefix: 'store/' });
+ ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} });
+
+ const added: any[] = [];
+ const mockRuntime = {
+ entrypoints: {
+ add: (def: any) => added.push(def),
+ list: () => [],
+ snapshot: () => [],
+ },
+ } as any;
+
+ await ext.onBuild!(mockRuntime);
+ expect(added[0].key).toBe('store/test');
+ });
+
+ it('throws if file does not exist', () => {
+ const ext = catalog({ file: '/nonexistent/path.yaml' });
+ expect(() => ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} })).toThrow();
+ });
+});
diff --git a/packages/catalog/src/entrypoints.ts b/packages/catalog/src/entrypoints.ts
new file mode 100644
index 00000000..a5666af4
--- /dev/null
+++ b/packages/catalog/src/entrypoints.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod';
+import type { EntrypointDef } from '@lucid-agents/types/core';
+import type { CatalogItem, HandlerFactory } from './types';
+
+export type GenerateOptions = {
+ keyPrefix?: string;
+ network?: string;
+ handlerFactory?: HandlerFactory;
+ inputSchema?: z.ZodTypeAny;
+};
+
+const defaultInputSchema = z.object({
+ params: z.record(z.string(), z.unknown()).optional(),
+});
+
+export function generateEntrypoints(
+ items: CatalogItem[],
+ options?: GenerateOptions,
+): EntrypointDef[] {
+ const { keyPrefix, network, handlerFactory, inputSchema } = options ?? {};
+
+ return items.map((item): EntrypointDef => {
+ const key = keyPrefix ? `${keyPrefix}${item.key}` : item.key;
+ const entrypointNetwork = item.network ?? network;
+
+ const metadata = {
+ ...item.metadata,
+ catalogItem: item,
+ };
+
+ const entrypoint: EntrypointDef = {
+ key,
+ description: item.description,
+ price: item.price as EntrypointDef['price'],
+ input: inputSchema ?? defaultInputSchema,
+ metadata,
+ ...(entrypointNetwork ? { network: entrypointNetwork as EntrypointDef['network'] } : {}),
+ };
+
+ if (handlerFactory) {
+ entrypoint.handler = handlerFactory(item) as EntrypointDef['handler'];
+ }
+
+ return entrypoint;
+ });
+}
diff --git a/packages/catalog/src/extension.ts b/packages/catalog/src/extension.ts
new file mode 100644
index 00000000..fd8b011d
--- /dev/null
+++ b/packages/catalog/src/extension.ts
@@ -0,0 +1,56 @@
+import { readFileSync } from 'fs';
+import { extname } from 'path';
+import type {
+ Extension,
+ BuildContext,
+ AgentRuntime,
+} from '@lucid-agents/types/core';
+import type { CatalogItem, CatalogExtensionOptions } from './types';
+import { parseCatalogYaml, parseCatalogCsv } from './parser';
+import { generateEntrypoints } from './entrypoints';
+
+export type CatalogRuntime = {
+ items: CatalogItem[];
+};
+
+export function catalog(
+ options: CatalogExtensionOptions,
+): Extension<{ catalog?: CatalogRuntime }> {
+ let catalogItems: CatalogItem[] = [];
+
+ return {
+ name: 'catalog',
+ build(ctx: BuildContext): { catalog?: CatalogRuntime } {
+ const ext = extname(options.file).toLowerCase();
+ const content = readFileSync(options.file, 'utf-8');
+
+ if (ext === '.yaml' || ext === '.yml') {
+ catalogItems = parseCatalogYaml(content);
+ } else if (ext === '.csv') {
+ catalogItems = parseCatalogCsv(content);
+ } else {
+ throw new Error(
+ `Unsupported catalog file format: ${ext}. Use .yaml, .yml, or .csv`,
+ );
+ }
+
+ return {
+ catalog: {
+ items: catalogItems,
+ },
+ };
+ },
+ async onBuild(runtime: AgentRuntime): Promise {
+ const entrypoints = generateEntrypoints(catalogItems, {
+ keyPrefix: options.keyPrefix,
+ network: options.network,
+ handlerFactory: options.handlerFactory,
+ inputSchema: options.inputSchema,
+ });
+
+ for (const ep of entrypoints) {
+ runtime.entrypoints.add(ep);
+ }
+ },
+ };
+}
diff --git a/packages/catalog/src/index.ts b/packages/catalog/src/index.ts
new file mode 100644
index 00000000..26da9778
--- /dev/null
+++ b/packages/catalog/src/index.ts
@@ -0,0 +1,9 @@
+export { parseCatalogYaml, parseCatalogCsv } from './parser';
+export { generateEntrypoints } from './entrypoints';
+export { catalog } from './extension';
+export {
+ CatalogItemSchema,
+ type CatalogItem,
+ type CatalogConfig,
+ type CatalogExtensionOptions,
+} from './types';
diff --git a/packages/catalog/src/parser.ts b/packages/catalog/src/parser.ts
new file mode 100644
index 00000000..027b81ca
--- /dev/null
+++ b/packages/catalog/src/parser.ts
@@ -0,0 +1,85 @@
+import YAML from 'yaml';
+import { parse as csvParse } from 'csv-parse/sync';
+import { CatalogItemSchema, type CatalogItem } from './types';
+
+export function parseCatalogYaml(content: string): CatalogItem[] {
+ const parsed = YAML.parse(content);
+
+ let rawItems: unknown[];
+
+ if (Array.isArray(parsed)) {
+ rawItems = parsed;
+ } else if (
+ parsed &&
+ typeof parsed === 'object' &&
+ 'products' in parsed &&
+ Array.isArray(parsed.products)
+ ) {
+ rawItems = parsed.products;
+ } else {
+ throw new Error(
+ 'YAML must contain a "products" array or be a top-level array',
+ );
+ }
+
+ const items: CatalogItem[] = [];
+ for (const raw of rawItems) {
+ const result = CatalogItemSchema.safeParse(raw);
+ if (!result.success) {
+ throw new Error(`Invalid catalog item: ${result.error.message}`);
+ }
+ items.push(result.data);
+ }
+
+ return items;
+}
+
+export function parseCatalogCsv(
+ content: string,
+): CatalogItem[] {
+ const records = csvParse(content, {
+ columns: true,
+ skip_empty_lines: true,
+ trim: true,
+ }) as Record[];
+
+ // Validate key column exists
+ if (records.length > 0 && !('key' in records[0])) {
+ throw new Error('CSV must have a "key" column');
+ }
+
+ const items: CatalogItem[] = [];
+ for (const record of records) {
+ const metadata: Record = {};
+ let hasMetadata = false;
+
+ for (const [col, val] of Object.entries(record)) {
+ if (col.startsWith('meta_')) {
+ metadata[col.slice(5)] = val;
+ hasMetadata = true;
+ }
+ }
+
+ const item: CatalogItem = {
+ key: record.key,
+ name: record.name,
+ description: record.description || undefined,
+ price:
+ record.price && record.price.trim() !== ''
+ ? record.price
+ : undefined,
+ network: record.network || undefined,
+ ...(hasMetadata ? { metadata } : {}),
+ };
+
+ const result = CatalogItemSchema.safeParse(item);
+ if (!result.success) {
+ throw new Error(
+ `Invalid CSV row for key "${record.key}": ${result.error.message}`,
+ );
+ }
+ items.push(result.data);
+ }
+
+ return items;
+}
diff --git a/packages/catalog/src/types.ts b/packages/catalog/src/types.ts
new file mode 100644
index 00000000..e8aabbee
--- /dev/null
+++ b/packages/catalog/src/types.ts
@@ -0,0 +1,34 @@
+import { z } from 'zod';
+
+export const CatalogItemSchema = z.object({
+ key: z.string(),
+ name: z.string(),
+ description: z.string().optional(),
+ price: z
+ .union([
+ z.string(),
+ z.object({
+ invoke: z.string().optional(),
+ stream: z.string().optional(),
+ }),
+ ])
+ .optional(),
+ network: z.string().optional(),
+ metadata: z.record(z.string(), z.unknown()).optional(),
+});
+
+export type CatalogItem = z.infer;
+
+export type CatalogConfig = {
+ items: CatalogItem[];
+};
+
+export type HandlerFactory = (item: CatalogItem) => (...args: any[]) => any;
+
+export type CatalogExtensionOptions = {
+ file: string;
+ keyPrefix?: string;
+ network?: string;
+ handlerFactory?: HandlerFactory;
+ inputSchema?: z.ZodTypeAny;
+};
diff --git a/packages/catalog/tsconfig.build.json b/packages/catalog/tsconfig.build.json
new file mode 100644
index 00000000..39b29485
--- /dev/null
+++ b/packages/catalog/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../tsconfig.build.base.json",
+ "include": ["src/**/*"],
+ "exclude": ["dist", "node_modules", "**/*.test.ts", "**/__tests__/**"]
+}
diff --git a/packages/catalog/tsconfig.json b/packages/catalog/tsconfig.json
new file mode 100644
index 00000000..a6098995
--- /dev/null
+++ b/packages/catalog/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "composite": true
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/catalog/tsup.config.ts b/packages/catalog/tsup.config.ts
new file mode 100644
index 00000000..92f8226b
--- /dev/null
+++ b/packages/catalog/tsup.config.ts
@@ -0,0 +1,13 @@
+import { definePackageConfig } from '../tsup.config.base';
+
+export default definePackageConfig({
+ entry: ['src/index.ts'],
+ dts: true,
+ external: [
+ '@lucid-agents/core',
+ '@lucid-agents/types',
+ 'yaml',
+ 'csv-parse',
+ 'zod',
+ ],
+});
diff --git a/packages/examples/package.json b/packages/examples/package.json
index 942c26f7..1825ab49 100644
--- a/packages/examples/package.json
+++ b/packages/examples/package.json
@@ -18,6 +18,7 @@
"@lucid-agents/a2a": "workspace:*",
"@lucid-agents/analytics": "workspace:*",
"@lucid-agents/ap2": "workspace:*",
+ "@lucid-agents/catalog": "workspace:*",
"@lucid-agents/core": "workspace:*",
"@lucid-agents/hono": "workspace:*",
"@lucid-agents/http": "workspace:*",
diff --git a/packages/examples/src/catalog/catalog-mpp-store.ts b/packages/examples/src/catalog/catalog-mpp-store.ts
new file mode 100644
index 00000000..adb04d6f
--- /dev/null
+++ b/packages/examples/src/catalog/catalog-mpp-store.ts
@@ -0,0 +1,129 @@
+import { catalog, type CatalogItem } from '@lucid-agents/catalog';
+import { createAgent } from '@lucid-agents/core';
+import { createAgentApp } from '@lucid-agents/hono';
+import { http } from '@lucid-agents/http';
+import { mpp, tempo } from '@lucid-agents/mpp';
+import { join } from 'path';
+
+/**
+ * Catalog + MPP Store Agent
+ *
+ * Demonstrates how to generate hundreds of paid entrypoint routes from a single
+ * YAML catalog file, with payments enforced via Machine Payments Protocol (MPP).
+ *
+ * Instead of writing 10+ addEntrypoint() calls manually, the catalog extension
+ * reads products.yaml and auto-registers each product as a paid route.
+ *
+ * Run: bun run packages/examples/src/catalog/catalog-mpp-store.ts
+ *
+ * Environment variables:
+ * MPP_TEMPO_CURRENCY - Token address (default: pathUSD contract)
+ * MPP_TEMPO_RECIPIENT - Recipient wallet address (default: dev wallet)
+ * PORT - Server port (default: 3000)
+ */
+
+// ─── Handler Factory ─────────────────────────────────────────────
+// Each catalog item gets its own handler via this factory.
+// In production, you'd route to actual AI models or services.
+
+const handlerFactory = (item: CatalogItem) => {
+ return async (ctx: { input: { params?: Record } }) => {
+ return {
+ output: {
+ product: item.key,
+ name: item.name,
+ description: item.description ?? 'No description',
+ price: item.price ?? 'free',
+ metadata: item.metadata ?? {},
+ params: ctx.input.params ?? {},
+ timestamp: new Date().toISOString(),
+ },
+ };
+ };
+};
+
+// ─── Agent Setup ─────────────────────────────────────────────────
+
+const catalogFile = join(import.meta.dir, 'products.yaml');
+
+const agent = await createAgent({
+ name: 'catalog-mpp-store',
+ version: '1.0.0',
+ description: 'AI service marketplace powered by YAML catalog + MPP payments',
+})
+ .use(http())
+ .use(
+ mpp({
+ config: {
+ methods: [
+ tempo.server({
+ currency:
+ process.env.MPP_TEMPO_CURRENCY ??
+ '0x20c0000000000000000000000000000000000000', // pathUSD
+ recipient:
+ process.env.MPP_TEMPO_RECIPIENT ??
+ '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // dev wallet
+ }),
+ ],
+ currency: 'usd',
+ defaultIntent: 'charge',
+ },
+ })
+ )
+ .use(
+ catalog({
+ file: catalogFile,
+ keyPrefix: 'store/',
+ handlerFactory,
+ })
+ )
+ .build();
+
+// ─── Access Catalog at Runtime ───────────────────────────────────
+// The catalog runtime exposes parsed items for introspection.
+
+const items: CatalogItem[] =
+ (agent as unknown as { catalog?: { items: CatalogItem[] } }).catalog?.items ??
+ [];
+console.log(`\nLoaded ${items.length} products from catalog\n`);
+
+// ─── Create Hono App ─────────────────────────────────────────────
+
+const { app } = await createAgentApp(agent);
+
+// ─── Start Server ────────────────────────────────────────────────
+
+const port = Number(process.env.PORT ?? 3000);
+
+const server = Bun.serve({
+ port,
+ fetch: app.fetch,
+});
+
+console.log(
+ `Catalog MPP Store ready at http://${server.hostname}:${server.port}\n`
+);
+console.log('Endpoints:');
+console.log(' GET / -> Landing page');
+console.log(' GET /.well-known/agent.json -> Agent manifest');
+console.log('');
+
+// List all catalog-generated routes with prices
+for (const item of items) {
+ const key = `store/${item.key}`;
+ const price =
+ typeof item.price === 'object'
+ ? `invoke: $${item.price.invoke ?? '0'}, stream: $${item.price.stream ?? '0'}`
+ : item.price
+ ? `$${item.price}`
+ : 'free';
+ const tier = item.metadata?.tier ?? '-';
+ console.log(
+ ` POST /entrypoints/${key}/invoke -> ${item.name} (${price}) [${tier}]`
+ );
+}
+
+console.log(
+ '\nPayment: Machine Payments Protocol (HTTP 402 + WWW-Authenticate)'
+);
+console.log('Methods: tempo (stablecoin)\n');
diff --git a/packages/examples/src/catalog/products.csv b/packages/examples/src/catalog/products.csv
new file mode 100644
index 00000000..5f7d12cf
--- /dev/null
+++ b/packages/examples/src/catalog/products.csv
@@ -0,0 +1,6 @@
+key,name,description,price,network,meta_tier,meta_category,meta_rateLimit
+ocr,OCR Scan,Extract text from images using OCR,0.50,,standard,vision,200
+embed,Text Embedding,Generate vector embeddings for text,0.02,,basic,ml,2000
+classify,Text Classifier,Classify text into categories,0.10,,basic,nlp,500
+rag-query,RAG Query,Retrieval-augmented generation query,1.00,,premium,search,100
+sol-verify,Solana Verify,Verify Solana transaction signatures,0.05,solana:devnet,standard,blockchain,1000
diff --git a/packages/examples/src/catalog/products.yaml b/packages/examples/src/catalog/products.yaml
new file mode 100644
index 00000000..32fdcb2a
--- /dev/null
+++ b/packages/examples/src/catalog/products.yaml
@@ -0,0 +1,106 @@
+# AI Service Product Catalog
+#
+# Each product becomes an entrypoint route on the agent.
+# Prices are in USD. Payment is enforced via MPP (HTTP 402).
+#
+# Supported fields:
+# key - unique route identifier (required)
+# name - display name (required)
+# description - human-readable description
+# price - flat string ("1.00") or { invoke, stream } object
+# network - CAIP-2 network identifier
+# metadata - arbitrary key-value pairs
+
+products:
+ # ── Free tier ────────────────────────────────────────────────────
+ - key: health
+ name: Health Check
+ description: Free health check endpoint
+ metadata:
+ tier: free
+ category: system
+
+ # ── Basic tier ($0.01 - $0.10) ──────────────────────────────────
+ - key: text-stats
+ name: Text Statistics
+ description: Word count, character count, and reading time estimate
+ price: '0.01'
+ metadata:
+ tier: basic
+ category: text
+ rateLimit: 1000
+
+ - key: sentiment
+ name: Sentiment Analysis
+ description: Analyze text sentiment (positive, negative, neutral)
+ price: '0.05'
+ metadata:
+ tier: basic
+ category: nlp
+ rateLimit: 500
+
+ - key: translate
+ name: Quick Translate
+ description: Translate short text between languages
+ price: '0.10'
+ metadata:
+ tier: basic
+ category: nlp
+ rateLimit: 200
+
+ # ── Standard tier ($0.25 - $1.00) ──────────────────────────────
+ - key: summarize
+ name: Text Summarizer
+ description: Summarize long-form content into key points
+ price:
+ invoke: '0.50'
+ stream: '0.10'
+ metadata:
+ tier: standard
+ category: text
+
+ - key: code-review
+ name: Code Review
+ description: Automated code review with suggestions
+ price: '1.00'
+ metadata:
+ tier: standard
+ category: code
+ rateLimit: 100
+
+ # ── Premium tier ($2.00 - $5.00) ────────────────────────────────
+ - key: image-describe
+ name: Image Description
+ description: Generate detailed descriptions of images
+ price: '2.00'
+ metadata:
+ tier: premium
+ category: vision
+
+ - key: document-extract
+ name: Document Extraction
+ description: Extract structured data from PDFs and documents
+ price: '3.00'
+ metadata:
+ tier: premium
+ category: extraction
+
+ - key: generate-report
+ name: Report Generator
+ description: Generate comprehensive analysis reports
+ price:
+ invoke: '5.00'
+ stream: '1.00'
+ metadata:
+ tier: premium
+ category: analysis
+
+ # ── Cross-chain product ─────────────────────────────────────────
+ - key: solana-nft-metadata
+ name: Solana NFT Metadata
+ description: Fetch and analyze Solana NFT metadata
+ price: '0.25'
+ network: 'solana:devnet'
+ metadata:
+ tier: standard
+ category: blockchain