diff --git a/merchant.js b/merchant.js new file mode 100644 index 0000000..c4ba2b3 --- /dev/null +++ b/merchant.js @@ -0,0 +1,30 @@ +// Who sells this, for x402 discovery lists. GoPlausible's catalog reads the +// "x402-merchant" extension of a 402 challenge (name, website, logo, categories) +// when it settles a payment, and shows it on the merchant: one per pay-to +// address, which all four fizzl.eu services share on Algorand. So the info is +// the same on every fizzl.eu service. +const MERCHANT = { + name: 'Fizzl', + website: 'https://fizzl.eu', + logo: 'https://fizzl.eu/logo-512.png', + categories: ['ai-agents', 'security', 'trading-signals', 'developer-tools', 'x402', 'algorand'], +}; + +const merchantExtension = { + 'x402-merchant': { + info: MERCHANT, + schema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + website: { type: 'string' }, + logo: { type: 'string' }, + categories: { type: 'array', items: { type: 'string' } }, + }, + }, + }, +}; + +module.exports = { MERCHANT, merchantExtension }; diff --git a/server.js b/server.js index 95930f3..c845a65 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ const { ExactSvmScheme } = require('@x402/svm/exact/server'); const { createFacilitatorConfig } = require('@coinbase/x402'); const { fizzlCors } = require('./fizzl-cors'); const { declareDiscoveryExtension } = require('@x402/extensions/bazaar'); +const { merchantExtension } = require('./merchant'); const { McpServer, createMcpHandler } = require('@modelcontextprotocol/server'); const { z } = require('zod/v4'); const { createMediaCache } = require('./media'); @@ -241,14 +242,14 @@ if (x402PayTo) { description: "Check an EVM wallet's live token/NFT approvals and get a plain-language verdict (SAFE, CAUTION or RISK) with an explanation", mimeType: 'application/json', ...serviceMetadata, - extensions: checkWalletDiscovery + extensions: { ...checkWalletDiscovery, ...merchantExtension } }, 'POST /api/explain': { accepts: acceptsFor(x402ExplainPrice), description: 'What would signing this allow? Paste a wallet approval, transaction or typed-data signature before someone signs it: returns a SAFE, CAUTION or RISK verdict with a short plain-language explanation of what it gives away', mimeType: 'application/json', ...serviceMetadata, - extensions: explainDiscovery + extensions: { ...explainDiscovery, ...merchantExtension } } }, x402Server)))); diff --git a/test/algorand.test.js b/test/algorand.test.js index d58023f..ea05fd0 100644 --- a/test/algorand.test.js +++ b/test/algorand.test.js @@ -36,7 +36,10 @@ async function acceptsWith(env) { for (let i = 0; i < 50; i++) { try { const res = await fetch(`http://127.0.0.1:${port}/api/check-wallet`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }); - if (res.status === 402) return JSON.parse(Buffer.from(res.headers.get('payment-required'), 'base64').toString()).accepts; + if (res.status === 402) { + const challenge = JSON.parse(Buffer.from(res.headers.get('payment-required'), 'base64').toString()); + return Object.assign(challenge.accepts, { extensions: challenge.extensions }); + } } catch { /* not up yet */ } await new Promise((r) => setTimeout(r, 200)); } @@ -57,6 +60,9 @@ test('with ALGORAND_PAY_TO: $0.04 in USDC (ASA 31566704) on Algorand, with the A assert.equal(algo.amount, '40000'); assert.equal(algo.extra.feePayer, FEE_PAYER, 'not the URL facilitator\'s'); assert.ok(accepts.some((a) => a.network === 'eip155:8453'), 'Base still offered'); + // GoPlausible names the seller (one merchant per Algorand pay-to) from this. + assert.equal(accepts.extensions['x402-merchant'].info.name, 'Fizzl'); + assert.equal(accepts.extensions['x402-merchant'].info.logo, 'https://fizzl.eu/logo-512.png'); }); test('without ALGORAND_PAY_TO, or "off": no Algorand', async () => {