@cryptonly/x402-fastify 0.0.0-stage → 0.5.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,91 @@
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 Fastify
6
+
7
+ Accept x402 payments in Fastify, 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-fastify @x402/core @x402/fastify @x402/evm fastify
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```ts
28
+ import Fastify from 'fastify';
29
+ import { cryptonlyPayments } from '@cryptonly/x402-fastify';
30
+
31
+ const app = Fastify();
32
+ app.get('/health', async () => 'ok');
33
+
34
+ // Paid routes go in the plugin's scope.
35
+ await app.register(async (paid) => {
36
+ await paid.register(cryptonlyPayments, { apiKey: process.env.CRYPTONLY_API_KEY! });
37
+
38
+ // Priced in the dashboard as GET /weather/:city
39
+ paid.get('/weather/:city', async (request) => {
40
+ const { city } = request.params as { city: string };
41
+ return { city, temperatureC: 21 };
42
+ });
43
+
44
+ await paid.cryptonlyX402.ready(); // fails at startup if the prices can't load
45
+ });
46
+
47
+ await app.listen({ port: 4021 });
48
+ ```
49
+
50
+ `cryptonlyPayments` gates every route in the scope it is registered in. Register it at the root to cover the whole app. Routes the dashboard doesn't price pass through.
51
+
52
+ It reloads your prices every minute and fails closed, so a paid route is never served for free:
53
+
54
+ - 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.
55
+ - If a later reload fails, it keeps the last good prices and logs a warning.
56
+
57
+ The buyer is charged only when the handler succeeds. A `4xx`/`5xx` reply or a thrown error cancels the payment.
58
+
59
+ ## Options
60
+
61
+ | Option | Default | |
62
+ | --- | --- | --- |
63
+ | `apiKey` | required | Your Cryptonly API key. |
64
+ | `sandbox` | `false` | Use the sandbox. |
65
+ | `baseUrl` | production | Another API origin. Takes precedence over `sandbox`. |
66
+ | `refreshIntervalMs` | `60000` | How often to reload prices. `0` turns it off. |
67
+ | `retryAfterMs` | `5000` | After a failed first load, how long to answer `503` before the next try. |
68
+ | `schemes` | `[new ExactEvmScheme()]` | Schemes for Base. |
69
+ | `svmSchemes` | `[new ExactSvmScheme()]` if `@x402/svm` is installed | Schemes for Solana. |
70
+ | `logger` | `console` | Gets reload failures and skipped routes. |
71
+ | `paywallConfig` | — | Passed to the stock `@x402/fastify` hooks. |
72
+
73
+ `app.cryptonlyX402` has `ready()`, `refresh()`, `routes()` and `close()`. `app.close()` stops the refresh.
74
+
75
+ ## Solana
76
+
77
+ Install `@x402/svm` and `@solana/kit`, and add a Solana payout wallet in the dashboard. Solana prices then work with no other change.
78
+
79
+ ## Sandbox
80
+
81
+ 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.
82
+
83
+ ## Learn more
84
+
85
+ - [x402 quickstart](https://docs.cryptonly.net/guides/x402/quickstart/)
86
+ - [Discovery](https://docs.cryptonly.net/guides/x402/discovery/): list your endpoints for AI agents
87
+ - [Reference](https://docs.cryptonly.net/guides/x402/reference/): key scopes, limits and reason codes
88
+
89
+ ## License
90
+
91
+ [MIT](https://github.com/cryptonly-net/x402/blob/main/LICENSE)
@@ -0,0 +1,56 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+ import { type RoutesConfig } from '@x402/core/server';
3
+ import { type PaywallConfig } from '@x402/fastify';
4
+ import { type X402RouteSourceOptions } from '@cryptonly/x402';
5
+ /** Options for {@link cryptonlyPayments}. */
6
+ export type CryptonlyPaymentsOptions = X402RouteSourceOptions & {
7
+ /** Passed to the stock `@x402/fastify` hooks for the browser paywall. */
8
+ paywallConfig?: PaywallConfig;
9
+ };
10
+ /** What {@link cryptonlyPayments} decorates the instance with, as `app.cryptonlyX402`. */
11
+ export type CryptonlyX402 = {
12
+ /**
13
+ * Loads the configuration now (if not loaded yet) and starts refreshing.
14
+ * Rejects when the first load fails. Await it before `listen` to fail
15
+ * fast at startup.
16
+ */
17
+ ready(): Promise<void>;
18
+ /** Reloads the configuration now. Resolves `true` when the routes changed. */
19
+ refresh(): Promise<boolean>;
20
+ /** The routes in use, or `undefined` before the first successful load. */
21
+ routes(): RoutesConfig | undefined;
22
+ /** Stops refreshing. The last routes stay in use. Called on `app.close()`. */
23
+ close(): void;
24
+ };
25
+ declare module 'fastify' {
26
+ interface FastifyInstance {
27
+ cryptonlyX402: CryptonlyX402;
28
+ }
29
+ }
30
+ /**
31
+ * Fastify plugin whose paid routes and prices come from the Cryptonly
32
+ * dashboard (`GET /x402/config`), refreshed every minute, so a price change
33
+ * reaches production without a deploy.
34
+ *
35
+ * - The configuration loads on the first request (or on
36
+ * `app.cryptonlyX402.ready()`), once.
37
+ * - Each refresh rebuilds the stock `@x402/fastify` hooks only when the
38
+ * routes changed. A failed refresh keeps the last good routes.
39
+ * - Until a first load succeeds, every request in its scope answers 503
40
+ * (fail closed): paid endpoints are never served for free. Register it in
41
+ * the scope of the paid routes, or at the root to cover every route.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import Fastify from 'fastify';
46
+ * import { cryptonlyPayments } from '@cryptonly/x402-fastify';
47
+ *
48
+ * const app = Fastify();
49
+ * await app.register(cryptonlyPayments, { apiKey: process.env.CRYPTONLY_API_KEY! });
50
+ * app.get('/weather/:city', async (request) => ({ temperatureC: 21 }));
51
+ * ```
52
+ */
53
+ export declare const cryptonlyPayments: FastifyPluginAsync<CryptonlyPaymentsOptions>;
54
+ export { fetchX402Config, routesFromConfig } from '@cryptonly/x402';
55
+ export type { X402Config } from '@cryptonly/x402';
56
+ //# 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,EAEV,kBAAkB,EAGnB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAAmC,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EAKL,KAAK,sBAAsB,EAC5B,MAAM,iBAAiB,CAAC;AAEzB,6CAA6C;AAC7C,MAAM,MAAM,wBAAwB,GAAG,sBAAsB,GAAG;IAC9D,yEAAyE;IACzE,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AAEF,0FAA0F;AAC1F,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;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,8EAA8E;IAC9E,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEF,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,aAAa,EAAE,aAAa,CAAC;KAC9B;CACF;AAoFD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,iBAAiB,8CAG5B,CAAC;AAEH,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,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.routesFromConfig = exports.fetchX402Config = exports.cryptonlyPayments = void 0;
7
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
8
+ const server_1 = require("@x402/core/server");
9
+ const fastify_1 = require("@x402/fastify");
10
+ const x402_1 = require("@cryptonly/x402");
11
+ const HOOKS = Symbol('cryptonlyX402Hooks');
12
+ /**
13
+ * Runs the stock `paymentMiddlewareFromHTTPServer` against a stand-in
14
+ * instance and keeps the hooks it adds. They keep their per-request state on
15
+ * the request, so the real hooks can hand each request to the set in use.
16
+ */
17
+ function stockHooks(httpServer, paywallConfig) {
18
+ const hooks = {};
19
+ const capture = {
20
+ decorateRequest: () => capture,
21
+ addHook: (name, hook) => {
22
+ hooks[name] = hook;
23
+ return capture;
24
+ },
25
+ };
26
+ (0, fastify_1.paymentMiddlewareFromHTTPServer)(capture, httpServer, paywallConfig);
27
+ if (!hooks.onRequest || !hooks.onSend || !hooks.onError) {
28
+ throw new Error('@x402/fastify did not register its onRequest, onSend and onError hooks');
29
+ }
30
+ return hooks;
31
+ }
32
+ const plugin = async (app, options) => {
33
+ const source = (0, x402_1.createX402RouteSource)(options, (routes, config) => stockHooks(new server_1.x402HTTPResourceServer((0, x402_1.createResourceServer)(config, source.facilitator, source.schemes, source.svmSchemes), routes), options.paywallConfig));
34
+ // The stock hooks' per-request state, declared once on the real instance.
35
+ app.decorateRequest('x402Context', undefined);
36
+ app.decorateRequest('x402RawGuard', undefined);
37
+ app.decorate('cryptonlyX402', {
38
+ ready: source.ready,
39
+ refresh: source.refresh,
40
+ routes: () => source.current()?.routes,
41
+ close: source.close,
42
+ });
43
+ app.addHook('onRequest', async (request, reply) => {
44
+ if (!source.current()) {
45
+ try {
46
+ await source.ready();
47
+ }
48
+ catch {
49
+ reply.header('Retry-After', String(Math.ceil(source.retryAfterMs / 1000)));
50
+ return reply.status(503).send(x402_1.X402_CONFIG_UNAVAILABLE);
51
+ }
52
+ }
53
+ // Pinned so that a refresh mid-request can't mix two route sets.
54
+ const hooks = source.current().value;
55
+ request[HOOKS] = hooks;
56
+ return hooks.onRequest(request, reply);
57
+ });
58
+ app.addHook('onSend', async (request, reply, payload) => {
59
+ const hooks = request[HOOKS];
60
+ return hooks ? hooks.onSend(request, reply, payload) : payload;
61
+ });
62
+ app.addHook('onError', async (request, reply, error) => {
63
+ await request[HOOKS]?.onError(request, reply, error);
64
+ });
65
+ app.addHook('onClose', async () => {
66
+ source.close();
67
+ });
68
+ };
69
+ /**
70
+ * Fastify plugin whose paid routes and prices come from the Cryptonly
71
+ * dashboard (`GET /x402/config`), refreshed every minute, so a price change
72
+ * reaches production without a deploy.
73
+ *
74
+ * - The configuration loads on the first request (or on
75
+ * `app.cryptonlyX402.ready()`), once.
76
+ * - Each refresh rebuilds the stock `@x402/fastify` hooks only when the
77
+ * routes changed. A failed refresh keeps the last good routes.
78
+ * - Until a first load succeeds, every request in its scope answers 503
79
+ * (fail closed): paid endpoints are never served for free. Register it in
80
+ * the scope of the paid routes, or at the root to cover every route.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import Fastify from 'fastify';
85
+ * import { cryptonlyPayments } from '@cryptonly/x402-fastify';
86
+ *
87
+ * const app = Fastify();
88
+ * await app.register(cryptonlyPayments, { apiKey: process.env.CRYPTONLY_API_KEY! });
89
+ * app.get('/weather/:city', async (request) => ({ temperatureC: 21 }));
90
+ * ```
91
+ */
92
+ exports.cryptonlyPayments = (0, fastify_plugin_1.default)(plugin, {
93
+ fastify: '5.x',
94
+ name: '@cryptonly/x402-fastify',
95
+ });
96
+ var x402_2 = require("@cryptonly/x402");
97
+ Object.defineProperty(exports, "fetchX402Config", { enumerable: true, get: function () { return x402_2.fetchX402Config; } });
98
+ Object.defineProperty(exports, "routesFromConfig", { enumerable: true, get: function () { return x402_2.routesFromConfig; } });
99
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
2
  "name": "@cryptonly/x402-fastify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "Fastify plugin 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-fastify"
16
+ },
17
+ "homepage": "https://github.com/cryptonly-net/x402/tree/main/typescript/packages/x402-fastify#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/cryptonly-net/x402/issues"
20
+ },
21
+ "keywords": [
22
+ "cryptonly",
23
+ "x402",
24
+ "facilitator",
25
+ "payments",
26
+ "fastify",
27
+ "plugin"
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.5.0",
55
+ "fastify-plugin": "^5.1.0"
56
+ },
57
+ "peerDependencies": {
58
+ "@x402/core": "^2",
59
+ "@x402/evm": "^2",
60
+ "@x402/fastify": "^2",
61
+ "@x402/extensions": "^2",
62
+ "@x402/svm": "^2",
63
+ "fastify": "^5"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "@x402/extensions": {
67
+ "optional": true
68
+ },
69
+ "@x402/svm": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "@x402/core": "^2.28.0",
75
+ "@x402/evm": "^2.28.0",
76
+ "@x402/extensions": "^2.28.0",
77
+ "@x402/svm": "^2.28.0",
78
+ "@x402/fastify": "^2.28.0",
79
+ "fastify": "^5.12.7"
80
+ }
81
+ }