@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 +117 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +122 -0
- package/package.json +43 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|