diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 34b09ea..59d76dd 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,7 +21,29 @@ "Bash(python3 -m json.tool)", "Bash(bun install:*)", "Bash(grep:*)", - "Bash(bunx turbo:*)" + "Bash(bunx turbo:*)", + "Bash(find:*)", + "Bash(python3 -c \"import sys,json; data=json.load\\(sys.stdin\\); [print\\(t['path']\\) for t in data.get\\('tree',[]\\)]\")", + "Bash(python3 -c \"import sys,json; [print\\(f['name'],f['type']\\) for f in json.load\\(sys.stdin\\)]\")", + "Bash(temporal operator:*)", + "Read(//private/tmp/**)", + "Bash(gh api:*)", + "Bash(ls /Users/henriquebreim/facilitator/docker-compose*)", + "Bash(ls /Users/henriquebreim/facilitator/docker*)", + "WebFetch(domain:www.circle.com)", + "Bash(bun test:*)", + "Bash(npx tsc:*)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:www.x402.org)", + "WebFetch(domain:docs.x402.org)", + "Bash(rm *)", + "WebFetch(domain:www.x402scan.com)", + "WebFetch(domain:dexter.cash)", + "Bash(gh pr *)", + "Bash(sed -n '25,1260p' /Users/henriquebreim/facilitator/packages/relay/public/index.html)", + "Bash(sed -n '1279,1850p' /Users/henriquebreim/facilitator/packages/relay/public/index.html)", + "Bash(sed -n '1853,2142p' /Users/henriquebreim/facilitator/packages/relay/public/index.html)", + "WebFetch(domain:agentcard.402.md)" ] } } diff --git a/bun.lock b/bun.lock index a527e3e..c2eb021 100644 --- a/bun.lock +++ b/bun.lock @@ -39,7 +39,9 @@ "dependencies": { "@402md/shared": "workspace:*", "@elysiajs/cors": "^1", + "@elysiajs/html": "^1.4.0", "@elysiajs/swagger": "^1", + "@kitajs/html": "^4.2.13", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/sdk-node": "^0.214.0", "@stellar/mpp": "^0.4.0", @@ -132,6 +134,8 @@ "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], + "@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="], + "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], @@ -266,6 +270,10 @@ "@jsonjoy.com/util": ["@jsonjoy.com/util@1.9.0", "", { "dependencies": { "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ=="], + "@kitajs/html": ["@kitajs/html@4.2.13", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-o+8e61EsoLDPTP7rsPkYolca1YFybHuxU2Lr5fWDZCUkYT/6uBlVkvnZUdCXMQKentJL9dxwpR8/xK2Q+U4LhA=="], + + "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -630,7 +638,7 @@ "@types/aws-lambda": ["@types/aws-lambda@8.10.161", "", {}, "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="], @@ -820,7 +828,7 @@ "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -850,7 +858,7 @@ "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], @@ -878,6 +886,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -1566,9 +1576,9 @@ "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1584,6 +1594,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.67.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.67.0", "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0", "@jsonjoy.com/json-pointer": "17.67.0", "@jsonjoy.com/util": "17.67.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w=="], "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.67.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew=="], @@ -1592,6 +1604,8 @@ "@jsonjoy.com/util/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="], + "@kitajs/ts-html-plugin/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "@modelcontextprotocol/sdk/eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -1648,12 +1662,6 @@ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "eslint/@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], @@ -1714,8 +1722,6 @@ "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1760,6 +1766,12 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@grpc/proto-loader/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/base64": ["@jsonjoy.com/base64@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw=="], "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], @@ -1798,12 +1810,6 @@ "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "gel/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], @@ -1910,11 +1916,15 @@ "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@grpc/proto-loader/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@solana/codecs/@solana/codecs-core/@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -1926,6 +1936,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/packages/relay/drizzle/0003_cooing_sentry.sql b/packages/relay/drizzle/0003_cooing_sentry.sql new file mode 100644 index 0000000..60d1f4c --- /dev/null +++ b/packages/relay/drizzle/0003_cooing_sentry.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_transactions_buyer_network" ON "transactions" USING btree ("buyer_network");--> statement-breakpoint +CREATE INDEX "idx_transactions_seller_network" ON "transactions" USING btree ("seller_network"); \ No newline at end of file diff --git a/packages/relay/drizzle/meta/0003_snapshot.json b/packages/relay/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..e40f95e --- /dev/null +++ b/packages/relay/drizzle/meta/0003_snapshot.json @@ -0,0 +1,546 @@ +{ + "id": "dec484ad-a09e-49f6-b3b2-0f1ccfbe6c1c", + "prevId": "489e6a6b-3e17-4921-8216-15d9f418c3f3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.bazaar_resources": { + "name": "bazaar_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_url": { + "name": "resource_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seller_id": { + "name": "seller_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "merchant_id": { + "name": "merchant_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "network": { + "name": "network", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "pay_to": { + "name": "pay_to", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "scheme": { + "name": "scheme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "total_volume": { + "name": "total_volume", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_bazaar_resource_network": { + "name": "uniq_bazaar_resource_network", + "columns": [ + { + "expression": "resource_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "network", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bazaar_resources_seller": { + "name": "idx_bazaar_resources_seller", + "columns": [ + { + "expression": "seller_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bazaar_resources_base_url": { + "name": "idx_bazaar_resources_base_url", + "columns": [ + { + "expression": "base_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bazaar_resources_use_count": { + "name": "idx_bazaar_resources_use_count", + "columns": [ + { + "expression": "use_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bazaar_resources_seller_id_sellers_id_fk": { + "name": "bazaar_resources_seller_id_sellers_id_fk", + "tableFrom": "bazaar_resources", + "tableTo": "sellers", + "columnsFrom": ["seller_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sellers": { + "name": "sellers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "merchant_id": { + "name": "merchant_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "wallet_address": { + "name": "wallet_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "network": { + "name": "network", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sellers_merchant": { + "name": "idx_sellers_merchant", + "columns": [ + { + "expression": "merchant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sellers_merchant_id_unique": { + "name": "sellers_merchant_id_unique", + "nullsNotDistinct": false, + "columns": ["merchant_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "buyer_address": { + "name": "buyer_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "buyer_network": { + "name": "buyer_network", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "seller_address": { + "name": "seller_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "seller_network": { + "name": "seller_network", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "seller_id": { + "name": "seller_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gross_amount": { + "name": "gross_amount", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": true + }, + "net_amount": { + "name": "net_amount", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": false + }, + "platform_fee": { + "name": "platform_fee", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": false + }, + "gas_allowance": { + "name": "gas_allowance", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "stripe_fee": { + "name": "stripe_fee", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": false + }, + "pull_tx_hash": { + "name": "pull_tx_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "burn_tx_hash": { + "name": "burn_tx_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "mint_tx_hash": { + "name": "mint_tx_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "bridge_provider": { + "name": "bridge_provider", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_transactions_seller": { + "name": "idx_transactions_seller", + "columns": [ + { + "expression": "seller_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_status": { + "name": "idx_transactions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_created": { + "name": "idx_transactions_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_seller_date": { + "name": "idx_transactions_seller_date", + "columns": [ + { + "expression": "seller_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_workflow": { + "name": "idx_transactions_workflow", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_buyer": { + "name": "idx_transactions_buyer", + "columns": [ + { + "expression": "buyer_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_buyer_network": { + "name": "idx_transactions_buyer_network", + "columns": [ + { + "expression": "buyer_network", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_transactions_seller_network": { + "name": "idx_transactions_seller_network", + "columns": [ + { + "expression": "seller_network", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_seller_id_sellers_id_fk": { + "name": "transactions_seller_id_sellers_id_fk", + "tableFrom": "transactions", + "tableTo": "sellers", + "columnsFrom": ["seller_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/relay/drizzle/meta/_journal.json b/packages/relay/drizzle/meta/_journal.json index 3eca88f..a8975b8 100644 --- a/packages/relay/drizzle/meta/_journal.json +++ b/packages/relay/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1775793460521, "tag": "0002_plain_sunfire", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1776465361800, + "tag": "0003_cooing_sentry", + "breakpoints": true } ] } diff --git a/packages/relay/package.json b/packages/relay/package.json index 5fd4ef3..bd2fd97 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -9,12 +9,15 @@ "db:generate": "drizzle-kit generate", "db:push": "drizzle-kit push", "db:migrate": "drizzle-kit migrate", + "db:seed-dashboard": "bun run scripts/seed-dashboard.ts", "lint": "eslint src/" }, "dependencies": { "@402md/shared": "workspace:*", "@elysiajs/cors": "^1", + "@elysiajs/html": "^1.4.0", "@elysiajs/swagger": "^1", + "@kitajs/html": "^4.2.13", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/sdk-node": "^0.214.0", "@stellar/mpp": "^0.4.0", diff --git a/packages/relay/public/index.html b/packages/relay/public/index.html deleted file mode 100644 index f50502f..0000000 --- a/packages/relay/public/index.html +++ /dev/null @@ -1,2144 +0,0 @@ - - - - - - - 402md Facilitator — Cross-chain USDC settlement for x402 and MPP - - - - - - - - - - - - - - -
- -
-
-
-

- Any chain in, - your chain out. -

- -

- Cross-chain USDC settlement for x402 (Coinbase's HTTP 402 protocol) and MPP (Machine - Payments Protocol). Buyer pays on Solana, seller receives on Stellar. No SDK, no - dashboard, no code changes. -

- - -
-
-
- - -
-
- -
-
-
- -
- terminal -
-
# 1. Register your wallet (one-time)
-curl -X POST https://facilitator.402.md/register \
-  -H "Content-Type: application/json" \
-  -d '{"wallet": "GSELLER...", "network": "stellar:pubnet"}'
-
-# 2. Use standard @x402/express — zero 402md code
-import { paymentMiddleware } from "@x402/express"
-
-app.use(paymentMiddleware({
-  "GET /your-endpoint": {
-    accepts: [
-      { scheme: "exact", network: "eip155:8453", payTo: "0x4C95...c3b9", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } },
-      { scheme: "exact", network: "solana:mainnet", payTo: "4r7q...BDP5", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } },
-      { scheme: "exact", network: "stellar:pubnet", payTo: "GAX5...5HZR", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } },
-    ],
-  },
-}, "https://facilitator.402.md"))
-
-# Buyers on Base, Solana, or Stellar can now pay you.
-
-
-
- - -
-
- -

One chain is not enough

-

- x402 lets any API charge for access. But every facilitator today locks sellers to a - single chain — and leaves money on the table. -

- -
-
-

Chain lock-in

-

- 30 x402 facilitators, all single-chain. Buyer on Solana, seller on Stellar? Payment - fails. Revenue lost silently. -

-
-
-

Wrong tokens, wrong path

-

- DEX-based bridges mean wrapped USDC, slippage, and risk. Native USDC via CCTP V2 is - the only clean path. -

-
-
-

Fragmented protocols

-

x402 and MPP serve different ecosystems. No one speaks both — until now.

-
-
-

Agents don't choose chains

-

- AI agents need to pay, not navigate blockchain infrastructure. The payment layer - should be invisible. -

-
-
-
-
- - -
-
- -

Three steps. That's it.

- -
-
-
Seller
-

One curl to start receiving

-
-
- 01 -
-

Register your wallet

-

- POST /register with your address and chain. Get - facilitator addresses on all supported chains. -

-
-
-
- 02 -
-

Use standard x402

-

- Add @x402/express middleware. Point - payTo to your facilitator address. Zero 402md - dependencies. -

-
-
-
- 03 -
-

Receive USDC on your chain

-

- Buyer pays on any chain. 402md settles via CCTP V2. Native USDC, directly to - your wallet. -

-
-
-
-
- -
-
Buyer / Agent
-

Pay from any chain

-
-
- 01 -
-

Discover the seller

-

- GET the API. Receive a 402 response with facilitator - addresses on your chain. -

-
-
-
- 02 -
-

Pay with USDC

-

- Sign a standard x402 payment on Base - Solana or - Stellar. No bridge, no swap. -

-
-
-
- 03 -
-

Get the resource

-

- Payment verified in milliseconds. Resource delivered instantly. Settlement - runs in the background. -

-
-
-
-
-
-
-
- - -
-
- -

Opinionated choices, for good reasons

-

- Every decision optimizes for one thing: the seller should do zero extra work to accept - payments from any chain. -

- -
-
-

Non-custodial

-

- CCTP V2 mints USDC directly to the seller's wallet. 402md never holds your funds. -

-
-
-

Free

-

- 0% platform fee. Only network gas deducted from settlement. Fixed schedule, no - surprises. -

-
-
-

Zero dependencies

-

- Sellers use standard @x402/express from Coinbase. No 402md SDK, no custom packages. -

-
-
- -
-
-

Dual protocol

-

x402 and MPP through one facilitator. Two ecosystems, one registration.

-
-
-

Native USDC only

-

Circle CCTP V2 burns and mints real USDC. No wrapped tokens, no slippage.

-
-
-

Three chains

-

Base, Solana, Stellar. Adding a new EVM chain is a config change, not a deploy.

-
-
- - -
-
- - -
-
- -

Free. Seriously.

-

- Zero platform fee. The only cost is a fixed gas allowance per route, deducted from the - seller's payout. Here's what that looks like on a $1.00 payment. -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RouteSeller receivesGas allowance
- Stellar → - Stellar - $0.99999$0.00001 0.001%
- Solana → - Solana - $0.9990$0.0010 0.10%
- Base → - Base - $0.9980$0.0020 0.20%
- Stellar → - Solana - $0.9985$0.0015 0.15%
- Solana → - Stellar - $0.9975$0.0025 0.25%
- Stellar → - Base - $0.9975$0.0025 0.25%
- Base → - Stellar - $0.9968$0.0032 0.32%
- Base → - Solana - $0.9965$0.0035 0.35%
- Solana → - Base - $0.9965$0.0035 0.35%
-
-
-
- - -
-
- -

Common questions

- -
-
- What is Facilitator by 402md? -
- A cross-chain USDC settlement facilitator built by 402md for the x402 and MPP - protocols. Sellers register once and receive payments from buyers on any supported - chain — Base, Solana, or Stellar. -
-
- -
- How is this different from other x402 facilitators? -
- Existing facilitators are single-chain. If a buyer is on Solana and the seller is on - Stellar, the payment fails. 402md bridges that gap using Circle CCTP V2 — atomic, - non-custodial, native USDC. No wrapped tokens, no slippage. -
-
- -
- What chains are supported? -
- Base (EVM), Solana, and Stellar — all connected via Circle CCTP V2. Adding new EVM - chains requires only a config update, no contract deployments. -
-
- -
- How much does it cost? -
- Zero platform fee. The only deduction is a fixed gas allowance per route — for - example, $0.0032 for Base→Stellar on a $1.00 payment. Same-chain payments cost - even less ($0.00001 for Stellar→Stellar). -
-
- -
- Is it custodial? -
- No. Cross-chain settlements use Circle CCTP V2, which mints USDC directly to the - seller's wallet. 402md never holds seller funds. The facilitator only retains the - gas allowance needed to execute the settlement. -
-
- -
- How long does settlement take? -
- Same-chain: under 5 seconds. Stellar or Solana cross-chain: 5-30 seconds. EVM - cross-chain: 15-19 minutes, limited by block finality. The buyer gets the resource - instantly — settlement happens in the background. -
-
- -
- Do I need to install any 402md package? -
- No. Sellers use the standard @x402/express SDK from Coinbase. The only - 402md-specific step is a one-time POST /register call — after that, everything works - with standard x402 tooling. -
-
- -
- What about same-chain payments? -
- If buyer and seller are on the same chain, 402md does a direct USDC transfer — no - bridge involved. Faster settlement, lower gas, same API. -
-
- -
- What is MPP? -
- Machine Payments Protocol, developed by Stellar and used by services like OpenAI, - Anthropic, and Dune. 402md supports MPP alongside x402, enabling AI agents from both - ecosystems to transact through one facilitator. -
-
-
-
-
- - -
-
- -

One curl. All chains.

-

- Register your wallet and start accepting cross-chain USDC in under a minute. No - dashboard, no login, no SDK. -

- -
- USDC - Base - Solana - Stellar -
- -
-
-
- -
- - - - -
- - -
- - - - - - - - -
- - - -

0x followed by 40 hex characters

- -
- - API docs ↗ -
-
-
- - -
-
-
-
-
- - - - - - - diff --git a/packages/relay/scripts/seed-dashboard.ts b/packages/relay/scripts/seed-dashboard.ts new file mode 100644 index 0000000..e41c423 --- /dev/null +++ b/packages/relay/scripts/seed-dashboard.ts @@ -0,0 +1,188 @@ +/** + * Seed the dashboard with synthetic sellers, bazaar resources, and transactions. + * + * Usage (from packages/relay): + * bun run scripts/seed-dashboard.ts + * bun run scripts/seed-dashboard.ts --reset + * + * All seeded rows use merchantIds prefixed with `seed-` so the reset is safe + * and never touches real data. + */ +import { sql, inArray, like } from 'drizzle-orm' +import { db, sellers, bazaarResources, transactions } from '@402md/shared/db' + +const SEED_PREFIX = 'seed-' + +const CHAINS = [ + { slug: 'base', caip2: 'eip155:8453', evm: true }, + { slug: 'ethereum', caip2: 'eip155:1', evm: true }, + { slug: 'optimism', caip2: 'eip155:10', evm: true }, + { slug: 'arbitrum', caip2: 'eip155:42161', evm: true }, + { slug: 'linea', caip2: 'eip155:59144', evm: true }, + { slug: 'unichain', caip2: 'eip155:130', evm: true }, + { slug: 'worldchain', caip2: 'eip155:480', evm: true }, + { slug: 'solana', caip2: 'solana:mainnet', evm: false }, + { slug: 'stellar', caip2: 'stellar:pubnet', evm: false }, +] + +const PROTOCOLS = ['x402', 'mpp'] as const +const STATUSES = ['settled', 'settled', 'settled', 'settled', 'pending', 'failed'] as const + +const RESOURCE_CATALOG = [ + { path: '/weather/forecast', description: 'Global weather forecast API', cents: '5000' }, + { path: '/market/quote', description: 'Real-time stock quote', cents: '10000' }, + { path: '/llm/chat', description: 'Cheap LLM chat endpoint', cents: '25000' }, + { path: '/embed/text', description: 'Text embedding 1536-dim', cents: '2000' }, + { path: '/ocr/extract', description: 'OCR image-to-text', cents: '50000' }, + { path: '/translate/en-es', description: 'EN→ES machine translation', cents: '3000' }, + { path: '/speech/transcribe', description: 'Audio transcription', cents: '150000' }, + { path: '/image/generate', description: 'AI image generation', cents: '200000' }, + { path: '/geocode/reverse', description: 'Reverse geocoding', cents: '1000' }, + { path: '/news/feed', description: 'Curated news feed', cents: '500' }, + { path: '/crypto/price', description: 'Crypto price ticker', cents: '1500' }, + { path: '/scrape/url', description: 'URL scrape + summarize', cents: '80000' }, +] + +const BUYER_EVM = '0xA1B2C3D4E5F60718293A4B5C6D7E8F9012345678' +const BUYER_SOL = 'So1anaB4yer1111111111111111111111111111111' +const BUYER_STELLAR = 'GABC5SELLERXSTELLARZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZA' + +function walletFor(chain: (typeof CHAINS)[number], idx: number): string { + if (chain.evm) return `0x${(idx + 1).toString(16).padStart(2, '0').repeat(20)}`.slice(0, 42) + if (chain.slug === 'solana') + return `So1anaSel1er${idx.toString().padStart(2, '0')}${'1'.repeat(30)}`.slice(0, 44) + return `GSELLER${idx.toString().padStart(2, '0')}${'A'.repeat(48)}`.slice(0, 56) +} + +function buyerFor(caip2: string): string { + if (caip2.startsWith('eip155:')) return BUYER_EVM + if (caip2.startsWith('solana:')) return BUYER_SOL + return BUYER_STELLAR +} + +function txHash(prefix: string, n: number): string { + return `0x${prefix}${n.toString(16).padStart(60, '0')}` +} + +function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +function pick(arr: readonly T[]): T { + return arr[Math.floor(Math.random() * arr.length)] +} + +async function reset() { + console.log('→ wiping previous seed rows…') + const seedSellers = await db + .select({ id: sellers.id }) + .from(sellers) + .where(like(sellers.merchantId, `${SEED_PREFIX}%`)) + const ids = seedSellers.map((s) => s.id) + if (ids.length > 0) { + await db.delete(transactions).where(inArray(transactions.sellerId, ids)) + await db.delete(bazaarResources).where(inArray(bazaarResources.sellerId, ids)) + await db.delete(sellers).where(inArray(sellers.id, ids)) + } + // transactions w/ no sellerId fallback wipe + await db.execute( + sql`DELETE FROM transactions WHERE pull_tx_hash LIKE '0xseed%' OR burn_tx_hash LIKE '0xseed%'`, + ) + console.log(` removed ${ids.length} sellers and dependents`) +} + +async function seed() { + console.log('→ inserting sellers…') + const sellerRows = CHAINS.map((chain, i) => ({ + merchantId: `${SEED_PREFIX}${chain.slug}-${i + 1}`, + walletAddress: walletFor(chain, i + 1), + network: chain.caip2, + })) + const insertedSellers = await db.insert(sellers).values(sellerRows).returning() + console.log(` inserted ${insertedSellers.length} sellers`) + + console.log('→ inserting bazaar resources…') + const resourceRows = insertedSellers.flatMap((seller, i) => { + const slice = RESOURCE_CATALOG.slice(i % 3, (i % 3) + 3) + return slice.map((r) => ({ + resourceUrl: `https://api.seller${i + 1}.example.com${r.path}`, + baseUrl: `https://api.seller${i + 1}.example.com`, + description: r.description, + sellerId: seller.id, + merchantId: seller.merchantId, + network: seller.network, + payTo: seller.walletAddress, + amount: r.cents, + scheme: 'exact', + useCount: randomInt(10, 800), + totalVolume: (BigInt(r.cents) * BigInt(randomInt(10, 800))).toString(), + })) + }) + await db.insert(bazaarResources).values(resourceRows) + console.log(` inserted ${resourceRows.length} resources`) + + console.log('→ inserting transactions…') + const TX_COUNT = 300 + const txRows: (typeof transactions.$inferInsert)[] = [] + for (let i = 0; i < TX_COUNT; i++) { + const seller = pick(insertedSellers) + const sellerChain = CHAINS.find((c) => c.caip2 === seller.network)! + // 35% cross-chain so the matrix has spread + const crossChain = Math.random() < 0.35 + const buyerChain = crossChain + ? pick(CHAINS.filter((c) => c.caip2 !== sellerChain.caip2)) + : sellerChain + const type = crossChain ? 'BRIDGE_SETTLEMENT' : 'SAME_CHAIN' + const protocol = pick(PROTOCOLS) + const status = pick(STATUSES) + + const gross = BigInt(randomInt(500, 500000)) // $0.0005 – $0.50 + const gasAllowance = crossChain ? BigInt(randomInt(100, 2000)) : 0n + const net = gross - gasAllowance + const daysAgo = randomInt(0, 29) + const createdAt = new Date(Date.now() - daysAgo * 86400 * 1000 - randomInt(0, 86400 * 1000)) + const settledAt = + status === 'settled' ? new Date(createdAt.getTime() + randomInt(5, 60) * 1000) : null + + txRows.push({ + type, + protocol, + status, + buyerAddress: buyerFor(buyerChain.caip2), + buyerNetwork: buyerChain.caip2, + sellerAddress: seller.walletAddress, + sellerNetwork: seller.network, + sellerId: seller.id, + grossAmount: gross.toString(), + netAmount: net.toString(), + platformFee: '0', + gasAllowance: gasAllowance.toString(), + pullTxHash: txHash('seed', i), + burnTxHash: crossChain ? txHash('seedb', i) : null, + mintTxHash: crossChain && status === 'settled' ? txHash('seedm', i) : null, + createdAt, + settledAt, + }) + } + // batch insert in chunks of 100 (PG param limit safety) + for (let i = 0; i < txRows.length; i += 100) { + await db.insert(transactions).values(txRows.slice(i, i + 100)) + } + console.log(` inserted ${txRows.length} transactions`) +} + +async function main() { + if (!process.env.DATABASE_URL) { + console.error('DATABASE_URL not set') + process.exit(1) + } + if (process.argv.includes('--reset')) await reset() + await seed() + console.log('✔ dashboard seeded — hit /dashboard to see it') + process.exit(0) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/relay/src/bazaar/aggregations.test.ts b/packages/relay/src/bazaar/aggregations.test.ts new file mode 100644 index 0000000..bc48c38 --- /dev/null +++ b/packages/relay/src/bazaar/aggregations.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, test } from 'bun:test' +import '@/shared/test-helpers' +import { parseWindow, getCostComparison, RouteNotConfiguredError } from './aggregations' +import { toLimitOffset } from './bazaar.routes' + +describe('parseWindow', () => { + test('accepts 1d / 7d / 30d', () => { + expect(parseWindow('1d')).toBe('1d') + expect(parseWindow('7d')).toBe('7d') + expect(parseWindow('30d')).toBe('30d') + }) + + test('defaults to 7d for unknown or missing', () => { + expect(parseWindow(undefined)).toBe('7d') + expect(parseWindow('')).toBe('7d') + expect(parseWindow('42h')).toBe('7d') + expect(parseWindow('2d')).toBe('7d') + }) +}) + +describe('getCostComparison', () => { + test('returns three tiers with CCTP allowance from gas schedule', () => { + const result = getCostComparison({ + buyerNetwork: 'eip155:8453', + sellerNetwork: 'stellar:pubnet', + }) + + expect(result.buyerNetwork).toBe('eip155:8453') + expect(result.sellerNetwork).toBe('stellar:pubnet') + expect(result.tiers).toHaveLength(3) + expect(result.tiers.map((t) => t.amount)).toEqual(['100000', '1000000', '100000000']) + + for (const tier of result.tiers) { + expect(typeof tier.cctpAllowance).toBe('string') + expect(typeof tier.percentAlternative).toBe('string') + expect(typeof tier.savingsVsPercent).toBe('string') + // savings = percentAlternative - cctpAllowance + const expected = (BigInt(tier.percentAlternative) - BigInt(tier.cctpAllowance)).toString() + expect(tier.savingsVsPercent).toBe(expected) + } + }) + + test('percentAlternative respects the $0.10 floor for small amounts', () => { + const result = getCostComparison({ + buyerNetwork: 'eip155:8453', + sellerNetwork: 'stellar:pubnet', + }) + const smallest = result.tiers[0] + // $0.10 amount * 1% = $0.001, but the floor is $0.10 → percentAlternative = 100_000 micro-USDC + expect(smallest.percentAlternative).toBe('100000') + }) + + test('percentAlternative scales at 1% above the floor', () => { + const result = getCostComparison({ + buyerNetwork: 'eip155:8453', + sellerNetwork: 'stellar:pubnet', + }) + // $100 amount * 1% = $1.00 = 1_000_000 micro-USDC + expect(result.tiers[2].percentAlternative).toBe('1000000') + }) + + test('includes documentation notes', () => { + const result = getCostComparison({ + buyerNetwork: 'eip155:8453', + sellerNetwork: 'stellar:pubnet', + }) + expect(result.notes.cctpSource).toBe('gas-schedule.ts') + expect(result.notes.percentAssumption).toContain('1%') + expect(result.notes.percentAssumption).toContain('$0.10') + }) + + test('throws RouteNotConfiguredError for unsupported network', () => { + expect(() => + getCostComparison({ buyerNetwork: 'eip155:999999', sellerNetwork: 'stellar:pubnet' }), + ).toThrow(RouteNotConfiguredError) + }) +}) + +describe('RouteNotConfiguredError', () => { + test('has code and statusCode fields for error-handling middleware', () => { + const err = new RouteNotConfiguredError('eip155:8453', 'eip155:999999') + expect(err.code).toBe('ROUTE_NOT_CONFIGURED') + expect(err.statusCode).toBe(404) + expect(err.message).toContain('eip155:8453') + expect(err.message).toContain('eip155:999999') + }) +}) + +describe('toLimitOffset', () => { + test('defaults when both undefined', () => { + expect(toLimitOffset(undefined, undefined)).toEqual({ limit: 20, offset: 0 }) + }) + + test('parses valid numeric strings', () => { + expect(toLimitOffset('50', '10')).toEqual({ limit: 50, offset: 10 }) + }) + + test('clamps limit to max 100', () => { + expect(toLimitOffset('500', '0')).toEqual({ limit: 100, offset: 0 }) + }) + + test('clamps limit to min 1', () => { + expect(toLimitOffset('0', '0')).toEqual({ limit: 1, offset: 0 }) + expect(toLimitOffset('-5', '0')).toEqual({ limit: 1, offset: 0 }) + }) + + test('clamps offset to min 0', () => { + expect(toLimitOffset('20', '-10')).toEqual({ limit: 20, offset: 0 }) + }) + + test('falls back to defaults when input is not a finite number', () => { + // This is the NaN regression: Number('abc') = NaN, must not reach SQL + expect(toLimitOffset('abc', 'xyz')).toEqual({ limit: 20, offset: 0 }) + expect(toLimitOffset('Infinity', 'NaN')).toEqual({ limit: 20, offset: 0 }) + }) + + test('handles empty string as missing (falls back to defaults)', () => { + expect(toLimitOffset('', '')).toEqual({ limit: 20, offset: 0 }) + }) +}) diff --git a/packages/relay/src/bazaar/aggregations.ts b/packages/relay/src/bazaar/aggregations.ts new file mode 100644 index 0000000..9843734 --- /dev/null +++ b/packages/relay/src/bazaar/aggregations.ts @@ -0,0 +1,592 @@ +import { sql, and, eq, like, desc, type SQL } from 'drizzle-orm' +import { db, bazaarResources, sellers, transactions } from '@402md/shared/db' +import { networks, resolveSlugByCaip2, getGasAllowanceBySlug } from '@402md/shared/networks' + +export type Window = '1d' | '7d' | '30d' + +export function parseWindow(raw: string | undefined): Window { + if (raw === '1d' || raw === '7d' || raw === '30d') return raw + return '7d' +} + +function windowInterval(w: Window): SQL { + if (w === '1d') return sql`NOW() - INTERVAL '1 day'` + if (w === '7d') return sql`NOW() - INTERVAL '7 days'` + return sql`NOW() - INTERVAL '30 days'` +} + +function normalizeType(raw: string): 'same_chain' | 'cross_chain' | string { + if (raw === 'SAME_CHAIN') return 'same_chain' + if (raw === 'BRIDGE_SETTLEMENT') return 'cross_chain' + return raw +} + +function denormalizeType(raw: string): string { + if (raw === 'same_chain') return 'SAME_CHAIN' + if (raw === 'cross_chain') return 'BRIDGE_SETTLEMENT' + return raw +} + +export class RouteNotConfiguredError extends Error { + readonly code = 'ROUTE_NOT_CONFIGURED' + readonly statusCode = 404 + constructor(from: string, to: string) { + super(`No gas schedule for route ${from} → ${to}`) + } +} + +// ---------------------------------------------------------------------------- +// 1. getStats +// ---------------------------------------------------------------------------- + +export interface StatsResponse { + window: Window + chainsActive: number + chainsSupported: number + uniqueRoutes: number + crossChainRoutes: number + sameChainRoutes: number + totalVolume: string + txCount: number + protocolSplit: Record + typeSplit: Record +} + +export async function getStats({ window }: { window: Window }): Promise { + const interval = windowInterval(window) + + const rows = await db.execute<{ + unique_routes: string + cross_chain_routes: string + same_chain_routes: string + tx_count: string + total_volume: string | null + chains_active: string + protocol_split: Record | null + type_split: Record | null + }>(sql` + WITH base AS ( + SELECT * FROM transactions + WHERE created_at > ${interval} AND status = 'settled' + ), + stats AS ( + SELECT + COUNT(DISTINCT (buyer_network || '|' || seller_network))::text AS unique_routes, + COUNT(DISTINCT CASE WHEN buyer_network <> seller_network THEN buyer_network || '|' || seller_network END)::text AS cross_chain_routes, + COUNT(DISTINCT CASE WHEN buyer_network = seller_network THEN buyer_network || '|' || seller_network END)::text AS same_chain_routes, + COUNT(*)::text AS tx_count, + COALESCE(SUM(gross_amount), 0)::text AS total_volume + FROM base + ), + chains AS ( + SELECT COUNT(DISTINCT x.net)::text AS chains_active + FROM base t, LATERAL (VALUES (t.buyer_network), (t.seller_network)) AS x(net) + ), + protocols AS ( + SELECT COALESCE(jsonb_object_agg(protocol, cnt), '{}'::jsonb) AS protocol_split + FROM (SELECT protocol, COUNT(*)::int AS cnt FROM base GROUP BY protocol) p + ), + types AS ( + SELECT COALESCE(jsonb_object_agg(type, cnt), '{}'::jsonb) AS type_split + FROM (SELECT type, COUNT(*)::int AS cnt FROM base GROUP BY type) t + ) + SELECT stats.*, chains.chains_active, protocols.protocol_split, types.type_split + FROM stats, chains, protocols, types + `) + + const head = rows[0] ?? { + unique_routes: '0', + cross_chain_routes: '0', + same_chain_routes: '0', + tx_count: '0', + total_volume: '0', + chains_active: '0', + protocol_split: {}, + type_split: {}, + } + + const protocolSplit: Record = { ...(head.protocol_split ?? {}) } + + const rawTypeSplit = head.type_split ?? {} + const typeSplit: Record = {} + for (const [rawType, count] of Object.entries(rawTypeSplit)) { + typeSplit[normalizeType(rawType)] = count + } + + return { + window, + chainsActive: Number(head.chains_active), + chainsSupported: networks.length, + uniqueRoutes: Number(head.unique_routes), + crossChainRoutes: Number(head.cross_chain_routes), + sameChainRoutes: Number(head.same_chain_routes), + totalVolume: head.total_volume ?? '0', + txCount: Number(head.tx_count), + protocolSplit, + typeSplit, + } +} + +// ---------------------------------------------------------------------------- +// 2. getRoutes +// ---------------------------------------------------------------------------- + +export interface RouteRow { + buyerNetwork: string + sellerNetwork: string + txCount: number + volume: string + isCrossChain: boolean +} + +export interface RoutesResponse { + window: Window + routes: RouteRow[] +} + +export async function getRoutes({ window }: { window: Window }): Promise { + const interval = windowInterval(window) + + const rows = await db.execute<{ + buyer_network: string + seller_network: string + tx_count: string + volume: string | null + }>(sql` + SELECT buyer_network, seller_network, COUNT(*)::text AS tx_count, COALESCE(SUM(gross_amount), 0)::text AS volume + FROM transactions + WHERE created_at > ${interval} AND status = 'settled' + GROUP BY buyer_network, seller_network + ORDER BY SUM(gross_amount) DESC NULLS LAST + `) + + return { + window, + routes: rows.map((r) => ({ + buyerNetwork: r.buyer_network, + sellerNetwork: r.seller_network, + txCount: Number(r.tx_count), + volume: r.volume ?? '0', + isCrossChain: r.buyer_network !== r.seller_network, + })), + } +} + +// ---------------------------------------------------------------------------- +// 3. getRankedResources +// ---------------------------------------------------------------------------- + +export interface RankedResourceAccept { + scheme: string + network: string + payTo: string + amount: string +} + +export interface RankedResourceItem { + resource: string + type: 'http' + description: string | null + merchantId: string + accepts: RankedResourceAccept[] + useCount: number + totalVolume: string + windowedUseCount: number + windowedVolume: string + lastUsedAt: string | null +} + +export interface RankedResourcesResponse { + window: Window + items: RankedResourceItem[] + total: number +} + +export interface RankedResourcesParams { + window: Window + rank: 'uses' | 'volume' + network?: string + q?: string + limit: number + offset: number +} + +export async function getRankedResources( + params: RankedResourcesParams, +): Promise { + const { window, rank, network, q, limit, offset } = params + const interval = windowInterval(window) + + const conditions: SQL[] = [] + if (network) conditions.push(eq(bazaarResources.network, network)) + if (q) conditions.push(like(bazaarResources.resourceUrl, `%${q}%`)) + const where = conditions.length > 0 ? and(...conditions) : undefined + + const rows = await db + .select({ + id: bazaarResources.id, + resourceUrl: bazaarResources.resourceUrl, + description: bazaarResources.description, + merchantId: bazaarResources.merchantId, + network: bazaarResources.network, + payTo: bazaarResources.payTo, + amount: bazaarResources.amount, + scheme: bazaarResources.scheme, + useCount: bazaarResources.useCount, + totalVolume: bazaarResources.totalVolume, + lastUsedAt: bazaarResources.lastUsedAt, + }) + .from(bazaarResources) + .where(where) + .orderBy(desc(bazaarResources.useCount), desc(bazaarResources.lastUsedAt)) + + const windowed = await db.execute<{ + resource_url: string + use_count: string + volume: string | null + }>(sql` + SELECT b.resource_url, + COUNT(t.id)::text AS use_count, + COALESCE(SUM(t.gross_amount), 0)::text AS volume + FROM bazaar_resources b + LEFT JOIN transactions t + ON t.seller_id = b.seller_id + AND t.seller_network = b.network + AND t.created_at > ${interval} + AND t.status = 'settled' + GROUP BY b.resource_url + `) + + const windowedByUrl = new Map() + for (const w of windowed) { + windowedByUrl.set(w.resource_url, { + useCount: Number(w.use_count), + volume: w.volume ?? '0', + }) + } + + type GroupedEntry = { + resource: string + type: 'http' + description: string | null + merchantId: string + accepts: RankedResourceAccept[] + useCount: number + totalVolume: string + windowedUseCount: number + windowedVolume: string + lastUsedAt: string | null + } + + const grouped = new Map() + + for (const r of rows) { + const existing = grouped.get(r.resourceUrl) + const accept: RankedResourceAccept = { + scheme: r.scheme, + network: r.network, + payTo: r.payTo, + amount: r.amount, + } + + if (existing) { + existing.accepts.push(accept) + existing.useCount += r.useCount + existing.totalVolume = (BigInt(existing.totalVolume) + BigInt(r.totalVolume)).toString() + const lastIso = r.lastUsedAt?.toISOString() ?? null + if (lastIso && (!existing.lastUsedAt || lastIso > existing.lastUsedAt)) { + existing.lastUsedAt = lastIso + } + } else { + const w = windowedByUrl.get(r.resourceUrl) + grouped.set(r.resourceUrl, { + resource: r.resourceUrl, + type: 'http', + description: r.description, + merchantId: r.merchantId, + accepts: [accept], + useCount: r.useCount, + totalVolume: r.totalVolume ?? '0', + windowedUseCount: w?.useCount ?? 0, + windowedVolume: w?.volume ?? '0', + lastUsedAt: r.lastUsedAt?.toISOString() ?? null, + }) + } + } + + const items = [...grouped.values()] + + if (rank === 'volume') { + items.sort((a, b) => { + const diff = BigInt(b.windowedVolume) - BigInt(a.windowedVolume) + return diff > 0n ? 1 : diff < 0n ? -1 : 0 + }) + } else { + items.sort((a, b) => b.windowedUseCount - a.windowedUseCount) + } + + const paginated = items.slice(offset, offset + limit) + + return { window, items: paginated, total: items.length } +} + +// ---------------------------------------------------------------------------- +// 4. getRankedSellers +// ---------------------------------------------------------------------------- + +export interface RankedSellerItem { + merchantId: string + primaryNetwork: string + txCount: number + volume: string + resourceCount: number + firstSeenAt: string | null +} + +export interface RankedSellersResponse { + window: Window + items: RankedSellerItem[] + total: number +} + +export interface RankedSellersParams { + window: Window + rank: 'volume' | 'tx_count' + network?: string + limit: number + offset: number +} + +export async function getRankedSellers( + params: RankedSellersParams, +): Promise { + const { window, rank, network, limit, offset } = params + const interval = windowInterval(window) + + const orderColumn = rank === 'tx_count' ? sql`tx_count` : sql`volume` + + const networkFilter = network ? sql`AND s.network = ${network}` : sql`` + + const rows = await db.execute<{ + merchant_id: string + primary_network: string + first_seen_at: Date | string | null + tx_count: string + volume: string | null + resource_count: string + }>(sql` + SELECT s.merchant_id, + s.network AS primary_network, + s.created_at AS first_seen_at, + COUNT(t.id)::text AS tx_count, + COALESCE(SUM(t.gross_amount), 0)::text AS volume, + (SELECT COUNT(*)::text FROM bazaar_resources WHERE seller_id = s.id) AS resource_count + FROM sellers s + LEFT JOIN transactions t + ON t.seller_id = s.id + AND t.created_at > ${interval} + AND t.status = 'settled' + WHERE 1 = 1 ${networkFilter} + GROUP BY s.id + ORDER BY ${orderColumn} DESC NULLS LAST + LIMIT ${limit} OFFSET ${offset} + `) + + const totalRows = await db.execute<{ total: string }>(sql` + SELECT COUNT(*)::text AS total FROM sellers s WHERE 1 = 1 ${networkFilter} + `) + + const items: RankedSellerItem[] = rows.map((r) => ({ + merchantId: r.merchant_id, + primaryNetwork: r.primary_network, + txCount: Number(r.tx_count), + volume: r.volume ?? '0', + resourceCount: Number(r.resource_count), + firstSeenAt: r.first_seen_at ? new Date(r.first_seen_at).toISOString() : null, + })) + + return { window, items, total: Number(totalRows[0]?.total ?? 0) } +} + +// ---------------------------------------------------------------------------- +// 5. getTransactions +// ---------------------------------------------------------------------------- + +export interface TransactionItem { + id: string + type: string + protocol: string | null + status: string + buyerNetwork: string + sellerNetwork: string + merchantId: string | null + grossAmount: string + netAmount: string | null + gasAllowance: string | null + pullTxHash: string | null + burnTxHash: string | null + mintTxHash: string | null + createdAt: string | null + settledAt: string | null +} + +export interface TransactionsResponse { + items: TransactionItem[] + total: number + limit: number + offset: number +} + +export interface TransactionsParams { + window?: Window + merchantId?: string + buyerNetwork?: string + sellerNetwork?: string + status?: string + protocol?: string + type?: string + limit: number + offset: number +} + +export async function getTransactions(params: TransactionsParams): Promise { + const { window, merchantId, buyerNetwork, sellerNetwork, status, protocol, type, limit, offset } = + params + + const conditions: SQL[] = [] + if (window) conditions.push(sql`${transactions.createdAt} > ${windowInterval(window)}`) + if (merchantId) conditions.push(eq(sellers.merchantId, merchantId)) + if (buyerNetwork) conditions.push(eq(transactions.buyerNetwork, buyerNetwork)) + if (sellerNetwork) conditions.push(eq(transactions.sellerNetwork, sellerNetwork)) + if (status) conditions.push(eq(transactions.status, status)) + if (protocol) conditions.push(eq(transactions.protocol, protocol)) + if (type) conditions.push(eq(transactions.type, denormalizeType(type))) + + const where = conditions.length > 0 ? and(...conditions) : undefined + + const rows = await db + .select({ + id: transactions.id, + type: transactions.type, + protocol: transactions.protocol, + status: transactions.status, + buyerNetwork: transactions.buyerNetwork, + sellerNetwork: transactions.sellerNetwork, + merchantId: sellers.merchantId, + grossAmount: transactions.grossAmount, + netAmount: transactions.netAmount, + gasAllowance: transactions.gasAllowance, + pullTxHash: transactions.pullTxHash, + burnTxHash: transactions.burnTxHash, + mintTxHash: transactions.mintTxHash, + createdAt: transactions.createdAt, + settledAt: transactions.settledAt, + }) + .from(transactions) + .leftJoin(sellers, eq(sellers.id, transactions.sellerId)) + .where(where) + .orderBy(desc(transactions.createdAt)) + .limit(limit) + .offset(offset) + + const totalRows = await db + .select({ count: sql`COUNT(*)::text` }) + .from(transactions) + .leftJoin(sellers, eq(sellers.id, transactions.sellerId)) + .where(where) + + const items: TransactionItem[] = rows.map((r) => ({ + id: r.id, + type: normalizeType(r.type), + protocol: r.protocol, + status: r.status, + buyerNetwork: r.buyerNetwork, + sellerNetwork: r.sellerNetwork, + merchantId: r.merchantId, + grossAmount: r.grossAmount, + netAmount: r.netAmount, + gasAllowance: r.gasAllowance, + pullTxHash: r.pullTxHash, + burnTxHash: r.burnTxHash, + mintTxHash: r.mintTxHash, + createdAt: r.createdAt?.toISOString() ?? null, + settledAt: r.settledAt?.toISOString() ?? null, + })) + + return { items, total: Number(totalRows[0]?.count ?? '0'), limit, offset } +} + +// ---------------------------------------------------------------------------- +// 6. getCostComparison +// ---------------------------------------------------------------------------- + +export interface CostComparisonTier { + amount: string + cctpAllowance: string + percentAlternative: string + savingsVsPercent: string +} + +export interface CostComparisonResponse { + buyerNetwork: string + sellerNetwork: string + tiers: CostComparisonTier[] + notes: { + cctpSource: string + percentAssumption: string + } +} + +const COST_COMPARISON_TIERS = ['100000', '1000000', '100000000'] +const PERCENT_BPS = 100n // 1% = 100 bps +const PERCENT_MIN_FLOOR = 100000n // $0.10 baseline spread + +function computePercentAlternative(amount: string): string { + const gross = BigInt(amount) + const onePercent = (gross * PERCENT_BPS) / 10000n + return (onePercent > PERCENT_MIN_FLOOR ? onePercent : PERCENT_MIN_FLOOR).toString() +} + +export function getCostComparison(input: { + buyerNetwork: string + sellerNetwork: string +}): CostComparisonResponse { + const { buyerNetwork, sellerNetwork } = input + + const fromSlug = resolveSlugByCaip2(buyerNetwork) + const toSlug = resolveSlugByCaip2(sellerNetwork) + if (!fromSlug || !toSlug) { + throw new RouteNotConfiguredError(buyerNetwork, sellerNetwork) + } + + let allowance: string + try { + allowance = getGasAllowanceBySlug(fromSlug, toSlug) + } catch (err) { + if (err instanceof Error && err.message.startsWith('No gas schedule for')) { + throw new RouteNotConfiguredError(buyerNetwork, sellerNetwork) + } + throw err + } + + const tiers: CostComparisonTier[] = COST_COMPARISON_TIERS.map((amount) => { + const percent = computePercentAlternative(amount) + const savings = (BigInt(percent) - BigInt(allowance)).toString() + return { + amount, + cctpAllowance: allowance, + percentAlternative: percent, + savingsVsPercent: savings, + } + }) + + return { + buyerNetwork, + sellerNetwork, + tiers, + notes: { + cctpSource: 'gas-schedule.ts', + percentAssumption: '1% + $0.10 market-maker spread baseline', + }, + } +} diff --git a/packages/relay/src/bazaar/bazaar.routes.ts b/packages/relay/src/bazaar/bazaar.routes.ts index 528a241..e808d46 100644 --- a/packages/relay/src/bazaar/bazaar.routes.ts +++ b/packages/relay/src/bazaar/bazaar.routes.ts @@ -1,5 +1,31 @@ import { Elysia, t } from 'elysia' import { listResources } from './bazaar.service' +import { bazaarCacheKey, cached } from './cache' +import { + parseWindow, + getStats, + getRoutes, + getRankedResources, + getRankedSellers, + getTransactions, + getCostComparison, + RouteNotConfiguredError, +} from './aggregations' + +const CACHE_TTL = 60 +const DEFAULT_LIMIT = 20 +const DEFAULT_OFFSET = 0 +const MAX_LIMIT = 100 + +export function toLimitOffset(limitRaw: string | undefined, offsetRaw: string | undefined) { + const parsedLimit = limitRaw ? Number(limitRaw) : DEFAULT_LIMIT + const limit = Number.isFinite(parsedLimit) + ? Math.min(Math.max(parsedLimit, 1), MAX_LIMIT) + : DEFAULT_LIMIT + const parsedOffset = offsetRaw ? Number(offsetRaw) : DEFAULT_OFFSET + const offset = Number.isFinite(parsedOffset) ? Math.max(parsedOffset, 0) : DEFAULT_OFFSET + return { limit, offset } +} export const bazaarRoutes = new Elysia({ prefix: '/discovery' }).get( '/resources', @@ -22,3 +48,138 @@ export const bazaarRoutes = new Elysia({ prefix: '/discovery' }).get( }), }, ) + +export const bazaarEnrichedRoutes = new Elysia({ prefix: '/bazaar' }) + .get( + '/stats', + async ({ query }) => { + const window = parseWindow(query.window) + const { value } = await cached(bazaarCacheKey('stats', { window }), CACHE_TTL, () => + getStats({ window }), + ) + return value + }, + { + query: t.Object({ window: t.Optional(t.String()) }), + }, + ) + .get( + '/routes', + async ({ query }) => { + const window = parseWindow(query.window) + const { value } = await cached(bazaarCacheKey('routes', { window }), CACHE_TTL, () => + getRoutes({ window }), + ) + return value + }, + { + query: t.Object({ window: t.Optional(t.String()) }), + }, + ) + .get( + '/resources', + async ({ query }) => { + const window = parseWindow(query.window) + const rank = query.rank === 'volume' ? 'volume' : 'uses' + const { limit, offset } = toLimitOffset(query.limit, query.offset) + const params = { window, rank, network: query.network, q: query.q, limit, offset } + const { value } = await cached(bazaarCacheKey('resources', params), CACHE_TTL, () => + getRankedResources(params), + ) + return value + }, + { + query: t.Object({ + window: t.Optional(t.String()), + rank: t.Optional(t.String()), + network: t.Optional(t.String()), + q: t.Optional(t.String()), + limit: t.Optional(t.String()), + offset: t.Optional(t.String()), + }), + }, + ) + .get( + '/sellers', + async ({ query }) => { + const window = parseWindow(query.window) + const rank = query.rank === 'tx_count' ? 'tx_count' : 'volume' + const { limit, offset } = toLimitOffset(query.limit, query.offset) + const params = { window, rank, network: query.network, limit, offset } + const { value } = await cached(bazaarCacheKey('sellers', params), CACHE_TTL, () => + getRankedSellers(params), + ) + return value + }, + { + query: t.Object({ + window: t.Optional(t.String()), + rank: t.Optional(t.String()), + network: t.Optional(t.String()), + limit: t.Optional(t.String()), + offset: t.Optional(t.String()), + }), + }, + ) + .get( + '/transactions', + async ({ query }) => { + const { limit, offset } = toLimitOffset(query.limit, query.offset) + const window = query.window ? parseWindow(query.window) : undefined + const params = { + window, + merchantId: query.merchantId, + buyerNetwork: query.buyerNetwork, + sellerNetwork: query.sellerNetwork, + status: query.status, + protocol: query.protocol, + type: query.type, + limit, + offset, + } + const { value } = await cached(bazaarCacheKey('transactions', params), CACHE_TTL, () => + getTransactions(params), + ) + return value + }, + { + query: t.Object({ + window: t.Optional(t.String()), + merchantId: t.Optional(t.String()), + buyerNetwork: t.Optional(t.String()), + sellerNetwork: t.Optional(t.String()), + status: t.Optional(t.String()), + protocol: t.Optional(t.String()), + type: t.Optional(t.String()), + limit: t.Optional(t.String()), + offset: t.Optional(t.String()), + }), + }, + ) + .get( + '/cost-comparison', + ({ query, set }) => { + if (!query.buyerNetwork || !query.sellerNetwork) { + set.status = 400 + return { error: 'MISSING_PARAMS', message: 'buyerNetwork and sellerNetwork are required' } + } + try { + return getCostComparison({ + buyerNetwork: query.buyerNetwork, + sellerNetwork: query.sellerNetwork, + }) + } catch (err) { + if (err instanceof RouteNotConfiguredError) { + set.status = 404 + return { error: err.code, message: err.message } + } + throw err + } + }, + { + query: t.Object({ + buyerNetwork: t.Optional(t.String()), + sellerNetwork: t.Optional(t.String()), + }), + }, + ) diff --git a/packages/relay/src/bazaar/cache.ts b/packages/relay/src/bazaar/cache.ts new file mode 100644 index 0000000..54eb21e --- /dev/null +++ b/packages/relay/src/bazaar/cache.ts @@ -0,0 +1,25 @@ +import { redis } from '@402md/shared/cache' + +export interface CachedResult { + value: T + hit: boolean +} + +export async function cached( + key: string, + ttlSeconds: number, + compute: () => Promise, +): Promise> { + const raw = await redis.get(key) + if (raw) return { value: JSON.parse(raw) as T, hit: true } + const value = await compute() + await redis.set(key, JSON.stringify(value), 'EX', ttlSeconds) + return { value, hit: false } +} + +export function bazaarCacheKey(endpoint: string, params: Record): string { + const sortedKeys = Object.keys(params).sort() + const canonical: Record = {} + for (const k of sortedKeys) canonical[k] = params[k] ?? null + return `402md:bazaar:${endpoint}:${JSON.stringify(canonical)}` +} diff --git a/packages/relay/src/index.ts b/packages/relay/src/index.ts index c964c94..3fd50de 100644 --- a/packages/relay/src/index.ts +++ b/packages/relay/src/index.ts @@ -12,12 +12,15 @@ if (!envCheck.ok) { import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' import { swagger } from '@elysiajs/swagger' +import { html } from '@elysiajs/html' import { sql } from 'drizzle-orm' +import { LandingPage } from '@/views/landing/page' +import { DashboardPage } from '@/views/dashboard/page' import { sellersRoutes } from '@/sellers/sellers.routes' import { settlementsRoutes } from '@/settlements/settlements.routes' import { onrampRoutes } from '@/onramp/onramp.routes' import { stellarMppRoutes } from '@/mpp/stellar-mpp.routes' -import { bazaarRoutes } from '@/bazaar/bazaar.routes' +import { bazaarRoutes, bazaarEnrichedRoutes } from '@/bazaar/bazaar.routes' import { db } from '@402md/shared/db' import { redis } from '@402md/shared/cache' import { getTemporalClient } from '@/shared/temporal' @@ -25,6 +28,7 @@ import { checkRateLimit } from '@/shared/rate-limit' export const app = new Elysia() .use(cors()) + .use(html()) .use( swagger({ documentation: { @@ -50,9 +54,9 @@ export const app = new Elysia() .use(onrampRoutes) .use(stellarMppRoutes) .use(bazaarRoutes) - .get('/', () => Bun.file(new URL('../public/index.html', import.meta.url).pathname), { - detail: { hide: true }, - }) + .use(bazaarEnrichedRoutes) + .get('/', () => LandingPage(), { detail: { hide: true } }) + .get('/dashboard', () => DashboardPage(), { detail: { hide: true } }) .get('/cover.mp4', () => Bun.file(new URL('../public/cover.mp4', import.meta.url).pathname)) .get('/health', async () => { const checks = { diff --git a/packages/relay/src/shared/rate-limit.ts b/packages/relay/src/shared/rate-limit.ts index 30b7bf9..ffe4e8e 100644 --- a/packages/relay/src/shared/rate-limit.ts +++ b/packages/relay/src/shared/rate-limit.ts @@ -15,6 +15,12 @@ const RATE_LIMITS: Record = { '/mpp/config': { maxRequests: 100, windowSeconds: 60 }, '/mpp/charge': { maxRequests: 500, windowSeconds: 60 }, '/bazaar': { maxRequests: 100, windowSeconds: 60 }, + '/bazaar/stats': { maxRequests: 200, windowSeconds: 60 }, + '/bazaar/routes': { maxRequests: 200, windowSeconds: 60 }, + '/bazaar/resources': { maxRequests: 200, windowSeconds: 60 }, + '/bazaar/sellers': { maxRequests: 200, windowSeconds: 60 }, + '/bazaar/transactions': { maxRequests: 200, windowSeconds: 60 }, + '/bazaar/cost-comparison': { maxRequests: 200, windowSeconds: 60 }, } export async function checkRateLimit(path: string, ip: string): Promise { diff --git a/packages/relay/src/views/dashboard/client-script.ts b/packages/relay/src/views/dashboard/client-script.ts new file mode 100644 index 0000000..97f26c8 --- /dev/null +++ b/packages/relay/src/views/dashboard/client-script.ts @@ -0,0 +1,384 @@ +// Dashboard polling + render script. Rendered as a raw + + +) diff --git a/packages/relay/src/views/dashboard/styles.ts b/packages/relay/src/views/dashboard/styles.ts new file mode 100644 index 0000000..928f2e9 --- /dev/null +++ b/packages/relay/src/views/dashboard/styles.ts @@ -0,0 +1,443 @@ +export const dashboardStyles = /* css */ ` + main.dashboard { + padding-top: var(--space-xl); + padding-bottom: var(--space-2xl); + } + + /* ── Section rhythm ── + Hairline top + generous padding on each section creates editorial rhythm + without per-section decoration. Matches the landing's section pattern + but calibrated for data density. */ + main.dashboard section { + margin-bottom: 0; + padding-top: var(--space-xl); + padding-bottom: var(--space-xl); + border-top: 1px solid var(--border); + } + main.dashboard section:first-of-type { + padding-top: var(--space-md); + border-top: 0; + } + + main.dashboard h2 { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--signal-deep); + margin-bottom: var(--space-md); + } + + .section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-md); + flex-wrap: wrap; + } + .section-head h2 { + margin-bottom: 0; + } + + /* ── Snapshot readouts ── + Flat editorial row, no cards. Vertical hairlines separate columns; + the section's own rhythm handles top/bottom boundaries, so no extra + horizontals here — otherwise they stack with the next section's border. */ + .snapshot { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + } + .stat { + padding: 0 var(--space-lg) 0 0; + border-right: 1px solid var(--border); + } + .stat + .stat { + padding-left: var(--space-lg); + } + .stat:last-child { + border-right: 0; + padding-right: 0; + } + .stat .label { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6875rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-2); + margin-bottom: var(--space-xs); + } + .stat .value { + font-family: 'Bricolage Grotesque', sans-serif; + font-size: 1.75rem; + font-weight: 500; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; + line-height: 1.1; + } + .stat .sub { + font-size: 0.8125rem; + color: var(--text-2); + margin-top: var(--space-xs); + } + + /* ── Segmented window toggle ── */ + .window-toggle { + display: inline-flex; + gap: 2px; + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2px; + } + .window-toggle button { + background: transparent; + border: none; + font: inherit; + font-size: 0.8125rem; + color: var(--text-2); + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + transition: + color 150ms var(--ease-out-quart), + background-color 150ms var(--ease-out-quart); + } + .window-toggle button:hover:not(.active) { + color: var(--text); + } + .window-toggle button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + .window-toggle button.active { + background: var(--accent-soft); + color: var(--accent); + } + + /* ── Rank toggle (inline text-like) ── */ + .rank-toggle { + display: inline-flex; + gap: var(--space-xs); + font-size: 0.8125rem; + } + .rank-toggle button { + background: transparent; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 4px 10px; + font: inherit; + font-size: 0.8125rem; + color: var(--text-2); + cursor: pointer; + transition: + color 150ms var(--ease-out-quart), + border-color 150ms var(--ease-out-quart), + background-color 150ms var(--ease-out-quart); + } + .rank-toggle button:hover:not(.active) { + color: var(--text); + border-color: var(--border-mid); + } + .rank-toggle button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + .rank-toggle button.active { + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-soft); + } + + /* ── Per-chain grid ── */ + .chain-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: var(--space-sm); + } + .chain-card { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--space-md); + transition: border-color 150ms var(--ease-out-quart); + } + .chain-card:hover { + border-color: var(--border-mid); + } + .chain-card .chain-name { + font-weight: 500; + font-size: 0.875rem; + margin-bottom: var(--space-sm); + letter-spacing: -0.005em; + } + .chain-card .chain-metric { + font-size: 0.8125rem; + color: var(--text-2); + font-variant-numeric: tabular-nums; + } + .chain-card .chain-metric + .chain-metric { + margin-top: 2px; + } + .chain-card .chain-metric strong { + color: var(--text); + font-family: 'JetBrains Mono', monospace; + font-weight: 500; + } + + /* ── Route matrix ── */ + .route-matrix { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); + } + table.matrix { + width: 100%; + border-collapse: collapse; + font-size: 0.75rem; + font-variant-numeric: tabular-nums; + table-layout: fixed; + } + table.matrix th, + table.matrix td { + border: 1px solid var(--border); + padding: 8px 10px; + text-align: center; + white-space: nowrap; + } + table.matrix th { + background: var(--bg-alt); + color: var(--text-2); + font-weight: 500; + font-family: 'JetBrains Mono', monospace; + font-size: 0.6875rem; + letter-spacing: 0.02em; + } + table.matrix thead th { + border-top: 0; + } + table.matrix tr > th:first-child, + table.matrix tr > td:first-child { + border-left: 0; + } + table.matrix tr > th:last-child, + table.matrix tr > td:last-child { + border-right: 0; + } + table.matrix tbody tr:last-child th, + table.matrix tbody tr:last-child td { + border-bottom: 0; + } + table.matrix th.corner { + background: transparent; + border: none; + } + table.matrix td { + font-family: 'JetBrains Mono', monospace; + transition: filter 150ms var(--ease-out-quart); + } + table.matrix td:not(.empty):hover { + filter: brightness(0.96); + } + table.matrix td.same-chain { + border-style: dashed; + } + table.matrix td.empty { + color: var(--text-3); + } + + /* ── Data tables ── */ + table.list { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; + font-variant-numeric: tabular-nums; + } + table.list th, + table.list td { + text-align: left; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + } + table.list th { + font-family: 'JetBrains Mono', monospace; + font-weight: 500; + color: var(--text-3); + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.08em; + } + table.list tbody tr { + transition: background-color 120ms var(--ease-out-quart); + } + table.list tbody tr:hover { + background: var(--bg-alt); + } + table.list tbody tr:last-child td { + border-bottom: 0; + } + table.list td.num { + font-family: 'JetBrains Mono', monospace; + text-align: right; + } + table.list td.mono { + font-family: 'JetBrains Mono', monospace; + font-size: 0.8125rem; + color: var(--text); + } + table.list a { + color: var(--accent); + transition: opacity 150ms; + } + table.list a:hover { + opacity: 0.7; + text-decoration: none; + } + + /* ── Badges ── */ + .badge { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.6875rem; + font-family: 'JetBrains Mono', monospace; + font-weight: 500; + letter-spacing: 0.02em; + white-space: nowrap; + } + .badge.same { background: var(--green-soft); color: var(--green); } + .badge.cross { background: var(--accent-soft); color: var(--accent); } + .badge.x402 { background: var(--purple-soft); color: var(--purple); } + .badge.mpp { background: var(--warn-soft); color: var(--warn-deep); } + .badge.status-settled { background: var(--green-soft); color: var(--green); } + .badge.status-pending { background: var(--bg-alt); color: var(--text-2); } + .badge.status-failed { + background: var(--error-soft); + color: var(--error); + } + + /* ── Cost comparison panel ── */ + .panel { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--space-lg); + } + .panel-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-md); + flex-wrap: wrap; + margin-bottom: var(--space-md); + } + .panel-head strong { + font-family: 'Bricolage Grotesque', sans-serif; + font-weight: 500; + font-size: 1rem; + letter-spacing: -0.01em; + color: var(--text); + } + .panel-caption { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6875rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-3); + } + .panel table.list tbody tr:hover { + background: transparent; + } + .panel table.list th, + .panel table.list td { + border-bottom-color: var(--border-mid); + } + .cost-note { + font-size: 0.75rem; + color: var(--text-3); + margin-top: var(--space-md); + line-height: 1.55; + } + + /* ── Loading / empty / error ── */ + .loading { + padding: var(--space-lg); + color: var(--text-3); + font-size: 0.8125rem; + font-family: 'JetBrains Mono', monospace; + letter-spacing: 0.02em; + animation: dashboard-pulse 1.4s var(--ease-out-quart) infinite; + } + .empty, + .error { + padding: var(--space-lg); + text-align: center; + color: var(--text-3); + font-size: 0.8125rem; + background: var(--bg-alt); + border: 1px dashed var(--border-mid); + border-radius: var(--radius); + } + .error { + color: var(--warn-deep); + border-color: var(--warn-deep); + background: var(--warn-soft); + } + + @keyframes dashboard-pulse { + 0%, 100% { opacity: 0.55; } + 50% { opacity: 1; } + } + + /* ── Footer ── */ + footer.site { + margin-top: var(--space-xl); + padding-top: var(--space-lg); + border-top: 1px solid var(--border); + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + color: var(--text-3); + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--space-sm); + letter-spacing: 0.01em; + } + + @media (prefers-reduced-motion: reduce) { + .loading { + animation: none; + opacity: 0.7; + } + table.matrix td, + table.list tbody tr, + .chain-card, + .window-toggle button, + .rank-toggle button { + transition: none; + } + } + + @media (max-width: 480px) { + main.dashboard section { + padding-top: var(--space-lg); + padding-bottom: var(--space-lg); + } + .snapshot { + grid-template-columns: 1fr; + } + .stat { + padding: var(--space-md) 0; + border-right: 0; + border-bottom: 1px solid var(--border); + } + .stat:last-child { + border-bottom: 0; + } + .stat .value { + font-size: 1.5rem; + } + } +` diff --git a/packages/relay/src/views/landing/assets.d.ts b/packages/relay/src/views/landing/assets.d.ts new file mode 100644 index 0000000..c8f91b0 --- /dev/null +++ b/packages/relay/src/views/landing/assets.d.ts @@ -0,0 +1,14 @@ +declare module '*.html' { + const content: string + export default content +} + +declare module '*.css' { + const content: string + export default content +} + +declare module '*.txt' { + const content: string + export default content +} diff --git a/packages/relay/src/views/landing/content.html b/packages/relay/src/views/landing/content.html new file mode 100644 index 0000000..a4a206d --- /dev/null +++ b/packages/relay/src/views/landing/content.html @@ -0,0 +1,624 @@ +
+ +
+
+
+

+ Any chain in, + your chain out. +

+ +

+ Cross-chain USDC settlement for x402 (Coinbase's HTTP 402 protocol) and MPP (Machine + Payments Protocol). Buyer pays on Solana, seller receives on Stellar. No SDK, no + dashboard, no code changes. +

+ + +
+
+
+ + +
+
+ +
+
+ + terminal +
+
# 1. Register your wallet (one-time)
+curl -X POST https://facilitator.402.md/register \
+  -H "Content-Type: application/json" \
+  -d '{"wallet": "GSELLER...", "network": "stellar:pubnet"}'
+
+# 2. Use standard @x402/express — zero 402md code
+import { paymentMiddleware } from "@x402/express"
+
+app.use(paymentMiddleware({
+  "GET /your-endpoint": {
+    accepts: [
+      { scheme: "exact", network: "eip155:8453",    payTo: "0x4C95...c3b9", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Base
+      { scheme: "exact", network: "eip155:1",       payTo: "0x8aF2...71d4", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Ethereum
+      { scheme: "exact", network: "eip155:10",      payTo: "0x2E19...a0b7", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Optimism
+      { scheme: "exact", network: "eip155:42161",   payTo: "0xF1c3...9e82", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Arbitrum
+      { scheme: "exact", network: "eip155:59144",   payTo: "0x7b45...d60a", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Linea
+      { scheme: "exact", network: "eip155:130",     payTo: "0xD309...4cf1", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Unichain
+      { scheme: "exact", network: "eip155:480",     payTo: "0x5a6e...bb28", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // World Chain
+      { scheme: "exact", network: "solana:mainnet", payTo: "4r7q...BDP5", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Solana
+      { scheme: "exact", network: "stellar:pubnet", payTo: "GAX5...5HZR", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Stellar
+    ],
+  },
+}, "https://facilitator.402.md"))
+
+# Buyers on any of the 9 supported chains can now pay you.
+
+
+
+ + +
+
+ +

One chain is not enough

+

+ x402 lets any API charge for access. But every facilitator today locks sellers to a single + chain — and leaves money on the table. +

+ +
+
+

Chain lock-in

+

+ 30 x402 facilitators, all single-chain. Buyer on Solana, seller on Stellar? Payment + fails. Revenue lost silently. +

+
+
+

Wrong tokens, wrong path

+

+ DEX-based bridges mean wrapped USDC, slippage, and risk. Native USDC via CCTP V2 is the + only clean path. +

+
+
+

Fragmented protocols

+

x402 and MPP serve different ecosystems. No one speaks both — until now.

+
+
+

Agents don't choose chains

+

+ AI agents need to pay, not navigate blockchain infrastructure. The payment layer should + be invisible. +

+
+
+
+
+ + +
+
+ +

Three steps. That's it.

+ +
+
+
Seller
+

One curl to start receiving

+
+
+ 01 +
+

Register your wallet

+

+ POST /register with your address and chain. Get + facilitator addresses on all supported chains. +

+
+
+
+ 02 +
+

Use standard x402

+

+ Add @x402/express middleware. Point + payTo to your facilitator address. Zero 402md + dependencies. +

+
+
+
+ 03 +
+

Receive USDC on your chain

+

+ Buyer pays on any chain. 402md settles via CCTP V2. Native USDC, directly to your + wallet. +

+
+
+
+
+ +
+
Buyer / Agent
+

Pay from any chain

+
+
+ 01 +
+

Discover the seller

+

+ GET the API. Receive a 402 response with facilitator + addresses on your chain. +

+
+
+
+ 02 +
+

Pay with USDC

+

+ Sign a standard x402 payment on Base + Solana or + Stellar. No bridge, no swap. +

+
+
+
+ 03 +
+

Get the resource

+

+ Payment verified in milliseconds. Resource delivered instantly. Settlement runs in + the background. +

+
+
+
+
+
+
+
+ + +
+
+ +

Opinionated choices, for good reasons

+

+ Every decision optimizes for one thing: the seller should do zero extra work to accept + payments from any chain. +

+ +
+
+

Non-custodial

+

CCTP V2 mints USDC directly to the seller's wallet. 402md never holds your funds.

+
+
+

Free

+

+ 0% platform fee. Only network gas deducted from settlement. Fixed schedule, no + surprises. +

+
+
+

Zero dependencies

+

Sellers use standard @x402/express from Coinbase. No 402md SDK, no custom packages.

+
+
+ +
+
+

Dual protocol

+

x402 and MPP through one facilitator. Two ecosystems, one registration.

+
+
+

Native USDC only

+

Circle CCTP V2 burns and mints real USDC. No wrapped tokens, no slippage.

+
+
+

Three chains

+

Base, Solana, Stellar. Adding a new EVM chain is a config change, not a deploy.

+
+
+ + +
+
+ + +
+
+ +

Free. Seriously.

+

+ Zero platform fee. The only cost is a fixed gas allowance per route, deducted from the + seller's payout. Here's what that looks like on a $1.00 payment. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RouteSeller receivesGas allowance
+ Stellar → + Stellar + $0.99999$0.00001 0.001%
+ Solana → + Solana + $0.9990$0.0010 0.10%
+ Base → + Base + $0.9980$0.0020 0.20%
+ Stellar → + Solana + $0.9985$0.0015 0.15%
+ Solana → + Stellar + $0.9975$0.0025 0.25%
+ Stellar → + Base + $0.9975$0.0025 0.25%
+ Base → + Stellar + $0.9968$0.0032 0.32%
+ Base → + Solana + $0.9965$0.0035 0.35%
+ Solana → + Base + $0.9965$0.0035 0.35%
+
+
+
+ + +
+
+ +

Common questions

+ +
+
+ What is Facilitator by 402md? +
+ A cross-chain USDC settlement facilitator built by 402md for the x402 and MPP protocols. + Sellers register once and receive payments from buyers on any supported chain — Base, + Solana, or Stellar. +
+
+ +
+ How is this different from other x402 facilitators? +
+ Existing facilitators are single-chain. If a buyer is on Solana and the seller is on + Stellar, the payment fails. 402md bridges that gap using Circle CCTP V2 — atomic, + non-custodial, native USDC. No wrapped tokens, no slippage. +
+
+ +
+ What chains are supported? +
+ Base (EVM), Solana, and Stellar — all connected via Circle CCTP V2. Adding new EVM + chains requires only a config update, no contract deployments. +
+
+ +
+ How much does it cost? +
+ Zero platform fee. The only deduction is a fixed gas allowance per route — for example, + $0.0032 for Base→Stellar on a $1.00 payment. Same-chain payments cost even less + ($0.00001 for Stellar→Stellar). +
+
+ +
+ Is it custodial? +
+ No. Cross-chain settlements use Circle CCTP V2, which mints USDC directly to the + seller's wallet. 402md never holds seller funds. The facilitator only retains the gas + allowance needed to execute the settlement. +
+
+ +
+ How long does settlement take? +
+ Same-chain: under 5 seconds. Stellar or Solana cross-chain: 5-30 seconds. EVM + cross-chain: 15-19 minutes, limited by block finality. The buyer gets the resource + instantly — settlement happens in the background. +
+
+ +
+ Do I need to install any 402md package? +
+ No. Sellers use the standard @x402/express SDK from Coinbase. The only 402md-specific + step is a one-time POST /register call — after that, everything works with standard x402 + tooling. +
+
+ +
+ What about same-chain payments? +
+ If buyer and seller are on the same chain, 402md does a direct USDC transfer — no bridge + involved. Faster settlement, lower gas, same API. +
+
+ +
+ What is MPP? +
+ Machine Payments Protocol, developed by Stellar and used by services like OpenAI, + Anthropic, and Dune. 402md supports MPP alongside x402, enabling AI agents from both + ecosystems to transact through one facilitator. +
+
+
+
+
+ + +
+
+ +

One curl. All chains.

+

+ Register your wallet and start accepting cross-chain USDC in under a minute. No dashboard, + no login, no SDK. +

+ +
+ USDC + Base + Solana + Stellar +
+ +
+
+
+ +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

0x followed by 40 hex characters

+ +
+ + API docs ↗ +
+
+
+ + +
+
+
+
+
+ + + diff --git a/packages/relay/src/views/landing/page.test.ts b/packages/relay/src/views/landing/page.test.ts new file mode 100644 index 0000000..2f042ea --- /dev/null +++ b/packages/relay/src/views/landing/page.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'bun:test' +import '@/shared/test-helpers' +import { LandingPage } from './page' + +describe('LandingPage', () => { + const html = LandingPage() + + test('renders as a large HTML string with doctype', () => { + expect(typeof html).toBe('string') + expect(html.startsWith('')).toBe(true) + expect(html.length).toBeGreaterThan(50_000) + }) + + test('includes the shared Nav (same component as the dashboard)', () => { + expect(html).toContain('