@cryptonly/x402-express 0.0.0-stage → 0.6.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 CHANGED
@@ -1,3 +1,88 @@
1
- # Temporary Holding Version
1
+ <img src="https://raw.githubusercontent.com/cryptonly-net/.github/main/profile/assets/header-banner.svg" alt="Cryptonly" width="100%" />
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <div align="center">
4
+
5
+ # Cryptonly x402 for Express
6
+
7
+ Accept x402 payments in Express, with Cryptonly as the facilitator.
8
+
9
+ [Website](https://cryptonly.net) · [Documentation](https://docs.cryptonly.net/guides/x402/) · [Contact](https://cryptonly.net/contact/)
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ Cryptonly verifies and settles each payment, and pays the network fee. Payments go straight to your wallet. Your prices live in the Cryptonly dashboard, so a price change needs no deploy.
16
+
17
+ You need a Cryptonly API key with the `x402.verify` and `x402.settle` scopes. The [quickstart](https://docs.cryptonly.net/guides/x402/quickstart/) covers getting one.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @cryptonly/x402-express @x402/core @x402/express @x402/evm express
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```ts
28
+ import express from 'express';
29
+ import { cryptonlyPaymentMiddleware } from '@cryptonly/x402-express';
30
+
31
+ const app = express();
32
+ app.get('/health', (_req, res) => res.send('ok')); // free routes go first
33
+
34
+ const payments = cryptonlyPaymentMiddleware({ apiKey: process.env.CRYPTONLY_API_KEY! });
35
+ app.use(payments);
36
+
37
+ // Priced in the dashboard as GET /weather/:city
38
+ app.get('/weather/:city', (req, res) => {
39
+ res.json({ city: req.params.city, temperatureC: 21 });
40
+ });
41
+
42
+ await payments.ready(); // fails at startup if the prices can't load
43
+ app.listen(4021);
44
+ ```
45
+
46
+ It reloads your prices every minute and fails closed, so a paid route is never served for free:
47
+
48
+ - Until the first load succeeds, every request through it gets `503` with `Retry-After`. This happens with a bad key, an inactive account or no payout wallet.
49
+ - If a later reload fails, it keeps the last good prices and logs a warning.
50
+
51
+ The buyer is charged only when the handler succeeds. A `4xx`/`5xx` response or a thrown error cancels the payment.
52
+
53
+ ## Options
54
+
55
+ | Option | Default | |
56
+ | --- | --- | --- |
57
+ | `apiKey` | required | Your Cryptonly API key. |
58
+ | `sandbox` | `false` | Use the sandbox. |
59
+ | `baseUrl` | production | Another API origin. Takes precedence over `sandbox`. |
60
+ | `refreshIntervalMs` | `60000` | How often to reload prices. `0` turns it off. |
61
+ | `retryAfterMs` | `5000` | After a failed first load, how long to answer `503` before the next try. |
62
+ | `schemes` | `[new ExactEvmScheme()]` | Schemes for Base. |
63
+ | `svmSchemes` | `[new ExactSvmScheme()]` if `@x402/svm` is installed | Schemes for Solana. |
64
+ | `logger` | `console` | Gets reload failures and skipped routes. |
65
+ | `paywallConfig` | — | Passed to the stock `paymentMiddleware`. |
66
+ | `createMiddleware` | stock `paymentMiddleware` | Builds the inner middleware, for example to add hooks. |
67
+
68
+ The middleware also has `ready()`, `refresh()`, `routes()` and `close()`.
69
+
70
+ On NestJS, use [`@cryptonly/x402-nestjs`](https://www.npmjs.com/package/@cryptonly/x402-nestjs) for decorators. This middleware also works there, applied with `consumer.apply(payments).forRoutes(...)`.
71
+
72
+ ## Solana
73
+
74
+ Install `@x402/svm` and `@solana/kit`, and add a Solana payout wallet in the dashboard. Solana prices then work with no other change.
75
+
76
+ ## Sandbox
77
+
78
+ Pass `sandbox: true` and use a key from your [sandbox account](https://docs.cryptonly.net/guides/sandbox-environment/). The sandbox settles on Base Sepolia (`eip155:84532`) and Solana Devnet.
79
+
80
+ ## Learn more
81
+
82
+ - [x402 quickstart](https://docs.cryptonly.net/guides/x402/quickstart/)
83
+ - [Discovery](https://docs.cryptonly.net/guides/x402/discovery/): list your endpoints for AI agents
84
+ - [Reference](https://docs.cryptonly.net/guides/x402/reference/): key scopes, limits and reason codes
85
+
86
+ ## License
87
+
88
+ [MIT](https://github.com/cryptonly-net/x402/blob/main/LICENSE)
@@ -0,0 +1,73 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ import type { RoutesConfig } from '@x402/core/server';
3
+ import type { SchemeNetworkServer } from '@x402/core/types';
4
+ import { type PaywallConfig } from '@x402/express';
5
+ import { type CryptonlyFacilitatorClient, type X402Config, type X402Logger, type X402RouteSourceOptions } from '@cryptonly/x402';
6
+ /** An Express middleware. */
7
+ export type ExpressMiddleware = (req: Request, res: Response, next: NextFunction) => void | Promise<void>;
8
+ /** What {@link InnerMiddlewareFactory} gets besides the routes. */
9
+ export type InnerMiddlewareContext = {
10
+ config: X402Config;
11
+ facilitator: CryptonlyFacilitatorClient;
12
+ /** Registered on the account's EVM network. */
13
+ schemes: readonly SchemeNetworkServer[];
14
+ /** Registered on each Solana network of the account. */
15
+ svmSchemes: readonly SchemeNetworkServer[];
16
+ paywallConfig?: PaywallConfig;
17
+ };
18
+ /**
19
+ * Builds the payment middleware for one set of routes. The default is the
20
+ * stock `paymentMiddleware` from `@x402/express` over a fresh
21
+ * `x402ResourceServer`; replace it to add hooks, or in tests.
22
+ */
23
+ export type InnerMiddlewareFactory = (routes: RoutesConfig, context: InnerMiddlewareContext) => ExpressMiddleware;
24
+ /** Logger used for refresh failures and skipped resources. */
25
+ export type CryptonlyMiddlewareLogger = X402Logger;
26
+ /** Options for {@link cryptonlyPaymentMiddleware}. */
27
+ export type CryptonlyPaymentMiddlewareOptions = X402RouteSourceOptions & {
28
+ /** Passed to the stock `paymentMiddleware` for the browser paywall. */
29
+ paywallConfig?: PaywallConfig;
30
+ /** Builds the inner middleware. See {@link InnerMiddlewareFactory}. */
31
+ createMiddleware?: InnerMiddlewareFactory;
32
+ };
33
+ /** The middleware returned by {@link cryptonlyPaymentMiddleware}. */
34
+ export type CryptonlyPaymentMiddleware = ((req: Request, res: Response, next: NextFunction) => Promise<void>) & {
35
+ /**
36
+ * Loads the configuration now (if not loaded yet) and starts refreshing.
37
+ * Rejects when the first load fails. Await it before `listen` to fail
38
+ * fast at startup.
39
+ */
40
+ ready(): Promise<void>;
41
+ /** Reloads the configuration now. Resolves `true` when the routes changed. */
42
+ refresh(): Promise<boolean>;
43
+ /** The routes in use, or `undefined` before the first successful load. */
44
+ routes(): RoutesConfig | undefined;
45
+ /** Stops refreshing. The last routes stay in use. */
46
+ close(): void;
47
+ };
48
+ /** The default {@link InnerMiddlewareFactory}: stock `@x402/express`. */
49
+ export declare const stockPaymentMiddleware: InnerMiddlewareFactory;
50
+ /**
51
+ * Express middleware whose paid routes and prices come from the Cryptonly
52
+ * dashboard (`GET /x402/config`), refreshed every minute, so a price change
53
+ * reaches production without a deploy.
54
+ *
55
+ * - The configuration loads on the first request (or on `ready()`), once.
56
+ * - Each refresh rebuilds the stock `@x402/express` middleware only when the
57
+ * routes changed. A failed refresh keeps the last good routes.
58
+ * - Until a first load succeeds, every request through it answers 503
59
+ * (fail closed): paid endpoints are never served for free. Mount it after
60
+ * health checks and other free routes.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { cryptonlyPaymentMiddleware } from '@cryptonly/x402-express';
65
+ *
66
+ * app.use(cryptonlyPaymentMiddleware({ apiKey: process.env.CRYPTONLY_API_KEY! }));
67
+ * app.get('/weather', (req, res) => res.json({ temperatureC: 21 }));
68
+ * ```
69
+ */
70
+ export declare function cryptonlyPaymentMiddleware(options: CryptonlyPaymentMiddlewareOptions): CryptonlyPaymentMiddleware;
71
+ export { fetchX402Config, routesFromConfig } from '@cryptonly/x402';
72
+ export type { X402Config } from '@cryptonly/x402';
73
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,UAAU,EAEf,KAAK,sBAAsB,EAC5B,MAAM,iBAAiB,CAAC;AAEzB,6BAA6B;AAC7B,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,KACf,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,mEAAmE;AACnE,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,0BAA0B,CAAC;IACxC,+CAA+C;IAC/C,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC,wDAAwD;IACxD,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3C,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,CACnC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,sBAAsB,KAC5B,iBAAiB,CAAC;AAEvB,8DAA8D;AAC9D,MAAM,MAAM,yBAAyB,GAAG,UAAU,CAAC;AAEnD,sDAAsD;AACtD,MAAM,MAAM,iCAAiC,GAAG,sBAAsB,GAAG;IACvE,uEAAuE;IACvE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C,CAAC;AAEF,qEAAqE;AACrE,MAAM,MAAM,0BAA0B,GAAG,CAAC,CACxC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,KACf,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG;IACpB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,8EAA8E;IAC9E,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,0EAA0E;IAC1E,MAAM,IAAI,YAAY,GAAG,SAAS,CAAC;IACnC,qDAAqD;IACrD,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,EAAE,sBAQlC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAmC5B;AAED,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routesFromConfig = exports.fetchX402Config = exports.stockPaymentMiddleware = void 0;
4
+ exports.cryptonlyPaymentMiddleware = cryptonlyPaymentMiddleware;
5
+ const express_1 = require("@x402/express");
6
+ const x402_1 = require("@cryptonly/x402");
7
+ /** The default {@link InnerMiddlewareFactory}: stock `@x402/express`. */
8
+ const stockPaymentMiddleware = (routes, { config, facilitator, schemes, svmSchemes, paywallConfig }) => (0, express_1.paymentMiddleware)(routes, (0, x402_1.createResourceServer)(config, facilitator, schemes, svmSchemes), paywallConfig);
9
+ exports.stockPaymentMiddleware = stockPaymentMiddleware;
10
+ /**
11
+ * Express middleware whose paid routes and prices come from the Cryptonly
12
+ * dashboard (`GET /x402/config`), refreshed every minute, so a price change
13
+ * reaches production without a deploy.
14
+ *
15
+ * - The configuration loads on the first request (or on `ready()`), once.
16
+ * - Each refresh rebuilds the stock `@x402/express` middleware only when the
17
+ * routes changed. A failed refresh keeps the last good routes.
18
+ * - Until a first load succeeds, every request through it answers 503
19
+ * (fail closed): paid endpoints are never served for free. Mount it after
20
+ * health checks and other free routes.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { cryptonlyPaymentMiddleware } from '@cryptonly/x402-express';
25
+ *
26
+ * app.use(cryptonlyPaymentMiddleware({ apiKey: process.env.CRYPTONLY_API_KEY! }));
27
+ * app.get('/weather', (req, res) => res.json({ temperatureC: 21 }));
28
+ * ```
29
+ */
30
+ function cryptonlyPaymentMiddleware(options) {
31
+ const createMiddleware = options.createMiddleware ?? exports.stockPaymentMiddleware;
32
+ const source = (0, x402_1.createX402RouteSource)(options, (routes, config) => createMiddleware(routes, {
33
+ config,
34
+ facilitator: source.facilitator,
35
+ schemes: source.schemes,
36
+ svmSchemes: source.svmSchemes,
37
+ ...(options.paywallConfig ? { paywallConfig: options.paywallConfig } : {}),
38
+ }));
39
+ const middleware = async (req, res, next) => {
40
+ if (!source.current()) {
41
+ try {
42
+ await source.ready();
43
+ }
44
+ catch {
45
+ if (!res.headersSent) {
46
+ res.setHeader('Retry-After', String(Math.ceil(source.retryAfterMs / 1000)));
47
+ res.status(503).json(x402_1.X402_CONFIG_UNAVAILABLE);
48
+ }
49
+ return;
50
+ }
51
+ }
52
+ await source.current().value(req, res, next);
53
+ };
54
+ return Object.assign(middleware, {
55
+ ready: source.ready,
56
+ refresh: source.refresh,
57
+ routes: () => source.current()?.routes,
58
+ close: source.close,
59
+ });
60
+ }
61
+ var x402_2 = require("@cryptonly/x402");
62
+ Object.defineProperty(exports, "fetchX402Config", { enumerable: true, get: function () { return x402_2.fetchX402Config; } });
63
+ Object.defineProperty(exports, "routesFromConfig", { enumerable: true, get: function () { return x402_2.routesFromConfig; } });
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAyGA,gEAqCC;AA3ID,2CAAsE;AACtE,0CASyB;AA6DzB,yEAAyE;AAClE,MAAM,sBAAsB,GAA2B,CAC5D,MAAM,EACN,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,EAC3D,EAAE,CACF,IAAA,2BAAiB,EACf,MAAM,EACN,IAAA,2BAAoB,EAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,CAAC,EAC9D,aAAa,CACd,CAAC;AARS,QAAA,sBAAsB,0BAQ/B;AAEJ;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,0BAA0B,CACxC,OAA0C;IAE1C,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,8BAAsB,CAAC;IAC5E,MAAM,MAAM,GAAuC,IAAA,4BAAqB,EACtE,OAAO,EACP,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CACjB,gBAAgB,CAAC,MAAM,EAAE;QACvB,MAAM;QACN,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC,CACL,CAAC;IAEF,MAAM,UAAU,GAAG,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC3E,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YACtB,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;oBACrB,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;oBAC5E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,8BAAuB,CAAC,CAAC;gBAChD,CAAC;gBACD,OAAO;YACT,CAAC;QACH,CAAC;QACD,MAAM,MAAM,CAAC,OAAO,EAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAChD,CAAC,CAAC;IAEF,OAAO,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE;QAC/B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,MAAM;QACtC,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC,CAAC;AACL,CAAC;AAED,wCAAoE;AAA3D,uGAAA,eAAe,OAAA;AAAE,wGAAA,gBAAgB,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
2
  "name": "@cryptonly/x402-express",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.0",
4
+ "description": "Express middleware for x402 payments with Cryptonly as the facilitator; prices come from the Cryptonly dashboard.",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "type": "commonjs",
8
+ "sideEffects": false,
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/cryptonly-net/x402.git",
15
+ "directory": "typescript/packages/x402-express"
16
+ },
17
+ "homepage": "https://github.com/cryptonly-net/x402/tree/main/typescript/packages/x402-express#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/cryptonly-net/x402/issues"
20
+ },
21
+ "keywords": [
22
+ "cryptonly",
23
+ "x402",
24
+ "facilitator",
25
+ "payments",
26
+ "express",
27
+ "middleware"
28
+ ],
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.js",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json",
45
+ "clean": "rm -rf dist",
46
+ "prepublishOnly": "npm run build",
47
+ "test": "jest",
48
+ "typecheck": "tsc -p tsconfig.json"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@cryptonly/x402": "0.6.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@x402/core": "^2",
58
+ "@x402/evm": "^2",
59
+ "@x402/express": "^2",
60
+ "@x402/extensions": "^2",
61
+ "@x402/svm": "^2",
62
+ "express": "^4 || ^5"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@x402/extensions": {
66
+ "optional": true
67
+ },
68
+ "@x402/svm": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@x402/core": "^2.28.0",
74
+ "@x402/evm": "^2.28.0",
75
+ "@x402/extensions": "^2.28.0",
76
+ "@x402/svm": "^2.28.0",
77
+ "@x402/express": "^2.28.0",
78
+ "@types/express": "^5.0.6",
79
+ "express": "^5.3.0",
80
+ "@nestjs/common": "11.1.11",
81
+ "@nestjs/core": "11.1.11",
82
+ "@nestjs/platform-express": "11.1.11",
83
+ "reflect-metadata": "0.2.2",
84
+ "rxjs": "7.8.2"
85
+ }
86
+ }