@agentcash/router 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +86 -0
- package/.claude/skills/router-guide/SKILL.md +533 -0
- package/dist/index.cjs +108 -60
- package/dist/index.d.cts +11 -6
- package/dist/index.d.ts +11 -6
- package/dist/index.js +93 -45
- package/package.json +18 -19
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# CLAUDE.md — @agentcash/router
|
|
2
|
+
|
|
3
|
+
Protocol-agnostic route framework for Next.js App Router APIs with x402 payment, MPP payment, SIWX identity auth, and API key auth. Used by stablestudio, enrichx402, x402email, agentfacilitator, agentupload.
|
|
4
|
+
|
|
5
|
+
## Guiding Principles
|
|
6
|
+
|
|
7
|
+
1. **Route definition is 3-6 lines.** Everything else is derived.
|
|
8
|
+
2. **Single source of truth.** Route registry drives discovery, OpenAPI, pricing, Bazaar schemas.
|
|
9
|
+
3. **Auth and pricing are orthogonal and composable.** `.paid()`, `.siwx()`, `.apiKey()`, `.unprotected()`.
|
|
10
|
+
4. **Observability is pluggable via RouterPlugin.** Zero boilerplate.
|
|
11
|
+
5. **The package owns the x402 server lifecycle.** Init, verify, settle.
|
|
12
|
+
6. **Convention over configuration.** Sane defaults for Base, USDC, exact scheme.
|
|
13
|
+
7. **Compose, don't reimplement.** Zero payment/auth protocol logic — delegates to `@x402/*` and `@coinbase/x402`.
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
**Orchestrate pipeline:** auth check -> body parse -> price resolve -> payment verify -> handler invoke -> settle -> finalize
|
|
18
|
+
|
|
19
|
+
- `src/orchestrate.ts` — Full request lifecycle orchestration
|
|
20
|
+
- `src/builder.ts` — Fluent RouteBuilder API
|
|
21
|
+
- `src/handler.ts` — Safe handler invocation with error mapping
|
|
22
|
+
- `src/types.ts` — Core types (RouteEntry, HandlerContext, HttpError)
|
|
23
|
+
- `src/registry.ts` — Route registry (Map-backed, silent overwrite on duplicate keys)
|
|
24
|
+
- `src/pricing.ts` — Price resolution (static, tiered, dynamic)
|
|
25
|
+
- `src/plugin.ts` — Plugin hook system
|
|
26
|
+
- `src/server.ts` — x402 server initialization
|
|
27
|
+
- `src/auth/` — Auth modules (siwx.ts, api-key.ts, nonce.ts)
|
|
28
|
+
- `src/protocols/` — Protocol handlers (x402.ts, mpp.ts, detect.ts)
|
|
29
|
+
- `src/discovery/` — Auto-generated endpoints (well-known.ts, openapi.ts)
|
|
30
|
+
|
|
31
|
+
## Critical Rules
|
|
32
|
+
|
|
33
|
+
- **Error handling:** Respect `.status` on any thrown error, not just `HttpError`. The `Object.assign(new Error(), { status })` pattern is universal in Node.js.
|
|
34
|
+
- **SIWX challenge:** Must return a proper x402v2 challenge with `PAYMENT-REQUIRED` header and JSON body containing `extensions['sign-in-with-x']` with `domain`, `uri`, `version`, `chainId`, `type`, `nonce`, `issuedAt`.
|
|
35
|
+
- **Discovery:** `authMode !== 'unprotected'` determines well-known visibility, not the protocol list. SIWX routes return 402 challenges and must be discoverable.
|
|
36
|
+
- **OpenAPI:** Merge paths for multi-method endpoints (GET + DELETE on same path). Never overwrite.
|
|
37
|
+
- **Duplicate route keys:** Registry silently overwrites (last-write-wins) with a dev-only `console.warn`. This is intentional — Next.js module loading order is non-deterministic during `next build`, so discovery stubs and real handlers may register the same key in either order. Prior art: ElysiaJS uses the identical pattern. See stablestudio `.claude/13_route-registry-dedup.md` for full research.
|
|
38
|
+
- **Dynamic pricing (v0.3.1+):** Early body parsing with `request.clone()` enables accurate dynamic pricing. `maxPrice` is optional and acts as a safety net (cap + fallback). Body is parsed before 402 challenge generation when pricing function exists. See `.claude/15_router-dynamic-pricing-solution.md` for full design and derisking.
|
|
39
|
+
|
|
40
|
+
## Environment Variables
|
|
41
|
+
|
|
42
|
+
The router uses the default facilitator from `@coinbase/x402`, which requires CDP API keys in `process.env`:
|
|
43
|
+
|
|
44
|
+
- `CDP_API_KEY_ID` — Coinbase Developer Platform API key ID
|
|
45
|
+
- `CDP_API_KEY_SECRET` — CDP API key secret
|
|
46
|
+
|
|
47
|
+
**Critical for Next.js apps with env validation (T3 stack, `@t3-oss/env-nextjs`):** These variables must be explicitly declared in your env schema. Next.js does not automatically expose all env vars to `process.env` — undeclared vars are invisible at runtime.
|
|
48
|
+
|
|
49
|
+
Without these keys, the default facilitator cannot authenticate with CDP:
|
|
50
|
+
- x402 server `initialize()` fails with "Failed to fetch supported kinds from facilitator: TypeError: fetch failed" or "Facilitator getSupported failed (401): Unauthorized"
|
|
51
|
+
- All payment routes return empty 402 responses (no `PAYMENT-REQUIRED` header, no body)
|
|
52
|
+
|
|
53
|
+
**Example env schema (T3/`@t3-oss/env-nextjs`):**
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { createEnv } from "@t3-oss/env-nextjs";
|
|
57
|
+
import { z } from "zod";
|
|
58
|
+
|
|
59
|
+
export const env = createEnv({
|
|
60
|
+
server: {
|
|
61
|
+
CDP_API_KEY_ID: z.string(),
|
|
62
|
+
CDP_API_KEY_SECRET: z.string(),
|
|
63
|
+
// ... other vars
|
|
64
|
+
},
|
|
65
|
+
runtimeEnv: {
|
|
66
|
+
CDP_API_KEY_ID: process.env.CDP_API_KEY_ID,
|
|
67
|
+
CDP_API_KEY_SECRET: process.env.CDP_API_KEY_SECRET,
|
|
68
|
+
// ... other vars
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Alternative:** Pass a custom facilitator config to `createRouter()` if you want to use a different facilitator URL or auth mechanism. But for most apps, the default CDP facilitator is correct.
|
|
74
|
+
|
|
75
|
+
## Version Stability
|
|
76
|
+
|
|
77
|
+
The public API is **not stable**. Downstream consumers should pin exact versions (`"@agentcash/router": "0.2.0"`, not `"^0.2.0"`). Breaking changes will happen as we build out multi-protocol support and discover patterns across services. Semver will be respected once we hit 1.0.
|
|
78
|
+
|
|
79
|
+
## Build & Test
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm build # tsup
|
|
83
|
+
pnpm test # vitest
|
|
84
|
+
pnpm typecheck # tsc --noEmit
|
|
85
|
+
pnpm check # format + lint + typecheck + build + test
|
|
86
|
+
```
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: router-guide
|
|
3
|
+
description: Use when creating, modifying, or debugging routes with @agentcash/router. Covers route creation, auth modes, pricing, provider monitoring, discovery, plugins, and troubleshooting. Trigger phrases include "add a route", "create an endpoint", "set up discovery", "add provider monitoring", "configure the router".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @agentcash/router Guide
|
|
7
|
+
|
|
8
|
+
You are helping a developer build API routes using `@agentcash/router`. This package is a fluent route builder for Next.js App Router that handles x402 payments, MPP payments, SIWX authentication, API key auth, and provider monitoring.
|
|
9
|
+
|
|
10
|
+
## Philosophy and Design Constraints
|
|
11
|
+
|
|
12
|
+
These are intentional decisions. Do not change them without explicit user approval.
|
|
13
|
+
|
|
14
|
+
### Why this exists
|
|
15
|
+
|
|
16
|
+
Every paid API route in a Merit Systems service shared the same ~80-150 lines of boilerplate: x402 server init, payment verification, body parsing, settlement gating, error handling, observability hooks. Five services meant five copies that drifted. This router eliminates that by encoding the lifecycle into a fluent builder that compiles to a single Next.js handler function.
|
|
17
|
+
|
|
18
|
+
### Core design principles
|
|
19
|
+
|
|
20
|
+
1. **Fluent builder, not config object.** Progressive discoverability via IDE autocomplete. The chain reads like a sentence: `route('search').paid('0.01').body(schema).handler(fn)`. Config objects hide available options; fluent chains surface them.
|
|
21
|
+
|
|
22
|
+
2. **Use `x402ResourceServer` primitives directly, not `withX402`.** The `withX402` wrapper from `@coinbase/x402` is a convenience that owns the full request lifecycle. We need to interleave body parsing, Zod validation, plugin hooks, and settlement gating at specific points in that lifecycle, so we call the lower-level primitives (`buildPaymentRequirementsFromOptions`, `verifyPayment`, `settlePayment`) directly.
|
|
23
|
+
|
|
24
|
+
3. **Auth modes are mutually exclusive per route, except `apiKey` + `paid`.** A route is either paid, SIWX-authenticated, API-key-gated, or unprotected. The one exception: `.apiKey()` can compose with `.paid()` because some routes need both identity (API key) and payment (x402/MPP). This is enforced at the type level.
|
|
25
|
+
|
|
26
|
+
4. **Body is only buffered when `.body()` is chained.** If a route doesn't declare a body schema, the request stream is untouched. This matters for routes that proxy multipart uploads or streams. For dynamic pricing, the body IS parsed before the 402 challenge (via `request.clone()`) so the pricing function can calculate an accurate price.
|
|
27
|
+
|
|
28
|
+
5. **Settlement is gated on `response.status < 400`.** If the handler throws or returns an error response, no settlement occurs. The payer's funds are not captured. This is a fundamental safety guarantee.
|
|
29
|
+
|
|
30
|
+
6. **The plugin interface is the observability boundary.** All Merit-specific telemetry (ClickHouse, Discord alerts, usage tracking) lives in a private `RouterPlugin` implementation. The router itself is fully open-source with zero Merit-specific code. The plugin hooks are fire-and-forget — they never delay the response.
|
|
31
|
+
|
|
32
|
+
7. **Self-registering routes + validated barrel (Approach B).** Routes self-register via `.handler()` at import time. A barrel file imports all route modules. Discovery endpoints (`.wellKnown()`, `.openapi()`) validate that the barrel is complete by comparing registered routes against the `prices` map. For routes in separate handler files (e.g., Next.js `route.ts` files), use **discovery stubs** — lightweight registrations that provide metadata for discovery without the real handler. Guard stubs with `registry.has()` to avoid unnecessary overwrites.
|
|
33
|
+
|
|
34
|
+
8. **Both x402 and MPP ship from day one.** Dual-protocol support is not an afterthought. Routes declare `protocols: ['x402', 'mpp']` and the orchestration layer routes to the correct handler based on the request header. MPP uses low-level `mpay` primitives (`Challenge`, `Credential`, `tempo.charge`) — not the high-level `Mpay.create()` wrapper — because the router owns orchestration.
|
|
35
|
+
|
|
36
|
+
9. **`zod-openapi` for OpenAPI 3.1.** Zod schemas are the single source of truth for request/response types. OpenAPI docs are auto-generated from them. No manual spec maintenance.
|
|
37
|
+
|
|
38
|
+
10. **Fakes over mocks in tests.** The test suite uses `FakeX402Server` (a behavioral fake that accepts known payer/payee/amount tuples) instead of vi.mock stubs. This tests real verification logic, not mock wiring.
|
|
39
|
+
|
|
40
|
+
### Version Stability
|
|
41
|
+
|
|
42
|
+
The public API is **not stable**. Downstream consumers should pin exact versions (`"@agentcash/router": "0.2.0"`, not `"^0.2.0"`). Breaking changes will happen as we build out multi-protocol support and discover patterns across services. Semver will be respected once we hit 1.0.
|
|
43
|
+
|
|
44
|
+
### Non-goals (deliberately rejected)
|
|
45
|
+
|
|
46
|
+
- **No middleware chain.** Express-style middleware (`use()`) was considered and rejected. The builder's fixed lifecycle (auth → parse → validate → price → verify → handler → settle) covers all routes. Custom logic goes in the handler or plugin.
|
|
47
|
+
- **No per-request config overrides.** The route is fully configured at definition time. Runtime behavior changes go through the handler function, not builder reconfiguration.
|
|
48
|
+
- **No automatic retry on settlement failure.** Settlement failures are logged as critical alerts. Retry logic belongs in the plugin implementation, not the router core.
|
|
49
|
+
|
|
50
|
+
## Architecture Overview
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
createRouter(config) → ServiceRouter
|
|
54
|
+
├── .route(key) → RouteBuilder (fluent chain)
|
|
55
|
+
│ ├── Auth: .paid() | .siwx() | .apiKey() | .unprotected()
|
|
56
|
+
│ ├── Schema: .body(zod) | .query(zod) | .output(zod)
|
|
57
|
+
│ ├── Meta: .description() | .path() | .provider()
|
|
58
|
+
│ └── Terminal: .handler(fn) → Next.js handler
|
|
59
|
+
├── .wellKnown() → /.well-known/x402 handler
|
|
60
|
+
├── .openapi() → /openapi.json handler
|
|
61
|
+
└── .monitors() → MonitorEntry[] (for cron)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Request lifecycle (orchestrate.ts)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Request in
|
|
68
|
+
→ await x402 server init
|
|
69
|
+
→ plugin.onRequest() → PluginContext
|
|
70
|
+
→ if unprotected: skip to handler
|
|
71
|
+
→ if apiKey route: verify API key → 401 if invalid
|
|
72
|
+
→ detectProtocol(request) from headers
|
|
73
|
+
→ if no payment header + dynamic pricing + body schema:
|
|
74
|
+
early body parse via request.clone() for accurate 402 price
|
|
75
|
+
→ if SIWX: challenge or verify → handleAuth
|
|
76
|
+
→ if no auth header: build 402 challenge
|
|
77
|
+
(x402: PAYMENT-REQUIRED header, MPP: WWW-Authenticate header)
|
|
78
|
+
→ bufferBody + validateBody (only if .body() chained)
|
|
79
|
+
→ resolvePrice (static / dynamic / tiered)
|
|
80
|
+
→ protocol verify (x402 or MPP)
|
|
81
|
+
→ plugin.onPaymentVerified()
|
|
82
|
+
→ handler(ctx) → result
|
|
83
|
+
→ if status < 400: settle payment (x402) or receipt (MPP)
|
|
84
|
+
→ if provider configured: fireProviderQuota()
|
|
85
|
+
→ plugin.onResponse()
|
|
86
|
+
Response out
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Key Files
|
|
90
|
+
|
|
91
|
+
| File | Purpose |
|
|
92
|
+
|------|---------|
|
|
93
|
+
| `src/index.ts` | `createRouter()`, `ServiceRouter`, re-exports |
|
|
94
|
+
| `src/builder.ts` | `RouteBuilder` fluent API with type-level state tracking |
|
|
95
|
+
| `src/orchestrate.ts` | Core request lifecycle, `createRequestHandler()` |
|
|
96
|
+
| `src/types.ts` | `HttpError`, `HandlerContext`, pricing/provider/auth types |
|
|
97
|
+
| `src/plugin.ts` | `RouterPlugin` interface, `consolePlugin()`, `firePluginHook()` |
|
|
98
|
+
| `src/pricing.ts` | `resolvePrice()`, `resolveMaxPrice()` |
|
|
99
|
+
| `src/handler.ts` | `safeCallHandler()` error boundary |
|
|
100
|
+
| `src/body.ts` | `bufferBody()`, `validateBody()` |
|
|
101
|
+
| `src/registry.ts` | `RouteRegistry` with barrel validation |
|
|
102
|
+
| `src/protocols/detect.ts` | Header-based protocol detection |
|
|
103
|
+
| `src/protocols/x402.ts` | x402 challenge/verify/settle wrappers |
|
|
104
|
+
| `src/protocols/mpp.ts` | MPP challenge/verify/receipt wrappers (uses mpay low-level primitives) |
|
|
105
|
+
| `src/server.ts` | x402 server initialization with retry |
|
|
106
|
+
| `src/auth/siwx.ts` | SIWX verification |
|
|
107
|
+
| `src/auth/api-key.ts` | API key verification |
|
|
108
|
+
| `src/auth/nonce.ts` | `NonceStore` interface + `MemoryNonceStore` |
|
|
109
|
+
| `src/discovery/well-known.ts` | `.well-known/x402` generation |
|
|
110
|
+
| `src/discovery/openapi.ts` | OpenAPI 3.1 spec generation |
|
|
111
|
+
|
|
112
|
+
## Environment Setup
|
|
113
|
+
|
|
114
|
+
**CRITICAL:** The router uses the default facilitator from `@coinbase/x402`, which requires CDP API keys at runtime:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
CDP_API_KEY_ID=your-key-id
|
|
118
|
+
CDP_API_KEY_SECRET=your-key-secret
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**For Next.js apps with env validation (T3 stack, `@t3-oss/env-nextjs`):** These must be declared in your env schema. Next.js does not expose undeclared env vars to `process.env`.
|
|
122
|
+
|
|
123
|
+
Without these keys, x402 server initialization fails:
|
|
124
|
+
- Error: `"Failed to fetch supported kinds from facilitator: TypeError: fetch failed"`
|
|
125
|
+
- Or: `"Facilitator getSupported failed (401): Unauthorized"`
|
|
126
|
+
- Symptom: All paid routes return empty 402 responses (no `PAYMENT-REQUIRED` header)
|
|
127
|
+
|
|
128
|
+
**Example env schema (T3):**
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// src/env.js
|
|
132
|
+
import { createEnv } from "@t3-oss/env-nextjs";
|
|
133
|
+
import { z } from "zod";
|
|
134
|
+
|
|
135
|
+
export const env = createEnv({
|
|
136
|
+
server: {
|
|
137
|
+
CDP_API_KEY_ID: z.string(),
|
|
138
|
+
CDP_API_KEY_SECRET: z.string(),
|
|
139
|
+
// ... other vars
|
|
140
|
+
},
|
|
141
|
+
runtimeEnv: {
|
|
142
|
+
CDP_API_KEY_ID: process.env.CDP_API_KEY_ID,
|
|
143
|
+
CDP_API_KEY_SECRET: process.env.CDP_API_KEY_SECRET,
|
|
144
|
+
// ... other vars
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Why this matters:** The default facilitator reads `process.env.CDP_API_KEY_ID` and `process.env.CDP_API_KEY_SECRET` when creating auth headers for the CDP facilitator API. If they're not in `process.env`, authentication fails silently — the facilitator sends requests without `Authorization` headers, and CDP returns 401.
|
|
150
|
+
|
|
151
|
+
### MPP environment
|
|
152
|
+
|
|
153
|
+
For MPP (Micropayment Protocol via Tempo blockchain), you need:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
MPP_SECRET_KEY=your-hmac-secret # HMAC key for challenge binding
|
|
157
|
+
TEMPO_RPC_URL=https://user:pass@rpc.mainnet.tempo.xyz # Authenticated Tempo RPC
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Tempo RPC requires authentication.** The default `rpc.tempo.xyz` returns 401. Get credentials from the Tempo team. The `rpcUrl` can be set in config or via `TEMPO_RPC_URL` env var (config takes precedence).
|
|
161
|
+
|
|
162
|
+
**Peer dependencies for MPP:** `mpay` is an optional peer dep. When installed, it brings `viem` as a transitive dependency. Both are required for MPP support.
|
|
163
|
+
|
|
164
|
+
## Creating Routes
|
|
165
|
+
|
|
166
|
+
### Step 1: Router setup (once per service)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// lib/routes.ts
|
|
170
|
+
import { createRouter } from '@agentcash/router';
|
|
171
|
+
|
|
172
|
+
export const router = createRouter({
|
|
173
|
+
payeeAddress: process.env.X402_PAYEE_ADDRESS!,
|
|
174
|
+
// Optional:
|
|
175
|
+
network: 'eip155:8453', // default
|
|
176
|
+
protocols: ['x402', 'mpp'], // protocols for auto-priced routes (default: ['x402'])
|
|
177
|
+
plugin: myPlugin, // observability
|
|
178
|
+
prices: { 'search': '0.02' }, // central pricing map
|
|
179
|
+
mpp: { // MPP support (requires mpay peer dep)
|
|
180
|
+
secretKey: process.env.MPP_SECRET_KEY!,
|
|
181
|
+
currency: '0x20c0000000000000000000000000000000000000', // PathUSD on Tempo
|
|
182
|
+
recipient: process.env.X402_PAYEE_ADDRESS!,
|
|
183
|
+
rpcUrl: process.env.TEMPO_RPC_URL, // falls back to TEMPO_RPC_URL env var
|
|
184
|
+
},
|
|
185
|
+
siwx: { nonceStore }, // custom nonce store
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Router-level `protocols`:** Sets default protocols for routes using the `prices` map (auto-priced routes). Routes using `.paid()` directly can override with `{ protocols: [...] }`.
|
|
190
|
+
|
|
191
|
+
### Step 2: Define route files
|
|
192
|
+
|
|
193
|
+
Each route file exports a Next.js handler (`GET`, `POST`, etc.):
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// app/api/search/route.ts
|
|
197
|
+
import { router } from '@/lib/routes';
|
|
198
|
+
import { searchSchema, searchResponseSchema } from '@/lib/schemas';
|
|
199
|
+
|
|
200
|
+
export const POST = router.route('search')
|
|
201
|
+
.paid('0.01')
|
|
202
|
+
.body(searchSchema)
|
|
203
|
+
.output(searchResponseSchema)
|
|
204
|
+
.description('Search the web')
|
|
205
|
+
.handler(async ({ body }) => searchService(body));
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Step 3: Barrel import
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// lib/routes/barrel.ts
|
|
212
|
+
import '@/app/api/search/route';
|
|
213
|
+
import '@/app/api/lookup/route';
|
|
214
|
+
// ... all route files
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The barrel ensures all routes are registered before discovery endpoints generate their output.
|
|
218
|
+
|
|
219
|
+
## Auth Modes
|
|
220
|
+
|
|
221
|
+
### Paid (x402) — static price
|
|
222
|
+
```typescript
|
|
223
|
+
router.route('search').paid('0.01').body(schema).handler(fn);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Paid (dynamic pricing)
|
|
227
|
+
```typescript
|
|
228
|
+
router.route('gen')
|
|
229
|
+
.paid((body) => calculateCost(body), { maxPrice: '5.00' })
|
|
230
|
+
.body(schema)
|
|
231
|
+
.handler(fn);
|
|
232
|
+
```
|
|
233
|
+
**How it works:** When a request arrives without a payment header, the router clones the request (`request.clone()`), parses the body early, and calls the pricing function to calculate an accurate price for the 402 challenge. This means clients see the real price, not a ceiling.
|
|
234
|
+
|
|
235
|
+
`maxPrice` is **optional** and acts as a safety net:
|
|
236
|
+
- **Cap:** If the pricing function returns a value above `maxPrice`, the price is capped and a warning is fired via the plugin.
|
|
237
|
+
- **Fallback:** If the pricing function throws, `maxPrice` is used as a degraded-mode fallback. Without `maxPrice`, the route returns 500.
|
|
238
|
+
- If omitted and the pricing function succeeds, the exact calculated price is used.
|
|
239
|
+
|
|
240
|
+
### Paid (tiered) — requires body
|
|
241
|
+
```typescript
|
|
242
|
+
router.route('upload').paid({
|
|
243
|
+
field: 'tier',
|
|
244
|
+
tiers: {
|
|
245
|
+
'10mb': { price: '0.02', label: '10 MB' },
|
|
246
|
+
'100mb': { price: '0.20', label: '100 MB' },
|
|
247
|
+
},
|
|
248
|
+
}).body(schema).handler(fn);
|
|
249
|
+
```
|
|
250
|
+
The tier value comes from `body[field]`. The 402 challenge uses the highest tier price.
|
|
251
|
+
|
|
252
|
+
### Dual protocol (x402 + MPP)
|
|
253
|
+
```typescript
|
|
254
|
+
router.route('search')
|
|
255
|
+
.paid('0.01', { protocols: ['x402', 'mpp'] })
|
|
256
|
+
.body(schema).handler(fn);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### SIWX (wallet auth, no payment)
|
|
260
|
+
```typescript
|
|
261
|
+
router.route('inbox/status')
|
|
262
|
+
.siwx()
|
|
263
|
+
.query(querySchema)
|
|
264
|
+
.handler(async ({ query, wallet }) => getStatus(query, wallet));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### API key (composable with paid)
|
|
268
|
+
```typescript
|
|
269
|
+
router.route('admin/lookup')
|
|
270
|
+
.apiKey((key) => key === 'valid' ? { id: 'acct-1' } : null)
|
|
271
|
+
.paid('0.05')
|
|
272
|
+
.body(schema)
|
|
273
|
+
.handler(async ({ body, account }) => lookup(body, account));
|
|
274
|
+
```
|
|
275
|
+
API key is checked first (401 on failure), then payment flow runs.
|
|
276
|
+
|
|
277
|
+
### Unprotected
|
|
278
|
+
```typescript
|
|
279
|
+
router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Handler Context
|
|
283
|
+
|
|
284
|
+
Every handler receives:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface HandlerContext<TBody, TQuery> {
|
|
288
|
+
body: TBody; // Parsed + validated (undefined if no .body())
|
|
289
|
+
query: TQuery; // Parsed + validated (undefined if no .query())
|
|
290
|
+
request: NextRequest; // Raw request
|
|
291
|
+
requestId: string; // Unique per-request UUID (for logging/tracing)
|
|
292
|
+
route: string; // Route key (e.g. 'search', 'lookup/org')
|
|
293
|
+
wallet: string | null; // Verified wallet (from payment or SIWX)
|
|
294
|
+
account: unknown; // From .apiKey() resolver
|
|
295
|
+
alert: AlertFn; // Fire observability alerts
|
|
296
|
+
setVerifiedWallet: (addr: string) => void;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Provider Monitoring
|
|
301
|
+
|
|
302
|
+
Routes that wrap third-party APIs can declare monitoring behavior per-provider. This surfaces quota/balance information through the plugin system.
|
|
303
|
+
|
|
304
|
+
### The six provider patterns
|
|
305
|
+
|
|
306
|
+
| Pattern | How handled |
|
|
307
|
+
|---------|-------------|
|
|
308
|
+
| Balance in response headers | `extractQuota` reads `headers` |
|
|
309
|
+
| Balance in response body | `extractQuota` reads `result` |
|
|
310
|
+
| Separate health-check endpoint | `monitor` function (for cron) |
|
|
311
|
+
| Overages at same rate | `overage: 'same-rate'` |
|
|
312
|
+
| Overages at increased rate | `overage: 'increased-rate'` |
|
|
313
|
+
| No overages, hard stop | `overage: 'hard-stop'` |
|
|
314
|
+
|
|
315
|
+
### Usage
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
export const POST = router.route('exa/search')
|
|
319
|
+
.paid('0.01')
|
|
320
|
+
.provider('exa', {
|
|
321
|
+
extractQuota: (result, headers) => ({
|
|
322
|
+
remaining: (result as any).rateLimit?.remaining ?? null,
|
|
323
|
+
limit: (result as any).rateLimit?.limit ?? null,
|
|
324
|
+
}),
|
|
325
|
+
monitor: async () => {
|
|
326
|
+
const res = await fetch('https://api.exa.ai/usage');
|
|
327
|
+
const data = await res.json();
|
|
328
|
+
return { remaining: data.credits, limit: data.limit };
|
|
329
|
+
},
|
|
330
|
+
overage: 'same-rate',
|
|
331
|
+
warn: 100,
|
|
332
|
+
critical: 10,
|
|
333
|
+
})
|
|
334
|
+
.body(searchSchema)
|
|
335
|
+
.handler(async ({ body }) => exaClient.search(body));
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### ProviderConfig fields
|
|
339
|
+
|
|
340
|
+
| Field | Type | Default | Description |
|
|
341
|
+
|-------|------|---------|-------------|
|
|
342
|
+
| `extractQuota` | `(result, headers) => QuotaInfo \| null` | — | Inline extraction after each success |
|
|
343
|
+
| `monitor` | `() => Promise<QuotaInfo \| null>` | — | Standalone check for cron |
|
|
344
|
+
| `overage` | `'same-rate' \| 'increased-rate' \| 'hard-stop'` | `'same-rate'` | What happens at zero |
|
|
345
|
+
| `warn` | `number` | — | Warn level threshold |
|
|
346
|
+
| `critical` | `number` | — | Critical level threshold |
|
|
347
|
+
|
|
348
|
+
### Threshold logic
|
|
349
|
+
|
|
350
|
+
- `remaining === null` → `healthy` (no data)
|
|
351
|
+
- `remaining <= critical` → `critical`
|
|
352
|
+
- `remaining <= warn` → `warn`
|
|
353
|
+
- Otherwise → `healthy`
|
|
354
|
+
|
|
355
|
+
### Cron monitors
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
for (const entry of router.monitors()) {
|
|
359
|
+
const quota = await entry.monitor();
|
|
360
|
+
// entry: { provider, route, monitor, overage, warn, critical }
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Safety guarantees
|
|
365
|
+
|
|
366
|
+
- `extractQuota` is fire-and-forget — exceptions never affect the response
|
|
367
|
+
- Only runs when `response.status < 400`
|
|
368
|
+
- Plugin hook (`onProviderQuota`) is non-blocking
|
|
369
|
+
|
|
370
|
+
## Discovery Setup
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// app/.well-known/x402/route.ts
|
|
374
|
+
import '@/lib/routes/barrel'; // barrel import FIRST to register all routes
|
|
375
|
+
import { router } from '@/lib/routes';
|
|
376
|
+
export const GET = router.wellKnown();
|
|
377
|
+
|
|
378
|
+
// app/openapi.json/route.ts
|
|
379
|
+
import '@/lib/routes/barrel';
|
|
380
|
+
import { router } from '@/lib/routes';
|
|
381
|
+
export const GET = router.openapi({ title: 'My API', version: '1.0.0' });
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Barrel import must come first.** Without it, Next.js lazy-loads route modules, so discovery endpoints hit before routes register → `route 'X' in prices map but not registered` error.
|
|
385
|
+
|
|
386
|
+
**`.well-known/x402` output** includes `mppResources` alongside `resources` when MPP routes exist:
|
|
387
|
+
```json
|
|
388
|
+
{ "version": 1, "resources": ["..."], "mppResources": ["..."] }
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**OpenAPI spec** includes `x-payment-info` with `price` and `protocols` per operation.
|
|
392
|
+
|
|
393
|
+
## Plugin (Observability)
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const myPlugin: RouterPlugin = {
|
|
397
|
+
onRequest(meta) { /* return PluginContext */ },
|
|
398
|
+
onPaymentVerified(ctx, payment) { /* log payment */ },
|
|
399
|
+
onPaymentSettled(ctx, settlement) { /* log tx */ },
|
|
400
|
+
onResponse(ctx, response) { /* log response */ },
|
|
401
|
+
onError(ctx, error) { /* alert on errors */ },
|
|
402
|
+
onAlert(ctx, alert) { /* handle custom alerts */ },
|
|
403
|
+
onProviderQuota(ctx, event) { /* handle quota events */ },
|
|
404
|
+
};
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
All hooks are optional. All are fire-and-forget. Use `consolePlugin()` for dev logging.
|
|
408
|
+
|
|
409
|
+
## Builder Compile-Time Safety
|
|
410
|
+
|
|
411
|
+
The type system (generic parameters `HasAuth`, `NeedsBody`, `HasBody`) prevents invalid chains:
|
|
412
|
+
|
|
413
|
+
- `.handler()` requires auth to be set first
|
|
414
|
+
- Dynamic/tiered pricing requires `.body()` before `.handler()`
|
|
415
|
+
- `.siwx()` is mutually exclusive with `.paid()`
|
|
416
|
+
- `.apiKey()` CAN compose with `.paid()`
|
|
417
|
+
|
|
418
|
+
## MPP Internals (Critical Pitfalls)
|
|
419
|
+
|
|
420
|
+
The router uses mpay's **low-level primitives**, not the high-level `Mpay.create()` API. This matters because mpay's internals have subtle conventions:
|
|
421
|
+
|
|
422
|
+
1. **NextRequest vs Request.** `Credential.fromRequest()` breaks with Next.js `NextRequest` due to subtle header handling differences. The router converts via `toStandardRequest()` — creating a new standard `Request` with the same URL, method, headers, and body.
|
|
423
|
+
|
|
424
|
+
2. **`Challenge.fromIntent()` takes payment data, not an HTTP Request.** The `request` field in `fromIntent()` is the payment request object (`{ amount, currency, recipient, decimals }`), NOT the HTTP Request. Passing the wrong object causes silent challenge generation failures.
|
|
425
|
+
|
|
426
|
+
3. **`tempo.charge().verify()` returns a receipt, not `{ valid, payer }`.** On success it returns `{ method, status, reference, timestamp }`. On failure it throws. Check `receipt.status === 'success'`, not `receipt.valid`.
|
|
427
|
+
|
|
428
|
+
4. **`getClient` must be synchronous.** mpay's `Client.getResolver()` checks `if (getClient) return getClient` — it does NOT await. An async `getClient` will silently fall through to the default RPC URL.
|
|
429
|
+
|
|
430
|
+
5. **Tempo RPC requires authentication.** The default `rpc.tempo.xyz` returns 401. Always provide `rpcUrl` or set `TEMPO_RPC_URL` env var with authenticated credentials.
|
|
431
|
+
|
|
432
|
+
6. **viem is loaded eagerly in `ensureMpay()`.** Since `getClient` must be synchronous, viem's `createClient` and `http` are loaded once when mpay initializes, not per-call. viem is a transitive dep of mpay and is always available when mpay is installed.
|
|
433
|
+
|
|
434
|
+
## Registration-Time Validation
|
|
435
|
+
|
|
436
|
+
These throw immediately when the route is defined (not at request time):
|
|
437
|
+
|
|
438
|
+
- Empty tier key in tiered pricing
|
|
439
|
+
- `maxPrice` that isn't a positive decimal
|
|
440
|
+
|
|
441
|
+
**Duplicate route keys** do NOT throw — the registry silently overwrites with a dev-only `console.warn`. This is intentional: Next.js `next build` loads modules non-deterministically, so discovery stubs and real handlers may register the same key in either order. Last writer wins. Prior art: ElysiaJS uses the identical pattern.
|
|
442
|
+
|
|
443
|
+
## Central Pricing Map
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
const router = createRouter({
|
|
447
|
+
payeeAddress: '...',
|
|
448
|
+
protocols: ['x402', 'mpp'], // default protocols for all auto-priced routes
|
|
449
|
+
prices: { 'search': '0.02', 'lookup': '0.05' },
|
|
450
|
+
mpp: { secretKey, currency, recipient, rpcUrl },
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// .paid() is auto-applied with router-level protocols:
|
|
454
|
+
export const POST = router.route('search').body(schema).handler(fn);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Routes using `prices` inherit the router-level `protocols` config. Routes using `.paid()` directly can override per-route with `{ protocols: [...] }`.
|
|
458
|
+
|
|
459
|
+
Barrel validation catches mismatches: keys in `prices` but not registered → error.
|
|
460
|
+
|
|
461
|
+
## Common Patterns
|
|
462
|
+
|
|
463
|
+
### Return an error from handler
|
|
464
|
+
```typescript
|
|
465
|
+
.handler(async ({ body }) => {
|
|
466
|
+
if (!valid(body)) throw new HttpError('Invalid input', 400);
|
|
467
|
+
return result;
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Return a raw Response (streaming)
|
|
472
|
+
```typescript
|
|
473
|
+
.handler(async ({ body }) => {
|
|
474
|
+
return new Response(streamData, {
|
|
475
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Fire a custom alert
|
|
481
|
+
```typescript
|
|
482
|
+
.handler(async ({ body, alert }) => {
|
|
483
|
+
const result = await callProvider(body);
|
|
484
|
+
if (result.slow) alert('warn', 'Slow response', { latency: result.latency });
|
|
485
|
+
return result;
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Troubleshooting
|
|
490
|
+
|
|
491
|
+
| Issue | Cause | Fix |
|
|
492
|
+
|-------|-------|-----|
|
|
493
|
+
| `route 'X' registered twice` warning | Discovery stub + real handler both register same key | Expected during `next build` — last writer wins. Use `registry.has()` guards on stubs to suppress. |
|
|
494
|
+
| `x402 server not initialized` | Missing peer deps | Install `@x402/core @x402/evm @x402/extensions @coinbase/x402` |
|
|
495
|
+
| 402 on every request | No payment header | Client must send `PAYMENT-SIGNATURE` (x402) or `Authorization: Payment` (MPP) |
|
|
496
|
+
| Body undefined in handler | No `.body()` chained | Add `.body(schema)` to the chain |
|
|
497
|
+
| Route not in discovery docs | Missing barrel import | Import the route file in your barrel |
|
|
498
|
+
| Settlement not happening | Handler returned status >= 400 | Settlement is gated on success responses |
|
|
499
|
+
| MPP 401 `unauthorized: authentication required` | Using default unauthenticated Tempo RPC | Set `TEMPO_RPC_URL` env var or `mpp.rpcUrl` config with authenticated URL |
|
|
500
|
+
| MPP `Credential.fromRequest()` returns undefined | NextRequest header handling incompatibility | Router handles this via `toStandardRequest()` — if you see this, the router dist is stale |
|
|
501
|
+
| MPP verify returns `status: 'success'` but route returns 402 | Code checking `.valid` instead of `.status` | Verify returns a receipt `{ status, reference }`, not `{ valid, payer }` |
|
|
502
|
+
| MPP using wrong RPC URL after rebuild | Next.js webpack cache or stale pnpm link | Delete `.next/`, run `pnpm install` in the app to pick up new router dist |
|
|
503
|
+
| `route 'X' in prices map but not registered` | Discovery endpoint hit before route module loaded | Add barrel import to discovery route files |
|
|
504
|
+
| `mpay package is required` | mpay not installed | `pnpm add mpay` — it's an optional peer dep |
|
|
505
|
+
|
|
506
|
+
## Maintaining This Skill
|
|
507
|
+
|
|
508
|
+
This skill file is the canonical reference for `@agentcash/router`. It ships with the repo at `.claude/skills/router-guide/SKILL.md` so that any agent working with the router has accurate, up-to-date guidance.
|
|
509
|
+
|
|
510
|
+
**When you modify the router, update this skill.** Specifically:
|
|
511
|
+
|
|
512
|
+
- **New interfaces or types** — Add them to the relevant section (Handler Context, Plugin, Provider Monitoring, etc.)
|
|
513
|
+
- **New builder methods** — Document in the Auth Modes or Common Patterns sections
|
|
514
|
+
- **New plugin hooks** — Add to the Plugin section
|
|
515
|
+
- **Changed behavior** — Update the Request Lifecycle diagram and any affected sections
|
|
516
|
+
- **New troubleshooting entries** — Add to the Troubleshooting table
|
|
517
|
+
- **New design constraints or non-goals** — Add to the Philosophy section
|
|
518
|
+
|
|
519
|
+
The goal is that any future agent can read this single file and implement correctly without needing to reverse-engineer the source. Keep it accurate, keep it concise.
|
|
520
|
+
|
|
521
|
+
## Porting Services to the Router
|
|
522
|
+
|
|
523
|
+
When porting an existing service to `@agentcash/router`, the service's route patterns may not cleanly map to the router's abstractions. **If you encounter a pattern that the router doesn't support, do not work around it silently.** Instead:
|
|
524
|
+
|
|
525
|
+
1. **Inform the user** — explain what the service does, what the router expects, and where the mismatch is.
|
|
526
|
+
2. **Suggest a router PR** — if the gap is a reasonable feature (not a one-off hack), propose adding it to the router. Describe the change, which files it touches, and why it's general-purpose.
|
|
527
|
+
3. **Let the user decide** — they may prefer a workaround, a router change, or restructuring the service.
|
|
528
|
+
|
|
529
|
+
Common conformance gaps to watch for:
|
|
530
|
+
- Handler that needs raw request body (not JSON) — router buffers as JSON when `.body()` is chained. Consider if the route should skip `.body()` and parse from `ctx.request` directly.
|
|
531
|
+
- Multiple pricing strategies on one route — router supports static, dynamic, or tiered, but only one per route.
|
|
532
|
+
- Auth mode that doesn't fit the 4 modes — router enforces paid/siwx/apiKey/unprotected. Composite auth beyond apiKey+paid is not supported.
|
|
533
|
+
- Custom response headers set by middleware — router owns the response lifecycle. Headers go in the handler return or via the plugin.
|
package/dist/index.cjs
CHANGED
|
@@ -120,7 +120,7 @@ var RouteRegistry = class {
|
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
// src/orchestrate.ts
|
|
123
|
-
var
|
|
123
|
+
var import_server3 = require("next/server");
|
|
124
124
|
|
|
125
125
|
// src/plugin.ts
|
|
126
126
|
function createDefaultContext(meta) {
|
|
@@ -354,70 +354,113 @@ async function settleX402Payment(server, payload, requirements) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
// src/protocols/mpp.ts
|
|
357
|
-
var
|
|
358
|
-
var
|
|
359
|
-
var
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
mpayLoaded = true;
|
|
372
|
-
} catch {
|
|
373
|
-
throw new Error("mpay package is required for MPP protocol support. Install it: pnpm add mpay");
|
|
357
|
+
var import_mpay = require("mpay");
|
|
358
|
+
var import_server2 = require("mpay/server");
|
|
359
|
+
var import_viem = require("viem");
|
|
360
|
+
var import_chains = require("viem/chains");
|
|
361
|
+
function buildGetClient(rpcUrl) {
|
|
362
|
+
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
363
|
+
if (!url) return {};
|
|
364
|
+
return {
|
|
365
|
+
getClient: () => (0, import_viem.createClient)({ chain: import_chains.tempo, transport: (0, import_viem.http)(url) })
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function toStandardRequest(request) {
|
|
369
|
+
if (request.constructor.name === "Request") {
|
|
370
|
+
return request;
|
|
374
371
|
}
|
|
372
|
+
return new Request(request.url, {
|
|
373
|
+
method: request.method,
|
|
374
|
+
headers: request.headers,
|
|
375
|
+
body: request.body,
|
|
376
|
+
// @ts-expect-error - Request.duplex is required for streaming bodies but not in types yet
|
|
377
|
+
duplex: "half"
|
|
378
|
+
});
|
|
375
379
|
}
|
|
380
|
+
var DEFAULT_DECIMALS = 6;
|
|
376
381
|
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
+
const standardRequest = toStandardRequest(request);
|
|
383
|
+
const currency = mppConfig.currency;
|
|
384
|
+
const recipient = mppConfig.recipient ?? "";
|
|
385
|
+
const methodIntent = import_server2.tempo.charge({
|
|
386
|
+
currency,
|
|
387
|
+
recipient
|
|
382
388
|
});
|
|
383
|
-
const challenge = Challenge.fromIntent(methodIntent, {
|
|
389
|
+
const challenge = import_mpay.Challenge.fromIntent(methodIntent, {
|
|
384
390
|
secretKey: mppConfig.secretKey,
|
|
385
|
-
realm: new URL(
|
|
386
|
-
request
|
|
391
|
+
realm: new URL(standardRequest.url).origin,
|
|
392
|
+
request: {
|
|
393
|
+
amount: price,
|
|
394
|
+
currency,
|
|
395
|
+
recipient,
|
|
396
|
+
decimals: DEFAULT_DECIMALS
|
|
397
|
+
}
|
|
387
398
|
});
|
|
388
|
-
return Challenge.serialize(challenge);
|
|
399
|
+
return import_mpay.Challenge.serialize(challenge);
|
|
389
400
|
}
|
|
390
401
|
async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
const standardRequest = toStandardRequest(request);
|
|
403
|
+
const currency = mppConfig.currency;
|
|
404
|
+
const recipient = mppConfig.recipient ?? "";
|
|
405
|
+
try {
|
|
406
|
+
const authHeader = standardRequest.headers.get("Authorization");
|
|
407
|
+
if (!authHeader) {
|
|
408
|
+
console.error("[MPP] No Authorization header found");
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const credential = import_mpay.Credential.fromRequest(standardRequest);
|
|
412
|
+
if (!credential?.challenge) {
|
|
413
|
+
console.error("[MPP] Invalid credential structure");
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const isValid = import_mpay.Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
|
|
417
|
+
if (!isValid) {
|
|
418
|
+
console.error("[MPP] Challenge HMAC verification failed");
|
|
419
|
+
return { valid: false, payer: null };
|
|
420
|
+
}
|
|
421
|
+
const methodIntent = import_server2.tempo.charge({
|
|
422
|
+
currency,
|
|
423
|
+
recipient,
|
|
424
|
+
...buildGetClient(mppConfig.rpcUrl)
|
|
425
|
+
});
|
|
426
|
+
const paymentRequest = {
|
|
427
|
+
amount: price,
|
|
428
|
+
currency,
|
|
429
|
+
recipient,
|
|
430
|
+
decimals: DEFAULT_DECIMALS
|
|
431
|
+
};
|
|
432
|
+
const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
|
|
433
|
+
const receipt = await methodIntent.verify({
|
|
434
|
+
credential,
|
|
435
|
+
request: resolvedRequest
|
|
436
|
+
});
|
|
437
|
+
if (!receipt || receipt.status !== "success") {
|
|
438
|
+
console.error("[MPP] Tempo verification failed:", receipt);
|
|
439
|
+
return { valid: false, payer: null };
|
|
440
|
+
}
|
|
441
|
+
const payer = receipt.reference ?? "";
|
|
442
|
+
return {
|
|
443
|
+
valid: true,
|
|
444
|
+
payer,
|
|
445
|
+
txHash: receipt.reference
|
|
446
|
+
};
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error("[MPP] Credential verification error:", {
|
|
449
|
+
message: error instanceof Error ? error.message : String(error),
|
|
450
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
451
|
+
errorType: error?.constructor?.name
|
|
452
|
+
});
|
|
453
|
+
return null;
|
|
406
454
|
}
|
|
407
|
-
return {
|
|
408
|
-
valid: true,
|
|
409
|
-
payer: verifyResult.payer
|
|
410
|
-
};
|
|
411
455
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const receipt = Receipt.from({
|
|
456
|
+
function buildMPPReceipt(reference) {
|
|
457
|
+
const receipt = import_mpay.Receipt.from({
|
|
415
458
|
method: "tempo",
|
|
416
459
|
status: "success",
|
|
417
460
|
reference,
|
|
418
461
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
419
462
|
});
|
|
420
|
-
return Receipt.serialize(receipt);
|
|
463
|
+
return import_mpay.Receipt.serialize(receipt);
|
|
421
464
|
}
|
|
422
465
|
|
|
423
466
|
// src/auth/siwx.ts
|
|
@@ -495,7 +538,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
495
538
|
firePluginResponse(deps, pluginCtx, meta, response);
|
|
496
539
|
}
|
|
497
540
|
function fail(status, message, meta, pluginCtx) {
|
|
498
|
-
const response =
|
|
541
|
+
const response = import_server3.NextResponse.json({ success: false, error: message }, { status });
|
|
499
542
|
firePluginResponse(deps, pluginCtx, meta, response);
|
|
500
543
|
return response;
|
|
501
544
|
}
|
|
@@ -595,7 +638,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
595
638
|
route: routeEntry.key
|
|
596
639
|
});
|
|
597
640
|
}
|
|
598
|
-
const response = new
|
|
641
|
+
const response = new import_server3.NextResponse(JSON.stringify(paymentRequired), {
|
|
599
642
|
status: 402,
|
|
600
643
|
headers: { "Content-Type": "application/json" }
|
|
601
644
|
});
|
|
@@ -727,7 +770,7 @@ async function parseBody(request, routeEntry) {
|
|
|
727
770
|
if (result.success) return { ok: true, data: result.data };
|
|
728
771
|
return {
|
|
729
772
|
ok: false,
|
|
730
|
-
response:
|
|
773
|
+
response: import_server3.NextResponse.json(
|
|
731
774
|
{ success: false, error: result.error, issues: result.issues },
|
|
732
775
|
{ status: 400 }
|
|
733
776
|
)
|
|
@@ -786,7 +829,7 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
786
829
|
});
|
|
787
830
|
return { price: routeEntry.maxPrice };
|
|
788
831
|
} else {
|
|
789
|
-
const errorResponse =
|
|
832
|
+
const errorResponse = import_server3.NextResponse.json(
|
|
790
833
|
{ success: false, error: "Price calculation failed" },
|
|
791
834
|
{ status: 500 }
|
|
792
835
|
);
|
|
@@ -796,7 +839,12 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
796
839
|
}
|
|
797
840
|
}
|
|
798
841
|
async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
799
|
-
const response = new
|
|
842
|
+
const response = new import_server3.NextResponse(null, {
|
|
843
|
+
status: 402,
|
|
844
|
+
headers: {
|
|
845
|
+
"Content-Type": "application/json"
|
|
846
|
+
}
|
|
847
|
+
});
|
|
800
848
|
let challengePrice;
|
|
801
849
|
if (bodyData !== void 0 && typeof routeEntry.pricing === "function") {
|
|
802
850
|
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
@@ -1087,7 +1135,7 @@ var MemoryNonceStore = class {
|
|
|
1087
1135
|
};
|
|
1088
1136
|
|
|
1089
1137
|
// src/discovery/well-known.ts
|
|
1090
|
-
var
|
|
1138
|
+
var import_server4 = require("next/server");
|
|
1091
1139
|
function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
1092
1140
|
let validated = false;
|
|
1093
1141
|
return async (_request) => {
|
|
@@ -1125,7 +1173,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1125
1173
|
if (instructions) {
|
|
1126
1174
|
body.instructions = instructions;
|
|
1127
1175
|
}
|
|
1128
|
-
return
|
|
1176
|
+
return import_server4.NextResponse.json(body, {
|
|
1129
1177
|
headers: {
|
|
1130
1178
|
"Access-Control-Allow-Origin": "*",
|
|
1131
1179
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -1136,12 +1184,12 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1136
1184
|
}
|
|
1137
1185
|
|
|
1138
1186
|
// src/discovery/openapi.ts
|
|
1139
|
-
var
|
|
1187
|
+
var import_server5 = require("next/server");
|
|
1140
1188
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
1141
1189
|
let cached = null;
|
|
1142
1190
|
let validated = false;
|
|
1143
1191
|
return async (_request) => {
|
|
1144
|
-
if (cached) return
|
|
1192
|
+
if (cached) return import_server5.NextResponse.json(cached);
|
|
1145
1193
|
if (!validated && pricesKeys) {
|
|
1146
1194
|
registry.validate(pricesKeys);
|
|
1147
1195
|
validated = true;
|
|
@@ -1168,7 +1216,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
|
1168
1216
|
tags: Array.from(tagSet).sort().map((name) => ({ name })),
|
|
1169
1217
|
paths
|
|
1170
1218
|
});
|
|
1171
|
-
return
|
|
1219
|
+
return import_server5.NextResponse.json(cached);
|
|
1172
1220
|
};
|
|
1173
1221
|
}
|
|
1174
1222
|
function deriveTag(routeKey) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { ZodType } from 'zod';
|
|
3
|
+
import { PaymentRequirements, PaymentRequired, SettleResponse } from '@x402/core/types';
|
|
3
4
|
|
|
4
5
|
interface NonceStore {
|
|
5
6
|
check(nonce: string): Promise<boolean>;
|
|
@@ -82,6 +83,7 @@ interface AlertEvent {
|
|
|
82
83
|
meta?: Record<string, unknown>;
|
|
83
84
|
}
|
|
84
85
|
type AlertFn = (level: AlertLevel, message: string, meta?: Record<string, unknown>) => void;
|
|
86
|
+
|
|
85
87
|
interface X402Server {
|
|
86
88
|
initialize(): Promise<void>;
|
|
87
89
|
buildPaymentRequirementsFromOptions(options: Array<{
|
|
@@ -91,18 +93,18 @@ interface X402Server {
|
|
|
91
93
|
payTo: string;
|
|
92
94
|
}>, context: {
|
|
93
95
|
request: Request;
|
|
94
|
-
}): Promise<
|
|
95
|
-
createPaymentRequiredResponse(requirements:
|
|
96
|
+
}): Promise<PaymentRequirements[]>;
|
|
97
|
+
createPaymentRequiredResponse(requirements: PaymentRequirements[], resource: {
|
|
96
98
|
url: string;
|
|
97
99
|
method: string;
|
|
98
100
|
description?: string;
|
|
99
|
-
}, error: string | null, extensions?: Record<string, unknown>): Promise<
|
|
100
|
-
findMatchingRequirements(requirements:
|
|
101
|
-
verifyPayment(payload: unknown, requirements:
|
|
101
|
+
}, error: string | null, extensions?: Record<string, unknown>): Promise<PaymentRequired>;
|
|
102
|
+
findMatchingRequirements(requirements: PaymentRequirements[], payload: unknown): PaymentRequirements;
|
|
103
|
+
verifyPayment(payload: unknown, requirements: PaymentRequirements): Promise<{
|
|
102
104
|
isValid: boolean;
|
|
103
105
|
payer?: string;
|
|
104
106
|
}>;
|
|
105
|
-
settlePayment(payload: unknown, requirements:
|
|
107
|
+
settlePayment(payload: unknown, requirements: PaymentRequirements): Promise<SettleResponse>;
|
|
106
108
|
}
|
|
107
109
|
type ProtocolType = 'x402' | 'mpp';
|
|
108
110
|
type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
|
|
@@ -183,6 +185,8 @@ interface RouterConfig {
|
|
|
183
185
|
secretKey: string;
|
|
184
186
|
currency: string;
|
|
185
187
|
recipient?: string;
|
|
188
|
+
/** Tempo RPC URL for on-chain verification. Falls back to TEMPO_RPC_URL env var. */
|
|
189
|
+
rpcUrl?: string;
|
|
186
190
|
};
|
|
187
191
|
/**
|
|
188
192
|
* Payment protocols to accept on auto-priced routes (those using the `prices` config).
|
|
@@ -239,6 +243,7 @@ interface OrchestrateDeps {
|
|
|
239
243
|
secretKey: string;
|
|
240
244
|
currency: string;
|
|
241
245
|
recipient?: string;
|
|
246
|
+
rpcUrl?: string;
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { ZodType } from 'zod';
|
|
3
|
+
import { PaymentRequirements, PaymentRequired, SettleResponse } from '@x402/core/types';
|
|
3
4
|
|
|
4
5
|
interface NonceStore {
|
|
5
6
|
check(nonce: string): Promise<boolean>;
|
|
@@ -82,6 +83,7 @@ interface AlertEvent {
|
|
|
82
83
|
meta?: Record<string, unknown>;
|
|
83
84
|
}
|
|
84
85
|
type AlertFn = (level: AlertLevel, message: string, meta?: Record<string, unknown>) => void;
|
|
86
|
+
|
|
85
87
|
interface X402Server {
|
|
86
88
|
initialize(): Promise<void>;
|
|
87
89
|
buildPaymentRequirementsFromOptions(options: Array<{
|
|
@@ -91,18 +93,18 @@ interface X402Server {
|
|
|
91
93
|
payTo: string;
|
|
92
94
|
}>, context: {
|
|
93
95
|
request: Request;
|
|
94
|
-
}): Promise<
|
|
95
|
-
createPaymentRequiredResponse(requirements:
|
|
96
|
+
}): Promise<PaymentRequirements[]>;
|
|
97
|
+
createPaymentRequiredResponse(requirements: PaymentRequirements[], resource: {
|
|
96
98
|
url: string;
|
|
97
99
|
method: string;
|
|
98
100
|
description?: string;
|
|
99
|
-
}, error: string | null, extensions?: Record<string, unknown>): Promise<
|
|
100
|
-
findMatchingRequirements(requirements:
|
|
101
|
-
verifyPayment(payload: unknown, requirements:
|
|
101
|
+
}, error: string | null, extensions?: Record<string, unknown>): Promise<PaymentRequired>;
|
|
102
|
+
findMatchingRequirements(requirements: PaymentRequirements[], payload: unknown): PaymentRequirements;
|
|
103
|
+
verifyPayment(payload: unknown, requirements: PaymentRequirements): Promise<{
|
|
102
104
|
isValid: boolean;
|
|
103
105
|
payer?: string;
|
|
104
106
|
}>;
|
|
105
|
-
settlePayment(payload: unknown, requirements:
|
|
107
|
+
settlePayment(payload: unknown, requirements: PaymentRequirements): Promise<SettleResponse>;
|
|
106
108
|
}
|
|
107
109
|
type ProtocolType = 'x402' | 'mpp';
|
|
108
110
|
type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
|
|
@@ -183,6 +185,8 @@ interface RouterConfig {
|
|
|
183
185
|
secretKey: string;
|
|
184
186
|
currency: string;
|
|
185
187
|
recipient?: string;
|
|
188
|
+
/** Tempo RPC URL for on-chain verification. Falls back to TEMPO_RPC_URL env var. */
|
|
189
|
+
rpcUrl?: string;
|
|
186
190
|
};
|
|
187
191
|
/**
|
|
188
192
|
* Payment protocols to accept on auto-priced routes (those using the `prices` config).
|
|
@@ -239,6 +243,7 @@ interface OrchestrateDeps {
|
|
|
239
243
|
secretKey: string;
|
|
240
244
|
currency: string;
|
|
241
245
|
recipient?: string;
|
|
246
|
+
rpcUrl?: string;
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
|
package/dist/index.js
CHANGED
|
@@ -320,63 +320,106 @@ async function settleX402Payment(server, payload, requirements) {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
// src/protocols/mpp.ts
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
mpayLoaded = true;
|
|
338
|
-
} catch {
|
|
339
|
-
throw new Error("mpay package is required for MPP protocol support. Install it: pnpm add mpay");
|
|
323
|
+
import { Challenge, Credential, Receipt } from "mpay";
|
|
324
|
+
import { tempo } from "mpay/server";
|
|
325
|
+
import { createClient, http } from "viem";
|
|
326
|
+
import { tempo as tempoChain } from "viem/chains";
|
|
327
|
+
function buildGetClient(rpcUrl) {
|
|
328
|
+
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
329
|
+
if (!url) return {};
|
|
330
|
+
return {
|
|
331
|
+
getClient: () => createClient({ chain: tempoChain, transport: http(url) })
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function toStandardRequest(request) {
|
|
335
|
+
if (request.constructor.name === "Request") {
|
|
336
|
+
return request;
|
|
340
337
|
}
|
|
338
|
+
return new Request(request.url, {
|
|
339
|
+
method: request.method,
|
|
340
|
+
headers: request.headers,
|
|
341
|
+
body: request.body,
|
|
342
|
+
// @ts-expect-error - Request.duplex is required for streaming bodies but not in types yet
|
|
343
|
+
duplex: "half"
|
|
344
|
+
});
|
|
341
345
|
}
|
|
346
|
+
var DEFAULT_DECIMALS = 6;
|
|
342
347
|
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
343
|
-
|
|
348
|
+
const standardRequest = toStandardRequest(request);
|
|
349
|
+
const currency = mppConfig.currency;
|
|
350
|
+
const recipient = mppConfig.recipient ?? "";
|
|
344
351
|
const methodIntent = tempo.charge({
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
recipient: mppConfig.recipient ?? ""
|
|
352
|
+
currency,
|
|
353
|
+
recipient
|
|
348
354
|
});
|
|
349
355
|
const challenge = Challenge.fromIntent(methodIntent, {
|
|
350
356
|
secretKey: mppConfig.secretKey,
|
|
351
|
-
realm: new URL(
|
|
352
|
-
request
|
|
357
|
+
realm: new URL(standardRequest.url).origin,
|
|
358
|
+
request: {
|
|
359
|
+
amount: price,
|
|
360
|
+
currency,
|
|
361
|
+
recipient,
|
|
362
|
+
decimals: DEFAULT_DECIMALS
|
|
363
|
+
}
|
|
353
364
|
});
|
|
354
365
|
return Challenge.serialize(challenge);
|
|
355
366
|
}
|
|
356
367
|
async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
const standardRequest = toStandardRequest(request);
|
|
369
|
+
const currency = mppConfig.currency;
|
|
370
|
+
const recipient = mppConfig.recipient ?? "";
|
|
371
|
+
try {
|
|
372
|
+
const authHeader = standardRequest.headers.get("Authorization");
|
|
373
|
+
if (!authHeader) {
|
|
374
|
+
console.error("[MPP] No Authorization header found");
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const credential = Credential.fromRequest(standardRequest);
|
|
378
|
+
if (!credential?.challenge) {
|
|
379
|
+
console.error("[MPP] Invalid credential structure");
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const isValid = Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
|
|
383
|
+
if (!isValid) {
|
|
384
|
+
console.error("[MPP] Challenge HMAC verification failed");
|
|
385
|
+
return { valid: false, payer: null };
|
|
386
|
+
}
|
|
387
|
+
const methodIntent = tempo.charge({
|
|
388
|
+
currency,
|
|
389
|
+
recipient,
|
|
390
|
+
...buildGetClient(mppConfig.rpcUrl)
|
|
391
|
+
});
|
|
392
|
+
const paymentRequest = {
|
|
393
|
+
amount: price,
|
|
394
|
+
currency,
|
|
395
|
+
recipient,
|
|
396
|
+
decimals: DEFAULT_DECIMALS
|
|
397
|
+
};
|
|
398
|
+
const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
|
|
399
|
+
const receipt = await methodIntent.verify({
|
|
400
|
+
credential,
|
|
401
|
+
request: resolvedRequest
|
|
402
|
+
});
|
|
403
|
+
if (!receipt || receipt.status !== "success") {
|
|
404
|
+
console.error("[MPP] Tempo verification failed:", receipt);
|
|
405
|
+
return { valid: false, payer: null };
|
|
406
|
+
}
|
|
407
|
+
const payer = receipt.reference ?? "";
|
|
408
|
+
return {
|
|
409
|
+
valid: true,
|
|
410
|
+
payer,
|
|
411
|
+
txHash: receipt.reference
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error("[MPP] Credential verification error:", {
|
|
415
|
+
message: error instanceof Error ? error.message : String(error),
|
|
416
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
417
|
+
errorType: error?.constructor?.name
|
|
418
|
+
});
|
|
419
|
+
return null;
|
|
372
420
|
}
|
|
373
|
-
return {
|
|
374
|
-
valid: true,
|
|
375
|
-
payer: verifyResult.payer
|
|
376
|
-
};
|
|
377
421
|
}
|
|
378
|
-
|
|
379
|
-
await ensureMpay();
|
|
422
|
+
function buildMPPReceipt(reference) {
|
|
380
423
|
const receipt = Receipt.from({
|
|
381
424
|
method: "tempo",
|
|
382
425
|
status: "success",
|
|
@@ -762,7 +805,12 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
762
805
|
}
|
|
763
806
|
}
|
|
764
807
|
async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
765
|
-
const response = new NextResponse2(null, {
|
|
808
|
+
const response = new NextResponse2(null, {
|
|
809
|
+
status: 402,
|
|
810
|
+
headers: {
|
|
811
|
+
"Content-Type": "application/json"
|
|
812
|
+
}
|
|
813
|
+
});
|
|
766
814
|
let challengePrice;
|
|
767
815
|
if (bodyData !== void 0 && typeof routeEntry.pricing === "function") {
|
|
768
816
|
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,8 +19,21 @@
|
|
|
19
19
|
"module": "./dist/index.js",
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"files": [
|
|
22
|
-
"dist"
|
|
22
|
+
"dist",
|
|
23
|
+
".claude/CLAUDE.md",
|
|
24
|
+
".claude/skills"
|
|
23
25
|
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"lint": "eslint src/",
|
|
30
|
+
"lint:fix": "eslint src/ --fix",
|
|
31
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
32
|
+
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm build && pnpm test"
|
|
36
|
+
},
|
|
24
37
|
"peerDependencies": {
|
|
25
38
|
"@coinbase/x402": "^2.1.0",
|
|
26
39
|
"@x402/core": "^2.3.0",
|
|
@@ -31,11 +44,6 @@
|
|
|
31
44
|
"zod": "^4.0.0",
|
|
32
45
|
"zod-openapi": "^5.0.0"
|
|
33
46
|
},
|
|
34
|
-
"peerDependenciesMeta": {
|
|
35
|
-
"mpay": {
|
|
36
|
-
"optional": true
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
47
|
"devDependencies": {
|
|
40
48
|
"@coinbase/x402": "^2.1.0",
|
|
41
49
|
"@eslint/js": "^10.0.1",
|
|
@@ -51,24 +59,15 @@
|
|
|
51
59
|
"tsup": "^8.0.0",
|
|
52
60
|
"typescript": "^5.8.0",
|
|
53
61
|
"typescript-eslint": "^8.55.0",
|
|
62
|
+
"viem": "^2.0.0",
|
|
54
63
|
"vitest": "^3.0.0",
|
|
55
64
|
"zod": "^4.0.0",
|
|
56
65
|
"zod-openapi": "^5.0.0"
|
|
57
66
|
},
|
|
67
|
+
"packageManager": "pnpm@10.28.0",
|
|
58
68
|
"license": "MIT",
|
|
59
69
|
"repository": {
|
|
60
70
|
"type": "git",
|
|
61
71
|
"url": "git+https://github.com/merit-systems/agentcash-router.git"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"build": "tsup",
|
|
65
|
-
"typecheck": "tsc --noEmit",
|
|
66
|
-
"lint": "eslint src/",
|
|
67
|
-
"lint:fix": "eslint src/ --fix",
|
|
68
|
-
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
69
|
-
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
70
|
-
"test": "vitest run",
|
|
71
|
-
"test:watch": "vitest",
|
|
72
|
-
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm build && pnpm test"
|
|
73
72
|
}
|
|
74
|
-
}
|
|
73
|
+
}
|