@billmyagent/x402-express 1.0.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/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @billmyagent/x402-express
2
+
3
+ Charge for an Express API with [x402](https://x402.org), settled and reconciled through BillMyAgent.
4
+
5
+ ```bash
6
+ npm install @billmyagent/x402-express express
7
+ ```
8
+
9
+ ```js
10
+ import express from 'express';
11
+ import { paymentMiddleware } from '@billmyagent/x402-express';
12
+
13
+ const app = express();
14
+
15
+ app.use(
16
+ await paymentMiddleware({
17
+ apiKey: process.env.BILLMYAGENT_API_KEY,
18
+ network: 'base',
19
+ routes: {
20
+ 'GET /premium': { price: '$0.01', config: { description: 'Premium data' } },
21
+ },
22
+ })
23
+ );
24
+
25
+ app.get('/premium', (_req, res) => res.json({ ok: true }));
26
+ app.listen(4021);
27
+ ```
28
+
29
+ `GET /premium` now answers `402 Payment Required` until a caller presents a valid
30
+ x402 payment. Any x402 client pays it automatically —
31
+ [`@billmyagent/payments-core`](../core), `x402-axios`, `x402-fetch`, or an agent
32
+ framework that speaks the protocol.
33
+
34
+ ## What this adds over `x402-express`
35
+
36
+ This is a thin wrapper over Coinbase's official middleware. It does two things
37
+ that one cannot.
38
+
39
+ **It routes the payment through BillMyAgent's facilitator**, authenticated with
40
+ your API key. Every payment your API takes is then recorded, checked against the
41
+ chain by the reconciler, and visible in your dashboard. Without it, payments
42
+ happen entirely between the buyer and Coinbase's facilitator and you have no
43
+ record beyond your own logs.
44
+
45
+ **It resolves your payout address from your account** instead of asking you to
46
+ paste it into your server config. `payTo` is an address that decides where money
47
+ goes. A typo in an environment variable sends a real payment to a stranger, and
48
+ it looks like a successful transfer from every side. Reading it from the account
49
+ you registered — the same address the facilitator checks every payment against
50
+ before broadcasting — means the value can only be wrong in one place, where you
51
+ can see and fix it.
52
+
53
+ ## Non-custodial
54
+
55
+ The buyer's signed EIP-3009 authorization transfers USDC to **your** address
56
+ directly. BillMyAgent never holds the funds and never appears in the transaction.
57
+ The platform fee is a separate accrual against a settlement the reconciler has
58
+ independently confirmed on-chain — it is not deducted from the transfer.
59
+
60
+ ## Timing: your handler runs before settlement
61
+
62
+ The x402 middleware verifies the payment, runs your route handler, and settles
63
+ afterwards. So a settlement that fails means you already served that response for
64
+ free.
65
+
66
+ This is how the protocol works and is identical with every facilitator, ours and
67
+ Coinbase's alike. For a cheap endpoint it is noise. For an expensive one, do the
68
+ cheap part in the handler and the expensive part after you have seen the
69
+ `X-PAYMENT-RESPONSE` header.
70
+
71
+ ## Networks
72
+
73
+ `base` and `base-sepolia`, settled in USDC. There is no Ethereum L1
74
+ settlement. Register a payout address for the network you want in the dashboard
75
+ under **Settings → Payouts** before your first paid request.
76
+
77
+ Use `base-sepolia` to develop against free testnet USDC.
78
+
79
+ ## API
80
+
81
+ ### `paymentMiddleware(options)`
82
+
83
+ Returns a `Promise<RequestHandler>`. Await it once at startup — the payout
84
+ address is resolved then, so a misconfigured account fails at deploy rather than
85
+ when a buyer arrives.
86
+
87
+ | Option | Default | Meaning |
88
+ | --- | --- | --- |
89
+ | `apiKey` | required | Your BillMyAgent API key |
90
+ | `routes` | required | `{ 'GET /path': { price: '$0.01' } }`, or `{ '/path': '$0.01' }` |
91
+ | `network` | `'base'` | Default chain for routes that do not name one |
92
+ | `baseUrl` | `https://api.billmyagent.ai` | Gateway to talk to |
93
+ | `payTo` | resolved | Override the payout address and skip the lookup |
94
+ | `paywall` | – | Passed through to the official browser paywall page |
95
+
96
+ ### `facilitator({ apiKey, baseUrl })`
97
+
98
+ The `FacilitatorConfig` on its own, if you would rather call the official
99
+ `paymentMiddleware` yourself and keep your own `payTo`.
100
+
101
+ ### `resolvePayTo({ apiKey, baseUrl, network })`
102
+
103
+ The payout address registered for a network. Throws if none is registered —
104
+ there is no safe fallback for "where should this money go".
105
+
106
+ ## Errors you might see
107
+
108
+ | Message | Cause |
109
+ | --- | --- |
110
+ | `your account has no payout address registered for <network>` | Set one in the dashboard, then restart |
111
+ | `could not reach the facilitator (401 …)` | The API key is wrong, revoked, or its owner's email is unverified |
112
+ | A buyer sees `invalid_exact_evm_payload_recipient_mismatch` | The `payTo` in the request is not your registered address. If you passed `payTo` yourself, that is the mismatch |
113
+ | A buyer sees `duplicate_settlement` | That authorization was already settled. The client should sign a fresh one |
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Charge for your Express API with x402, settled through BillMyAgent.
3
+ *
4
+ * This is a thin wrapper over the official `x402-express` middleware. It does
5
+ * exactly two things the official one cannot:
6
+ *
7
+ * 1. Points the facilitator at BillMyAgent and authenticates with your API
8
+ * key, so the payments your API takes are recorded, reconciled against the
9
+ * chain, and visible in your dashboard.
10
+ * 2. Resolves your payout address from your account instead of asking you to
11
+ * paste it into your server config.
12
+ *
13
+ * The second one matters more than it looks. `payTo` is an address inside a
14
+ * request that decides where money goes; a typo in an environment variable is a
15
+ * payment sent to a stranger, and it looks like a successful transfer from every
16
+ * side. Resolving it from the address you registered — the same one the
17
+ * facilitator checks every payment against before broadcasting — means the
18
+ * value can only be wrong in one place, where you can fix it.
19
+ *
20
+ * Non-custodial: the buyer's signed transfer pays your address directly.
21
+ * BillMyAgent never holds the funds and never appears in the transaction.
22
+ *
23
+ * import express from 'express';
24
+ * import { paymentMiddleware } from '@billmyagent/x402-express';
25
+ *
26
+ * const app = express();
27
+ * app.use(await paymentMiddleware({
28
+ * apiKey: process.env.BILLMYAGENT_API_KEY!,
29
+ * network: 'base',
30
+ * routes: { 'GET /premium': { price: '$0.01' } },
31
+ * }));
32
+ *
33
+ * ONE THING TO KNOW ABOUT TIMING
34
+ *
35
+ * The official middleware runs YOUR handler before it settles. So a settlement
36
+ * that fails means you already served that response for free. That is how the
37
+ * x402 protocol works and is the same with every facilitator, BillMyAgent's and
38
+ * Coinbase's alike. For a cheap endpoint it is noise; for an expensive one,
39
+ * verify first and do the expensive part after.
40
+ */
41
+ import type { RequestHandler } from 'express';
42
+ import type { FacilitatorConfig, Network, PaywallConfig, RouteConfig, RoutesConfig } from 'x402/types';
43
+ /** The production facilitator. Override for a local gateway. */
44
+ export declare const DEFAULT_BASE_URL = "https://api.billmyagent.ai";
45
+ /** Networks BillMyAgent settles. There is deliberately no Ethereum L1. */
46
+ export type SupportedNetwork = 'base' | 'base-sepolia';
47
+ export interface FacilitatorOptions {
48
+ /** Your BillMyAgent API key (the same `X-API-Key` the REST API takes). */
49
+ apiKey: string;
50
+ /** Gateway base URL. Defaults to production. */
51
+ baseUrl?: string;
52
+ }
53
+ /**
54
+ * A `FacilitatorConfig` pointed at BillMyAgent.
55
+ *
56
+ * Exported on its own so you can pass it to the official `paymentMiddleware`
57
+ * directly if you would rather keep your own `payTo` and skip the resolution
58
+ * step. Everything else in this package is built on it.
59
+ */
60
+ export declare function facilitator({ apiKey, baseUrl }: FacilitatorOptions): FacilitatorConfig;
61
+ /**
62
+ * Ask the gateway which payout address it will settle to on a network.
63
+ *
64
+ * This is the address you registered in your BillMyAgent account. The
65
+ * facilitator refuses any payment whose `payTo` does not match it, so reading it
66
+ * from here rather than declaring it locally removes the only way those two can
67
+ * disagree.
68
+ *
69
+ * Throws rather than defaulting: there is no safe fallback for "where should
70
+ * this money go".
71
+ */
72
+ export declare function resolvePayTo(options: FacilitatorOptions & {
73
+ network: SupportedNetwork;
74
+ }): Promise<string>;
75
+ /** A route price, with the network filled in for you. */
76
+ export type BillMyAgentRouteConfig = Omit<RouteConfig, 'network'> & {
77
+ network?: SupportedNetwork;
78
+ };
79
+ export interface PaymentMiddlewareOptions extends FacilitatorOptions {
80
+ /** Which chain to settle on. Default 'base'. */
81
+ network?: SupportedNetwork;
82
+ /**
83
+ * Routes to charge for, in the official shape — `{ 'GET /premium': { price:
84
+ * '$0.01' } }` — except that `network` is optional and defaults to the one
85
+ * above.
86
+ */
87
+ routes: Record<string, BillMyAgentRouteConfig | string>;
88
+ /**
89
+ * Payout address override. Normally omitted: leaving it out resolves the
90
+ * address from your account, which is the whole point of this wrapper. Supply
91
+ * it only when you need the middleware to start without a network call.
92
+ */
93
+ payTo?: string;
94
+ /** Passed through to the official middleware's paywall page. */
95
+ paywall?: PaywallConfig;
96
+ }
97
+ /**
98
+ * Build the Express middleware.
99
+ *
100
+ * Async because it resolves your payout address before serving anything. Doing
101
+ * it at startup rather than on the first paid request means a misconfigured
102
+ * account fails when you deploy, not when a buyer arrives.
103
+ */
104
+ export declare function paymentMiddleware(options: PaymentMiddlewareOptions): Promise<RequestHandler>;
105
+ export type { FacilitatorConfig, Network, PaywallConfig, RouteConfig, RoutesConfig };
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ /**
3
+ * Charge for your Express API with x402, settled through BillMyAgent.
4
+ *
5
+ * This is a thin wrapper over the official `x402-express` middleware. It does
6
+ * exactly two things the official one cannot:
7
+ *
8
+ * 1. Points the facilitator at BillMyAgent and authenticates with your API
9
+ * key, so the payments your API takes are recorded, reconciled against the
10
+ * chain, and visible in your dashboard.
11
+ * 2. Resolves your payout address from your account instead of asking you to
12
+ * paste it into your server config.
13
+ *
14
+ * The second one matters more than it looks. `payTo` is an address inside a
15
+ * request that decides where money goes; a typo in an environment variable is a
16
+ * payment sent to a stranger, and it looks like a successful transfer from every
17
+ * side. Resolving it from the address you registered — the same one the
18
+ * facilitator checks every payment against before broadcasting — means the
19
+ * value can only be wrong in one place, where you can fix it.
20
+ *
21
+ * Non-custodial: the buyer's signed transfer pays your address directly.
22
+ * BillMyAgent never holds the funds and never appears in the transaction.
23
+ *
24
+ * import express from 'express';
25
+ * import { paymentMiddleware } from '@billmyagent/x402-express';
26
+ *
27
+ * const app = express();
28
+ * app.use(await paymentMiddleware({
29
+ * apiKey: process.env.BILLMYAGENT_API_KEY!,
30
+ * network: 'base',
31
+ * routes: { 'GET /premium': { price: '$0.01' } },
32
+ * }));
33
+ *
34
+ * ONE THING TO KNOW ABOUT TIMING
35
+ *
36
+ * The official middleware runs YOUR handler before it settles. So a settlement
37
+ * that fails means you already served that response for free. That is how the
38
+ * x402 protocol works and is the same with every facilitator, BillMyAgent's and
39
+ * Coinbase's alike. For a cheap endpoint it is noise; for an expensive one,
40
+ * verify first and do the expensive part after.
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.DEFAULT_BASE_URL = void 0;
44
+ exports.facilitator = facilitator;
45
+ exports.resolvePayTo = resolvePayTo;
46
+ exports.paymentMiddleware = paymentMiddleware;
47
+ const x402_express_1 = require("x402-express");
48
+ /** The production facilitator. Override for a local gateway. */
49
+ exports.DEFAULT_BASE_URL = 'https://api.billmyagent.ai';
50
+ /**
51
+ * A `FacilitatorConfig` pointed at BillMyAgent.
52
+ *
53
+ * Exported on its own so you can pass it to the official `paymentMiddleware`
54
+ * directly if you would rather keep your own `payTo` and skip the resolution
55
+ * step. Everything else in this package is built on it.
56
+ */
57
+ function facilitator({ apiKey, baseUrl = exports.DEFAULT_BASE_URL }) {
58
+ if (!apiKey) {
59
+ throw new Error('@billmyagent/x402-express: apiKey is required');
60
+ }
61
+ const url = `${baseUrl.replace(/\/+$/, '')}/api/v1/x402`;
62
+ const headers = { 'X-API-Key': apiKey };
63
+ return {
64
+ url: url,
65
+ // Called per request by the official client, which is why this is a
66
+ // function rather than a static header map: it lets a key be rotated
67
+ // without a restart if you close over a mutable value.
68
+ createAuthHeaders: async () => ({
69
+ verify: { ...headers },
70
+ settle: { ...headers },
71
+ supported: { ...headers },
72
+ list: { ...headers },
73
+ }),
74
+ };
75
+ }
76
+ /**
77
+ * Ask the gateway which payout address it will settle to on a network.
78
+ *
79
+ * This is the address you registered in your BillMyAgent account. The
80
+ * facilitator refuses any payment whose `payTo` does not match it, so reading it
81
+ * from here rather than declaring it locally removes the only way those two can
82
+ * disagree.
83
+ *
84
+ * Throws rather than defaulting: there is no safe fallback for "where should
85
+ * this money go".
86
+ */
87
+ async function resolvePayTo(options) {
88
+ const { apiKey, baseUrl = exports.DEFAULT_BASE_URL, network } = options;
89
+ const url = `${baseUrl.replace(/\/+$/, '')}/api/v1/x402/supported`;
90
+ const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
91
+ if (!res.ok) {
92
+ throw new Error(`@billmyagent/x402-express: could not reach the facilitator (${res.status} ${res.statusText}). ` +
93
+ `Check BILLMYAGENT_API_KEY and that ${baseUrl} is correct.`);
94
+ }
95
+ const body = (await res.json());
96
+ const kind = (body.kinds ?? []).find((k) => k.network === network && k.extra?.payTo);
97
+ if (!kind?.extra?.payTo) {
98
+ throw new Error(`@billmyagent/x402-express: your account has no payout address registered for ${network}. ` +
99
+ `Set one in the dashboard under Settings → Payouts, then restart.`);
100
+ }
101
+ return kind.extra.payTo;
102
+ }
103
+ /**
104
+ * Build the Express middleware.
105
+ *
106
+ * Async because it resolves your payout address before serving anything. Doing
107
+ * it at startup rather than on the first paid request means a misconfigured
108
+ * account fails when you deploy, not when a buyer arrives.
109
+ */
110
+ async function paymentMiddleware(options) {
111
+ const network = options.network ?? 'base';
112
+ const payTo = options.payTo ?? (await resolvePayTo({ ...options, network }));
113
+ const routes = {};
114
+ for (const [pattern, value] of Object.entries(options.routes)) {
115
+ const config = typeof value === 'string' ? { price: value } : value;
116
+ // Object.assign rather than a spread into a fresh literal so a route that
117
+ // names its own network keeps it: a merchant charging in two places on two
118
+ // chains is a real configuration, not a mistake to normalise away.
119
+ routes[pattern] = { network, ...config };
120
+ }
121
+ return (0, x402_express_1.paymentMiddleware)(payTo, routes, facilitator(options), options.paywall);
122
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@billmyagent/x402-express",
3
+ "version": "1.0.0",
4
+ "description": "Charge for an Express API with x402, settled and reconciled through BillMyAgent",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": [
14
+ "x402",
15
+ "express",
16
+ "payments",
17
+ "usdc",
18
+ "facilitator"
19
+ ],
20
+ "license": "MIT",
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "type-check": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "x402": "1.2.0",
27
+ "x402-express": "1.2.0"
28
+ },
29
+ "peerDependencies": {
30
+ "express": ">=4.18.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/express": "^5.0.0",
34
+ "@types/node": "^22.10.5",
35
+ "typescript": "^5.7.3"
36
+ },
37
+ "overrides": {
38
+ "axios": "^1.18.0",
39
+ "ws": "^8.21.0",
40
+ "uuid@<11.1.1": "^11.1.1",
41
+ "qs": "^6.16.0"
42
+ }
43
+ }