diff --git a/server.js b/server.js index ce86c4e..feb9507 100644 --- a/server.js +++ b/server.js @@ -22,6 +22,7 @@ const { describePlainTextCall } = require('./usage'); const { trustProxyHops } = require('./proxy'); const { onPublicHost } = require('./public-host'); const { createMppPay, unlessMppPaid, addMppOffers } = require('./mpp-pay'); +const { wellKnown } = require('./well-known'); const app = express(); const PORT = process.env.PORT || 3000; @@ -706,26 +707,7 @@ app.all('/mcp', async (req, res) => { // x402scan and similar indexers read {version: 1, resources: [url, ...]}; // methods, prices and schemas come from /openapi.json (x-payment-info). const PUBLIC_ORIGIN = 'https://plaintext.fizzl.eu'; -const X402_WELL_KNOWN_MANIFEST = { - version: 1, - resources: [`${PUBLIC_ORIGIN}/api/check-wallet`, `${PUBLIC_ORIGIN}/api/explain`], - x402Version: 2, - kind: 'resource-server', - name: 'PlainText — wallet approval translator', - serviceName: 'PlainText', - category: 'security', - iconUrl: `${PUBLIC_ORIGIN}/icon.png`, - tags: ['wallet-security', 'approvals', 'crypto', 'explainer'], - description: "Checks a wallet's token/NFT approvals via GoPlus and explains risk in plain language.", - endpoints: [ - { url: `${PUBLIC_ORIGIN}/api/check-wallet`, method: 'POST', description: 'Explain wallet token approvals in plain language' }, - { url: `${PUBLIC_ORIGIN}/api/explain`, method: 'POST', description: 'What would signing this allow? A SAFE, CAUTION or RISK verdict with a plain-language explanation of a pasted approval, transaction or signature' } - ], - openapi: `${PUBLIC_ORIGIN}/openapi.json`, - docs: `${PUBLIC_ORIGIN}/openapi.json`, - contact: 'Fizzl13@protonmail.com', - updated: '2026-09-23T00:00:00Z' -}; +const X402_WELL_KNOWN_MANIFEST = wellKnown(PUBLIC_ORIGIN); // Instructions an AI agent can read and follow ("Connect to plaintext.fizzl.eu/skill.md"). app.get('/skill.md', (_req, res) => res.set('cache-control', 'public, max-age=300').type('text/markdown; charset=utf-8').sendFile(path.join(__dirname, 'public', 'skill.md'))); diff --git a/test/well-known.test.js b/test/well-known.test.js new file mode 100644 index 0000000..ce0898c --- /dev/null +++ b/test/well-known.test.js @@ -0,0 +1,19 @@ +// /.well-known/x402 (well-known.js): the paid resources as { url, method, name, description }, as the XRPL AI Hub +// reads them, and the endpoints list for existing readers. +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { wellKnown } = require('../well-known'); + +test('/.well-known/x402: each paid resource with a short name and a one-line description', () => { + const doc = wellKnown('https://plaintext.fizzl.eu'); + assert.equal(doc.version, 1); + assert.equal(doc.name, 'PlainText — wallet approval translator'); + assert.deepEqual(doc.resources.map((r) => [r.url, r.method]), [['https://plaintext.fizzl.eu/api/check-wallet', 'POST'], ['https://plaintext.fizzl.eu/api/explain', 'POST']]); + assert.equal(new Set(doc.resources.map((r) => r.name)).size, doc.resources.length, 'names are unique'); + for (const r of doc.resources) { + assert.ok(r.name.length > 0 && r.name.length <= 40, r.url); + assert.ok(r.description.length >= 40 && r.description.length <= 200, r.url); + } + assert.deepEqual(doc.endpoints.map((e) => e.url), doc.resources.map((r) => r.url), 'the endpoints list stays'); + assert.match(doc.description, /XRP Ledger/); +}); diff --git a/well-known.js b/well-known.js new file mode 100644 index 0000000..7309d0d --- /dev/null +++ b/well-known.js @@ -0,0 +1,30 @@ +// /.well-known/x402: the paid resources on this origin, for x402 indexers. Each resource carries a short name and a +// one-line description: the XRPL AI Hub (xrpl-ai.org) lists each resource by those (without them it shows the long +// 402 text). The endpoints list stays for existing readers. +function wellKnown(origin) { + return { + version: 1, + resources: [ + { url: `${origin}/api/check-wallet`, method: 'POST', name: 'Wallet approval check', description: "A wallet's open token and NFT approvals (via GoPlus), with a SAFE, CAUTION or RISK verdict explained in plain language." }, + { url: `${origin}/api/explain`, method: 'POST', name: 'Signature explainer', description: 'What would signing this allow? A SAFE, CAUTION or RISK verdict and a plain-language explanation of a pasted approval, transaction or signature.' }, + ], + x402Version: 2, + kind: 'resource-server', + name: 'PlainText — wallet approval translator', + serviceName: 'PlainText', + category: 'security', + iconUrl: `${origin}/icon.png`, + tags: ['wallet-security', 'approvals', 'crypto', 'explainer'], + description: "Checks a wallet's token/NFT approvals via GoPlus and explains risk in plain language. Pay in USDC on Base, Solana or Algorand, or RLUSD on the XRP Ledger.", + endpoints: [ + { url: `${origin}/api/check-wallet`, method: 'POST', description: 'Explain wallet token approvals in plain language' }, + { url: `${origin}/api/explain`, method: 'POST', description: 'What would signing this allow? A SAFE, CAUTION or RISK verdict with a plain-language explanation of a pasted approval, transaction or signature' }, + ], + openapi: `${origin}/openapi.json`, + docs: `${origin}/openapi.json`, + contact: 'Fizzl13@protonmail.com', + updated: '2026-10-13T00:00:00Z', + }; +} + +module.exports = { wellKnown };