From 4f1ab93075126244c3e5a28908de16303ca2e765 Mon Sep 17 00:00:00 2001 From: Anthonyushie Date: Mon, 19 Jan 2026 19:26:03 +0100 Subject: [PATCH 1/5] update --- package-lock.json | 68 ++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index a20cb10..e2dec99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,7 +161,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -521,7 +520,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -565,7 +563,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2074,7 +2071,6 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -3333,6 +3329,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3353,6 +3350,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3428,7 +3426,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3547,7 +3546,6 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3558,7 +3556,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3614,7 +3611,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -4769,7 +4765,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5415,7 +5410,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6089,6 +6083,7 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -6139,7 +6134,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/drbg.js": { "version": "1.0.1", @@ -6549,7 +6545,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6735,7 +6730,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7631,6 +7625,21 @@ } } }, + "node_modules/html-encoding-sniffer/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -7663,8 +7672,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/ieee754": { "version": "1.2.1", @@ -8329,6 +8337,21 @@ } } }, + "node_modules/jsdom/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/jsdom/node_modules/tr46": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", @@ -8778,7 +8801,6 @@ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", @@ -8878,6 +8900,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9755,6 +9778,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9770,6 +9794,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9782,7 +9807,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/process-nextick-args": { "version": "2.0.1", @@ -9908,7 +9934,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9918,7 +9943,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11176,7 +11200,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11432,7 +11455,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11813,7 +11835,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11922,7 +11943,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12265,7 +12285,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -12417,7 +12436,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 10778ebd7384bccd767e0d7f07dea77d16d4eb1b Mon Sep 17 00:00:00 2001 From: Anthonyushie Date: Tue, 20 Jan 2026 07:44:00 +0100 Subject: [PATCH 2/5] update --- .gitignore | 1 + package-lock.json | 185 +++++++++++- package.json | 2 + src/app/api/v1/invoices/[id]/route.ts | 64 +++++ src/app/api/v1/invoices/route.ts | 147 ++++++++++ src/app/dashboard/x402-demo/page.tsx | 388 ++++++++++++++++++++++++++ src/lib/x402-client.ts | 109 ++++++++ src/lib/x402.ts | 179 ++++++++++++ 8 files changed, 1061 insertions(+), 14 deletions(-) create mode 100644 src/app/api/v1/invoices/[id]/route.ts create mode 100644 src/app/api/v1/invoices/route.ts create mode 100644 src/app/dashboard/x402-demo/page.tsx create mode 100644 src/lib/x402-client.ts create mode 100644 src/lib/x402.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..cd01f46 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +.env.local # vercel .vercel diff --git a/package-lock.json b/package-lock.json index e2dec99..5df01c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emailjs/browser": "^4.4.1", "@stacks/connect": "^8.2.4", "@types/qrcode": "^1.5.6", + "axios": "^1.13.2", "clsx": "2.1.1", "framer-motion": "12.26.2", "lucide-react": "0.562.0", @@ -22,6 +23,7 @@ "react-dom": "19.2.3", "shadcn-ui": "0.9.5", "tailwind-merge": "3.4.0", + "x402-stacks": "^1.1.1", "zod": "^3.25.76" }, "devDependencies": { @@ -5059,6 +5061,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -5094,6 +5102,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5557,7 +5576,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5724,6 +5742,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6077,6 +6107,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6156,7 +6195,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6353,7 +6391,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6363,7 +6400,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6408,7 +6444,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6421,7 +6456,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7166,6 +7200,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -7182,6 +7236,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/framer-motion": { "version": "12.26.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.26.2.tgz", @@ -7228,7 +7298,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7298,7 +7367,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7323,7 +7391,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "devOptional": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7411,7 +7478,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7500,7 +7566,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7513,7 +7578,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "devOptional": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7556,7 +7620,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8919,7 +8982,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8992,6 +9054,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9841,6 +9924,12 @@ "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12301,6 +12390,74 @@ } } }, + "node_modules/x402-stacks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/x402-stacks/-/x402-stacks-1.1.1.tgz", + "integrity": "sha512-wMWrU8EZjDCoq5LsPVQdD/HNm2bq0aNHySBkK56qeuHlV/BtMxay4oQ9W+GuWBIeEeCXqRsdeWltwj7cvF3k0A==", + "license": "MIT", + "dependencies": { + "@stacks/network": "^6.13.0", + "@stacks/transactions": "^6.13.0", + "axios": "^1.6.0" + }, + "peerDependencies": { + "express": "^4.18.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, + "node_modules/x402-stacks/node_modules/@stacks/common": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", + "license": "MIT", + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "node_modules/x402-stacks/node_modules/@stacks/network": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.17.0.tgz", + "integrity": "sha512-numHbfKjwco/rbkGPOEz8+FcJ2nBnS/tdJ8R422Q70h3SiA9eqk9RjSzB8p4JP8yW1SZvW+eihADHfMpBuZyfw==", + "license": "MIT", + "dependencies": { + "@stacks/common": "^6.16.0", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/x402-stacks/node_modules/@stacks/transactions": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.17.0.tgz", + "integrity": "sha512-FUah2BRgV66ApLcEXGNGhwyFTRXqX5Zco3LpiM3essw8PF0NQlHwwdPgtDko5RfrJl3LhGXXe/30nwsfNnB3+g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.17.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + }, + "node_modules/x402-stacks/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/x402-stacks/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index 8b6f2fa..c4a5d24 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@emailjs/browser": "^4.4.1", "@stacks/connect": "^8.2.4", "@types/qrcode": "^1.5.6", + "axios": "^1.13.2", "clsx": "2.1.1", "framer-motion": "12.26.2", "lucide-react": "0.562.0", @@ -30,6 +31,7 @@ "react-dom": "19.2.3", "shadcn-ui": "0.9.5", "tailwind-merge": "3.4.0", + "x402-stacks": "^1.1.1", "zod": "^3.25.76" }, "devDependencies": { diff --git a/src/app/api/v1/invoices/[id]/route.ts b/src/app/api/v1/invoices/[id]/route.ts new file mode 100644 index 0000000..28b7d2b --- /dev/null +++ b/src/app/api/v1/invoices/[id]/route.ts @@ -0,0 +1,64 @@ +/** + * x402-Protected Invoice Status API + * + * GET /api/v1/invoices/[id] - Get invoice by ID (requires 0.0005 STX payment) + */ + +import { NextRequest, NextResponse } from "next/server"; +import { + x402Middleware, + addPaymentResponseHeaders, + X402_PRICING, +} from "@/lib/x402"; + +interface RouteContext { + params: Promise<{ id: string }>; +} + +export async function GET(request: NextRequest, context: RouteContext) { + const { id } = await context.params; + + // Validate UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(id)) { + return NextResponse.json( + { error: "Invalid invoice ID format", code: "INVALID_ID" }, + { status: 400 }, + ); + } + + // Check for x402 payment + const { response: paymentResponse, txId } = await x402Middleware(request, { + amountSTX: X402_PRICING.GET_INVOICE, + resource: `/api/v1/invoices/${id}`, + }); + + if (paymentResponse) { + return paymentResponse; + } + + // In a real implementation, you would fetch from a database + // For this demo, we return a mock response showing the pattern works + const responseData = { + success: true, + invoice: { + id, + status: "pending", + message: + "Invoice lookup requires database integration. This endpoint demonstrates x402 payment flow.", + }, + payment: { + txId, + amountPaid: `${X402_PRICING.GET_INVOICE} STX`, + }, + }; + + let response = NextResponse.json(responseData); + + if (txId) { + response = addPaymentResponseHeaders(response, txId); + } + + return response; +} diff --git a/src/app/api/v1/invoices/route.ts b/src/app/api/v1/invoices/route.ts new file mode 100644 index 0000000..245fdef --- /dev/null +++ b/src/app/api/v1/invoices/route.ts @@ -0,0 +1,147 @@ +/** + * x402-Protected Invoice API + * + * POST /api/v1/invoices - Create a new invoice (requires 0.001 STX payment) + * + * This endpoint demonstrates the x402 payment protocol: + * 1. Request without X-PAYMENT header → 402 response with payment details + * 2. Client signs STX transaction and retries with X-PAYMENT header + * 3. Server settles payment via facilitator, then creates invoice + */ + +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { + x402Middleware, + addPaymentResponseHeaders, + X402_PRICING, +} from "@/lib/x402"; +import { encodeInvoice, createPaymentUrl } from "@/lib/url-state"; + +// Request body schema +const CreateInvoiceSchema = z.object({ + recipient: z + .string() + .min(10, "Recipient address is too short") + .refine( + (val) => + val.startsWith("ST") || val.startsWith("SP") || val.startsWith("0x"), + { message: "Must be a valid Stacks or Ethereum address" }, + ), + amount: z.string().regex(/^\d+(\.\d{1,6})?$/, "Invalid amount format"), + token: z.string().default("USDC"), + memo: z.string().max(50).optional(), + network: z.enum(["stacks", "ethereum"]).default("stacks"), +}); + +export async function POST(request: NextRequest) { + // Step 1: Check for x402 payment + const { response: paymentResponse, txId } = await x402Middleware(request, { + amountSTX: X402_PRICING.CREATE_INVOICE, + resource: "/api/v1/invoices", + }); + + // If payment required or failed, return the 402 response + if (paymentResponse) { + return paymentResponse; + } + + // Step 2: Payment verified - parse request body + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json( + { error: "Invalid JSON body", code: "INVALID_REQUEST" }, + { status: 400 }, + ); + } + + // Step 3: Validate request body + const validation = CreateInvoiceSchema.safeParse(body); + if (!validation.success) { + return NextResponse.json( + { + error: "Validation failed", + code: "VALIDATION_ERROR", + details: validation.error.issues, + }, + { status: 400 }, + ); + } + + const { recipient, amount, token, memo, network } = validation.data; + + // Step 4: Create the invoice + const invoiceId = crypto.randomUUID(); + const createdAt = new Date().toISOString(); + + // Generate the payment URL + const invoiceData = { + invoiceId, + recipient, + amount, + token, + memo, + network, + createdAt, + }; + + const paymentUrl = createPaymentUrl(invoiceData); + const encodedToken = encodeInvoice(invoiceData); + + // Step 5: Build response + const responseData = { + success: true, + invoice: { + id: invoiceId, + recipient, + amount, + token, + memo, + network, + createdAt, + paymentUrl, + encodedToken, + }, + payment: { + txId, + amountPaid: `${X402_PRICING.CREATE_INVOICE} STX`, + }, + }; + + // Add payment confirmation header + let response = NextResponse.json(responseData, { status: 201 }); + + if (txId) { + response = addPaymentResponseHeaders(response, txId); + } + + return response; +} + +// GET endpoint info +export async function GET() { + return NextResponse.json({ + endpoint: "/api/v1/invoices", + methods: ["POST", "GET"], + description: "x402-protected invoice creation API", + pricing: { + POST: `${X402_PRICING.CREATE_INVOICE} STX per invoice`, + }, + documentation: "https://github.com/x402-stacks/x402-stacks", + example: { + request: { + recipient: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM", + amount: "100.00", + token: "USDC", + memo: "Payment for services", + network: "stacks", + }, + headers: { + "Content-Type": "application/json", + "X-PAYMENT": "", + }, + }, + }); +} diff --git a/src/app/dashboard/x402-demo/page.tsx b/src/app/dashboard/x402-demo/page.tsx new file mode 100644 index 0000000..3bcd897 --- /dev/null +++ b/src/app/dashboard/x402-demo/page.tsx @@ -0,0 +1,388 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { + Zap, + Send, + CheckCircle2, + AlertCircle, + Loader2, + ExternalLink, + Copy, + Code2, + Wallet, +} from "lucide-react"; +import { useWallet } from "@/context/WalletContext"; + +type FlowStep = + | "idle" + | "requesting" + | "payment_required" + | "paying" + | "success" + | "error"; + +interface InvoiceResult { + id: string; + paymentUrl: string; + txId?: string; +} + +export default function X402DemoPage() { + const { stacksConnected, connectStacks } = useWallet(); + + const [flowStep, setFlowStep] = useState("idle"); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + // Form state + const [recipient, setRecipient] = useState( + "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM", + ); + const [amount, setAmount] = useState("100.00"); + const [memo, setMemo] = useState("x402 Demo Invoice"); + + const handleCreateInvoice = async () => { + if (!stacksConnected) { + await connectStacks(); + return; + } + + setFlowStep("requesting"); + setError(null); + setResult(null); + + try { + // Step 1: Make request without payment (will get 402) + const response = await fetch("/api/v1/invoices", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ recipient, amount, token: "USDC", memo }), + }); + + if (response.status === 402) { + setFlowStep("payment_required"); + const paymentDetails = await response.json(); + + // In a real implementation, we would: + // 1. Sign a transaction using @stacks/transactions + // 2. Send it in the X-PAYMENT header + // For demo purposes, we'll show the 402 flow + + // Simulate payment flow visualization + await new Promise((resolve) => setTimeout(resolve, 1500)); + setFlowStep("paying"); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Show what the 402 response looks like + setResult({ + id: "demo-" + crypto.randomUUID().slice(0, 8), + paymentUrl: `/pay?i=demo`, + txId: undefined, + }); + setFlowStep("success"); + + // Store payment details for display + console.log("402 Payment Details:", paymentDetails); + } else if (response.ok) { + const data = await response.json(); + setResult({ + id: data.invoice.id, + paymentUrl: data.invoice.paymentUrl, + txId: data.payment?.txId, + }); + setFlowStep("success"); + } else { + throw new Error("Unexpected response"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + setFlowStep("error"); + } + }; + + const handleCopyUrl = async () => { + if (result?.paymentUrl) { + await navigator.clipboard.writeText( + window.location.origin + result.paymentUrl, + ); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const getStepInfo = () => { + switch (flowStep) { + case "requesting": + return { + icon: Send, + text: "Making API request...", + color: "text-blue-500", + }; + case "payment_required": + return { + icon: Zap, + text: "402 Payment Required!", + color: "text-brand-orange", + }; + case "paying": + return { + icon: Loader2, + text: "Processing STX payment...", + color: "text-yellow-500", + }; + case "success": + return { + icon: CheckCircle2, + text: "Invoice created!", + color: "text-green-500", + }; + case "error": + return { + icon: AlertCircle, + text: "Error occurred", + color: "text-red-500", + }; + default: + return null; + } + }; + + const stepInfo = getStepInfo(); + + return ( + +
+ {/* Header */} + +
+ + x402 Payment Protocol Demo +
+

Pay-per-Invoice API

+

+ Create invoices programmatically by paying 0.001 STX per API call +

+
+ + {/* Main Card */} + + {/* Wallet Status */} + {!stacksConnected && ( +
+
+ + + Connect your Stacks wallet to test x402 payments + +
+
+ )} + + {/* Form */} +
+
+ + setRecipient(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-sidebar border border-border-subtle focus:border-brand-orange focus:outline-none transition-colors" + placeholder="ST1..." + /> +
+
+
+ + setAmount(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-sidebar border border-border-subtle focus:border-brand-orange focus:outline-none transition-colors" + placeholder="100.00" + /> +
+
+ + setMemo(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-sidebar border border-border-subtle focus:border-brand-orange focus:outline-none transition-colors" + placeholder="Payment for..." + /> +
+
+
+ + {/* API Cost Indicator */} +
+ API Call Cost + + 0.001 STX + +
+ + {/* Submit Button */} + + + {/* Flow Status */} + + {stepInfo && ( + +
+ + {stepInfo.text} +
+
+ )} +
+ + {/* Result */} + + {result && flowStep === "success" && ( + +

+ ✅ Invoice Created Successfully! +

+
+
+ Invoice ID + + {result.id.slice(0, 16)}... + +
+ {result.txId && ( +
+ Payment TX + + View on Explorer + + +
+ )} +
+ +
+ )} +
+ + {/* Error */} + {error && ( + + {error} + + )} +
+ + {/* Info Section */} + +

+ + How x402 Works +

+
    +
  1. + + 1 + + Client sends API request without payment +
  2. +
  3. + + 2 + + Server responds with HTTP 402 and payment details +
  4. +
  5. + + 3 + + + Client signs STX transaction and retries with X-PAYMENT header + +
  6. +
  7. + + 4 + + Server settles payment via facilitator, returns data +
  8. +
+
+
+
+ ); +} diff --git a/src/lib/x402-client.ts b/src/lib/x402-client.ts new file mode 100644 index 0000000..9c8b343 --- /dev/null +++ b/src/lib/x402-client.ts @@ -0,0 +1,109 @@ +/** + * x402 Example Client + * + * Demonstrates how to consume x402-protected APIs using the + * withPaymentInterceptor pattern from x402-stacks. + * + * This file serves as documentation and can be used for testing. + */ + +import axios from "axios"; +import { + withPaymentInterceptor, + privateKeyToAccount, + decodeXPaymentResponse, +} from "x402-stacks"; + +/** + * Creates an x402-enabled API client for consuming Inflow's paid API. + * + * @param privateKey - Stacks wallet private key (hex) + * @param baseUrl - API base URL + * @returns Configured axios instance with automatic payment handling + * + * @example + * // Usage in an AI agent or external service: + * const client = createInflowClient(process.env.STACKS_PRIVATE_KEY!, 'https://inflow.app'); + * const invoice = await client.post('/api/v1/invoices', { + * recipient: 'ST1...', + * amount: '100.00', + * token: 'USDC' + * }); + * console.log('Invoice created:', invoice.data.invoice.paymentUrl); + */ +export function createInflowClient(privateKey: string, baseUrl: string) { + const account = privateKeyToAccount(privateKey, "testnet"); + + const client = withPaymentInterceptor( + axios.create({ + baseURL: baseUrl, + headers: { + "Content-Type": "application/json", + }, + }), + account, + ); + + return client; +} + +/** + * Example: Create an invoice using the paid API + */ +export async function createInvoiceExample( + client: ReturnType, + invoiceData: { + recipient: string; + amount: string; + token?: string; + memo?: string; + network?: "stacks" | "ethereum"; + }, +) { + try { + const response = await client.post("/api/v1/invoices", invoiceData); + + // Decode payment confirmation from headers + const paymentInfo = decodeXPaymentResponse( + response.headers["x-payment-response"], + ); + + console.log("✅ Invoice created successfully!"); + console.log("Invoice ID:", response.data.invoice.id); + console.log("Payment URL:", response.data.invoice.paymentUrl); + + if (paymentInfo) { + console.log("Payment TX:", paymentInfo.txId); + } + + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 402) { + console.log("Payment required:", error.response.data); + } + throw error; + } +} + +/** + * Example usage (for documentation): + * + * ```typescript + * import { createInflowClient, createInvoiceExample } from '@/lib/x402-client'; + * + * const client = createInflowClient( + * process.env.STACKS_PRIVATE_KEY!, + * 'http://localhost:3000' + * ); + * + * const result = await createInvoiceExample(client, { + * recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM', + * amount: '50.00', + * token: 'USDC', + * memo: 'API-generated invoice', + * network: 'stacks' + * }); + * + * console.log('Created invoice:', result.invoice.paymentUrl); + * ``` + */ diff --git a/src/lib/x402.ts b/src/lib/x402.ts new file mode 100644 index 0000000..7cc2c66 --- /dev/null +++ b/src/lib/x402.ts @@ -0,0 +1,179 @@ +/** + * x402 Payment Protocol Utilities for Next.js App Router + * + * This module provides middleware and utilities for implementing + * HTTP 402 Payment Required flows using x402-stacks. + */ + +import { NextRequest, NextResponse } from "next/server"; +import { + X402PaymentVerifier, + STXtoMicroSTX, + type X402PaymentRequired, +} from "x402-stacks"; + +// Environment configuration +const X402_RECEIVE_ADDRESS = + process.env.X402_RECEIVE_ADDRESS || + "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"; // Default testnet address +const X402_NETWORK = (process.env.X402_NETWORK || "testnet") as + | "mainnet" + | "testnet"; +const X402_FACILITATOR_URL = + process.env.X402_FACILITATOR_URL || "https://x402-backend-7eby.onrender.com"; + +// Default expiration: 5 minutes +const DEFAULT_EXPIRATION_SECONDS = 300; + +/** + * Configuration for x402 protected endpoints + */ +export interface X402Config { + /** Amount in STX (will be converted to microSTX) */ + amountSTX: number; + /** Optional resource identifier (defaults to request path) */ + resource?: string; + /** Expiration in seconds (default: 300) */ + expirationSeconds?: number; +} + +/** + * Generates a 402 Payment Required response + */ +export function generate402Response( + request: NextRequest, + config: X402Config, +): NextResponse { + const nonce = crypto.randomUUID(); + const expiresAt = new Date( + Date.now() + + (config.expirationSeconds || DEFAULT_EXPIRATION_SECONDS) * 1000, + ).toISOString(); + + const paymentRequired: X402PaymentRequired = { + maxAmountRequired: STXtoMicroSTX(config.amountSTX).toString(), + resource: config.resource || request.nextUrl.pathname, + payTo: X402_RECEIVE_ADDRESS, + network: X402_NETWORK, + nonce, + expiresAt, + tokenType: "STX", + }; + + return NextResponse.json(paymentRequired, { + status: 402, + headers: { + "X-Payment-Required": "true", + "X-Payment-Network": X402_NETWORK, + }, + }); +} + +/** + * Verifies and settles payment from X-PAYMENT header + */ +export async function verifyAndSettlePayment( + request: NextRequest, + config: X402Config, +): Promise<{ + isValid: boolean; + txId?: string; + error?: string; +}> { + const paymentHeader = request.headers.get("X-PAYMENT"); + + if (!paymentHeader) { + return { isValid: false, error: "Missing X-PAYMENT header" }; + } + + try { + const verifier = new X402PaymentVerifier( + X402_FACILITATOR_URL, + X402_NETWORK, + ); + + const result = await verifier.settlePayment(paymentHeader, { + expectedRecipient: X402_RECEIVE_ADDRESS, + minAmount: BigInt(STXtoMicroSTX(config.amountSTX)), + tokenType: "STX", + }); + + if (result.isValid) { + return { isValid: true, txId: result.txId }; + } else { + return { + isValid: false, + error: "Payment verification failed", + }; + } + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return { isValid: false, error: message }; + } +} + +/** + * x402 middleware for Next.js App Router + * + * Returns null if payment is valid, otherwise returns 402 response + */ +export async function x402Middleware( + request: NextRequest, + config: X402Config, +): Promise<{ + response: NextResponse | null; + txId?: string; +}> { + const hasPayment = request.headers.has("X-PAYMENT"); + + if (!hasPayment) { + // No payment provided - return 402 + return { response: generate402Response(request, config) }; + } + + // Payment provided - verify and settle + const result = await verifyAndSettlePayment(request, config); + + if (result.isValid) { + // Payment verified - proceed with request + return { response: null, txId: result.txId }; + } + + // Payment invalid - return error + return { + response: NextResponse.json( + { error: result.error, code: "PAYMENT_FAILED" }, + { status: 402 }, + ), + }; +} + +/** + * Helper to add payment response headers + */ +export function addPaymentResponseHeaders( + response: NextResponse, + txId: string, +): NextResponse { + const paymentResponse = JSON.stringify({ + txId, + status: "confirmed", + network: X402_NETWORK, + }); + + response.headers.set( + "X-PAYMENT-RESPONSE", + Buffer.from(paymentResponse).toString("base64"), + ); + + return response; +} + +/** + * Configuration constants for pricing + */ +export const X402_PRICING = { + CREATE_INVOICE: 0.001, // 0.001 STX to create an invoice + GET_INVOICE: 0.0005, // 0.0005 STX to fetch invoice status + VERIFY_PAYMENT: 0.0002, // 0.0002 STX to verify a payment +} as const; From a18332f4ce226aed0418c48a41bd92e195cf1739 Mon Sep 17 00:00:00 2001 From: Wutche Date: Tue, 20 Jan 2026 11:43:23 +0100 Subject: [PATCH 3/5] fix: use webpack instead of turbopack for dev script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4a5d24..a1786da 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build --webpack", "start": "next start", "lint": "eslint", From ebf3f13b585c365183c556055cefcd738ba80610 Mon Sep 17 00:00:00 2001 From: Wutche Date: Tue, 20 Jan 2026 12:28:49 +0100 Subject: [PATCH 4/5] fix: add x402-stacks and axios to serverExternalPackages --- next.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index 8eddb71..023babf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { // Mark Stacks packages as external during server compilation // to avoid Turbopack module resolution issues with browser-only code - serverExternalPackages: ["@stacks/connect", "@stacks/transactions"], + serverExternalPackages: ["@stacks/connect", "@stacks/transactions", "x402-stacks", "axios"], }; export default nextConfig; From e0e734cd9e15eefbd19f298fa6a208166ae0248b Mon Sep 17 00:00:00 2001 From: Anthonyushie Date: Wed, 21 Jan 2026 13:09:01 +0100 Subject: [PATCH 5/5] fixed x402 and next server issue --- src/app/dashboard/x402-demo/page.tsx | 18 +- src/components/dashboard/SearchModal.tsx | 434 +++++++++++------------ src/context/WalletContext.tsx | 28 +- 3 files changed, 243 insertions(+), 237 deletions(-) diff --git a/src/app/dashboard/x402-demo/page.tsx b/src/app/dashboard/x402-demo/page.tsx index 3bcd897..a282f63 100644 --- a/src/app/dashboard/x402-demo/page.tsx +++ b/src/app/dashboard/x402-demo/page.tsx @@ -77,10 +77,22 @@ export default function X402DemoPage() { setFlowStep("paying"); await new Promise((resolve) => setTimeout(resolve, 2000)); - // Show what the 402 response looks like + // Generate a real invoice URL for the demo (so the link actually works) + const demoInvoiceId = crypto.randomUUID(); + const { createPaymentUrl } = await import("@/lib/url-state"); + const demoPaymentUrl = createPaymentUrl({ + invoiceId: demoInvoiceId, + recipient, + amount, + token: "USDC", + memo, + network: "stacks", + createdAt: new Date().toISOString(), + }); + setResult({ - id: "demo-" + crypto.randomUUID().slice(0, 8), - paymentUrl: `/pay?i=demo`, + id: demoInvoiceId, + paymentUrl: demoPaymentUrl, txId: undefined, }); setFlowStep("success"); diff --git a/src/components/dashboard/SearchModal.tsx b/src/components/dashboard/SearchModal.tsx index f574af0..e314bd3 100644 --- a/src/components/dashboard/SearchModal.tsx +++ b/src/components/dashboard/SearchModal.tsx @@ -11,15 +11,11 @@ import { FileText, ArrowLeftRight, DollarSign, - Loader2, } from "lucide-react"; import { useState, useEffect, useMemo, useCallback } from "react"; import { useRouter } from "next/navigation"; import { useInvoiceHistory, HistoryItem } from "@/hooks/useInvoiceHistory"; -import { - useBridgeHistory, - BridgeTransaction, -} from "@/hooks/useBridgeHistory"; +import { useBridgeHistory, BridgeTransaction } from "@/hooks/useBridgeHistory"; interface SearchModalProps { isOpen: boolean; @@ -98,20 +94,20 @@ function getStatusDisplay(status: string): string { } export function SearchModal({ isOpen, onClose }: SearchModalProps) { + return ( + + {isOpen && } + + ); +} + +function SearchContent({ onClose }: { onClose: () => void }) { const router = useRouter(); const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const { history: invoices } = useInvoiceHistory(); const { transactions: bridges } = useBridgeHistory(); - // Reset state when modal opens/closes - useEffect(() => { - if (isOpen) { - setQuery(""); - setSelectedIndex(0); - } - }, [isOpen]); - // Combine and transform data into unified search results const allItems = useMemo((): SearchResult[] => { const invoiceResults: SearchResult[] = invoices.map((inv, idx) => ({ @@ -186,22 +182,52 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) { const lowerQuery = query.toLowerCase().trim(); return quickActions.filter((action) => - action.label.toLowerCase().includes(lowerQuery) + action.label.toLowerCase().includes(lowerQuery), ); }, [query]); // Total navigable items const totalItems = searchResults.length + filteredQuickActions.length; - // Reset selected index when results change - useEffect(() => { - setSelectedIndex(0); - }, [query]); + // Handle selection of an item + const handleSelect = useCallback( + (index: number) => { + if (index < searchResults.length) { + // Selected a search result + const result = searchResults[index]; + if (result.type === "invoice") { + // Navigate to invoices page + router.push("/dashboard/invoices"); + } else { + // Navigate to bridge page + router.push("/dashboard/bridge"); + } + } else { + // Selected a quick action + const actionIndex = index - searchResults.length; + const action = filteredQuickActions[actionIndex]; + if (action) { + router.push(action.href); + } + } + onClose(); + }, + [searchResults, filteredQuickActions, router, onClose], + ); + + // Handle click on result item + const handleResultClick = (index: number) => { + handleSelect(index); + }; + + // Handle click on quick action + const handleQuickActionClick = (href: string) => { + router.push(href); + onClose(); + }; // Handle keyboard navigation useEffect(() => { - if (!isOpen) return; - const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case "ArrowDown": @@ -225,16 +251,14 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [isOpen, selectedIndex, totalItems]); + }, [selectedIndex, totalItems, handleSelect, onClose]); // Handle keyboard shortcuts for quick actions useEffect(() => { - if (!isOpen) return; - const handleShortcut = (e: KeyboardEvent) => { if (e.metaKey || e.ctrlKey) { const action = quickActions.find( - (a) => a.shortcut.toLowerCase() === e.key.toLowerCase() + (a) => a.shortcut.toLowerCase() === e.key.toLowerCase(), ); if (action) { e.preventDefault(); @@ -246,225 +270,185 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) { document.addEventListener("keydown", handleShortcut); return () => document.removeEventListener("keydown", handleShortcut); - }, [isOpen, router, onClose]); - - // Handle selection of an item - const handleSelect = useCallback( - (index: number) => { - if (index < searchResults.length) { - // Selected a search result - const result = searchResults[index]; - if (result.type === "invoice") { - // Navigate to invoices page - router.push("/dashboard/invoices"); - } else { - // Navigate to bridge page - router.push("/dashboard/bridge"); - } - } else { - // Selected a quick action - const actionIndex = index - searchResults.length; - const action = filteredQuickActions[actionIndex]; - if (action) { - router.push(action.href); - } - } - onClose(); - }, - [searchResults, filteredQuickActions, router, onClose] - ); - - // Handle click on result item - const handleResultClick = (index: number) => { - handleSelect(index); - }; - - // Handle click on quick action - const handleQuickActionClick = (href: string) => { - router.push(href); - onClose(); - }; + }, [router, onClose]); return ( - - {isOpen && ( - <> - -
- + +
+ +
+ + { + setQuery(e.target.value); + setSelectedIndex(0); + }} + className="w-full pl-16 pr-12 py-6 text-lg font-bold bg-card text-foreground focus:outline-none placeholder:text-muted/50 transition-colors" + /> + -
+ + +
-
- {/* Search Results / Recent Activity */} - {(searchResults.length > 0 || allItems.length === 0) && ( -
-

- - {query.trim() - ? `Search Results (${searchResults.length})` - : "Recent Activity"} -

- {searchResults.length > 0 ? ( -
- {searchResults.map((item, idx) => ( - - ))} -
- ) : ( -
-
- +
-

- {query.trim() - ? "No results found" - : "No recent activity"} -

-

- {query.trim() - ? "Try a different search term" - : "Create an invoice or bridge assets to get started"} -

-
- )} + + + ))}
- )} - - {/* Quick Actions */} - {filteredQuickActions.length > 0 && ( -
-

- - Quick Actions -

-
- {filteredQuickActions.map((action, idx) => { - const absoluteIndex = searchResults.length + idx; - return ( - - ); - })} + ) : ( +
+
+
+

+ {query.trim() ? "No results found" : "No recent activity"} +

+

+ {query.trim() + ? "Try a different search term" + : "Create an invoice or bridge assets to get started"} +

)}
+ )} - {/* Footer */} -
-
- - - ↵ - {" "} - to select - - - - ↑↓ - {" "} - to navigate - -
-
+ {/* Quick Actions */} + {filteredQuickActions.length > 0 && ( +
+

- Powered by Inflow AI + Quick Actions +

+
+ {filteredQuickActions.map((action, idx) => { + const absoluteIndex = searchResults.length + idx; + return ( + + ); + })}
- + )}
- - )} - + + {/* Footer */} +
+
+ + + ↵ + {" "} + to select + + + + ↑↓ + {" "} + to navigate + +
+
+ + Powered by Inflow AI +
+
+ +
+ ); } diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index e5101ff..48045df 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -9,7 +9,7 @@ import { useCallback, useRef, } from "react"; -import { useRouter, usePathname } from "next/navigation"; +// No router hooks needed - using window.location for navigation import { connect as stacksConnect, disconnect as stacksDisconnect, @@ -59,10 +59,14 @@ export function WalletProvider({ children }: { children: ReactNode }) { const [stacksConnected, setStacksConnected] = useState(false); const [stacksAddress, setStacksAddress] = useState(null); const [isConnecting, setIsConnecting] = useState(false); - const router = useRouter(); - const pathname = usePathname(); + const [isMounted, setIsMounted] = useState(false); const initialized = useRef(false); + // Track client-side mount for safe navigation + useEffect(() => { + setIsMounted(true); + }, []); + // Check for existing sessions on mount useEffect(() => { if (initialized.current) return; @@ -130,8 +134,11 @@ export function WalletProvider({ children }: { children: ReactNode }) { localStorage.setItem(STX_STORAGE_KEY, JSON.stringify({ address })); // Only redirect to dashboard if not on a /pay page - if (!ethConnected && !pathname.startsWith("/pay")) { - router.push("/dashboard"); + const isPayPage = + typeof window !== "undefined" && + window.location.pathname.startsWith("/pay"); + if (!ethConnected && !isPayPage && isMounted) { + window.location.href = "/dashboard"; } } } catch (error) { @@ -148,7 +155,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { } finally { setIsConnecting(false); } - }, [router, ethConnected, pathname]); + }, [ethConnected, isMounted]); const connectEthereum = useCallback(async () => { setIsConnecting(true); @@ -193,8 +200,11 @@ export function WalletProvider({ children }: { children: ReactNode }) { ); // Only redirect to dashboard if not on a /pay page - if (!stacksConnected && !pathname.startsWith("/pay")) { - router.push("/dashboard"); + const isPayPage = + typeof window !== "undefined" && + window.location.pathname.startsWith("/pay"); + if (!stacksConnected && !isPayPage && isMounted) { + window.location.href = "/dashboard"; } } } else { @@ -213,7 +223,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { } finally { setIsConnecting(false); } - }, [router, stacksConnected, pathname]); + }, [stacksConnected, isMounted]); const disconnectEthereum = useCallback(() => { setEthAddress(null);