Skip to content

Commit 4406403

Browse files
author
S. K. Rotwang MMMMMMMMM
committed
Discovery: MCP Registry server.json + x402 bazaar listing; Telegram contact
- server.json: remote MCP manifest (io.github.rotwang9000/seneschal-data), validated against registry.modelcontextprotocol.io. Ready to publish. - x402.js: decorate every paid route with a bazaar discovery extension so the openx402 facilitator catalogues us in /discovery/resources on the next settlement. discoveryConfigForRouteKey() is pure + tested; the extension loads lazily with the rest of the x402 stack. - /.well-known/x402: enrich service block with description, control_panel, stats, Telegram contact, and a products[] array (Private Watch first). - README: lead with Private Watch + paid tiers; tools 9->18; tests 75->539; add panel row + MCP-Registry note; Discord -> Telegram (@OrknetP). - docs/index.html: fair-use contact Discord -> Telegram. - add @x402/extensions as an explicit dependency (was transitive). - 539/539 tests green.
1 parent 83065ae commit 4406403

8 files changed

Lines changed: 179 additions & 15 deletions

File tree

README.md

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,32 @@
33
[![ci](https://github.com/Rotwang9000/seneschal-data-api/actions/workflows/ci.yml/badge.svg)](https://github.com/Rotwang9000/seneschal-data-api/actions/workflows/ci.yml)
44
[![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
55

6-
Free, public, read-only REST + [Model Context Protocol](https://modelcontextprotocol.io)
6+
Free, public REST + [Model Context Protocol](https://modelcontextprotocol.io)
77
server exposing real-time and historical DeFi liquidation telemetry for
88
Aave, Morpho, Spark and Compound on Ethereum mainnet, plus block-builder
99
market share data from the operator's own slot-by-slot shadow recorder.
1010

11+
Two paid tiers sit on top, both billed per call over
12+
[x402](https://docs.x402.org) micropayments (USDC on Base — no account, no
13+
API key):
14+
15+
- **Private Watch** *(flagship)* — give us a Monero or Zcash **view key**
16+
and a webhook URL; we watch the chain on our own full nodes and POST you
17+
an HMAC-signed event for every inbound payment. Credit-metered
18+
($0.02/day + $0.005/call), so small receivers pay pennies and you never
19+
run a node. Drive it from the API, the MCP tools, or the
20+
[WalletConnect control panel](https://panel.seneschal.space).
21+
- **Premium data** — expected-value-ranked liquidation opportunities,
22+
per-builder bid distributions, and Penny Oracle atomic single-fact
23+
endpoints (DeFi + Monero/Zcash) from $0.001/call.
24+
1125
## Live endpoints
1226

1327
| What | URL | Auth |
1428
|-------------------------------|----------------------------------|----------|
1529
| REST API | `https://api.seneschal.space` | None |
1630
| MCP (Streamable HTTP) | `https://mcp.seneschal.space` | None |
31+
| Control panel (Private Watch) | `https://panel.seneschal.space` | Wallet |
1732
| Docs | `https://docs.seneschal.space` | - |
1833
| Live stats dashboard | `https://stats.seneschal.space` | - |
1934

@@ -42,7 +57,13 @@ Add this to your MCP client config (e.g. `~/.cursor/mcp.json`):
4257
}
4358
```
4459

45-
Nine tools become available to your agent:
60+
This server is published to the official
61+
[MCP Registry](https://registry.modelcontextprotocol.io) as
62+
`io.github.rotwang9000/seneschal-data` (see [`server.json`](server.json)),
63+
so MCP-aware clients and aggregators can discover it automatically.
64+
65+
Eighteen tools become available to your agent — free read tools, plus
66+
paid tools that hand back the exact URL + body to settle over x402:
4667

4768
| Tool | Purpose |
4869
|--------------------------------------|----------------------------------------------------------------------|
@@ -55,6 +76,15 @@ Nine tools become available to your agent:
5576
| `seneschal_builder_leaderboard` | Ethereum builder market share (24h, 7d, 30d, all-time) |
5677
| `seneschal_stats_overview` | Aggregate snapshot powering the public dashboard, incl. operator activity (counts only — no profit fields) |
5778
| `seneschal_flashloan_providers` | Curated catalogue of mainnet flash-loan providers, incl. LP-side commit-capital paths where applicable |
79+
| `seneschal_paywall_info` | Free metadata for the x402 paywall (network, recipient, per-call price) |
80+
| `seneschal_premium_opportunities` | EV-ranked at-risk borrowers with realised market intel *(x402, paid)* |
81+
| `seneschal_premium_builder_stats` | Per-builder bid distribution + hourly slot histogram for bundle pricing *(x402, paid)* |
82+
| `seneschal_q` | Penny Oracle dispatcher — atomic single facts across DeFi + Monero/Zcash, $0.001/call *(x402, paid)* |
83+
| `seneschal_private_watch_info` | Free metadata for Private Watch: meter, supported chains, NFPT upstream health, security notes |
84+
| `seneschal_private_watch_create` | Subscribe an XMR/ZEC view key to webhook payment monitoring *(x402, paid)* |
85+
| `seneschal_private_watch_topup` | URL + body to top up an existing watch's credit ($0.10 / $1 / $5 tiers) |
86+
| `seneschal_private_watch_historical` | One-off paid scan returning spendable + spent notes for a view key *(x402, paid; key never persists)* |
87+
| `seneschal_private_watch_derive_viewkey` | Free, rate-limited Zcash UFVK derivation from a BIP-39 mnemonic (with a loud security warning) |
5888

5989
## REST endpoints
6090

@@ -123,11 +153,13 @@ producing the same shapes you can point this server at it.
123153
npm test
124154
```
125155

126-
75 jest tests covering the query layer (in-memory SQLite fixtures), the
127-
Fastify REST routes (via `fastify.inject`), and the MCP server (both
156+
539 jest tests covering the query layer (in-memory SQLite fixtures), the
157+
Fastify REST routes (via `fastify.inject`), the MCP server (both
128158
in-process via `InMemoryTransport` and end-to-end via
129-
`StreamableHTTPClientTransport`). Plus `test/live-smoke.mjs` which
130-
exercises the live `mcp.seneschal.space` endpoint over Streamable HTTP.
159+
`StreamableHTTPClientTransport`), the x402 paywall + bazaar-discovery
160+
wiring, the Private Watch credit meter / poller / surge pricing, and the
161+
ops watchdog. Plus `test/live-smoke.mjs` which exercises the live
162+
`mcp.seneschal.space` endpoint over Streamable HTTP.
131163

132164
## Architecture
133165

@@ -190,6 +222,7 @@ budget a session before opening a paid request.
190222
- **Direct tips**: ETH / BTC addresses are surfaced on
191223
[stats.seneschal.space](https://stats.seneschal.space) once the
192224
operator sets `SENESCHAL_DONATE_ETH` / `SENESCHAL_DONATE_BTC`.
225+
- **Questions / allow-list bumps**: Telegram [`@OrknetP`](https://t.me/OrknetP).
193226

194227
Seneschal runs on a single Helsinki box; every cent helps keep it
195228
online.
@@ -202,5 +235,5 @@ MIT — see [LICENSE](LICENSE).
202235

203236
Seneschal is a single-operator Ethereum block builder and searcher
204237
running an `rbuilder` fork from a co-located server in Helsinki.
205-
Builder on-chain extra_data is `Seneschal/0.1`. Contact `@Rotwang9000`
206-
on Discord.
238+
Builder on-chain extra_data is `Seneschal/0.1`. Contact
239+
[`@OrknetP`](https://t.me/OrknetP) on Telegram (checked periodically).

docs/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ <h2>Data freshness &amp; coverage</h2>
880880

881881
<h2>Limits, terms, fair use</h2>
882882
<ul>
883-
<li>120 req/min per IP, soft. Heavy users should ask for an allow-list bump on Discord.</li>
883+
<li>120 req/min per IP, soft. Heavy users should ask for an allow-list bump on Telegram (<a href="https://t.me/OrknetP"><code>@OrknetP</code></a>).</li>
884884
<li>No SLA. This is a free best-effort service. Expect occasional restarts.</li>
885885
<li>Use of the data must not violate any privacy or legal restrictions in your jurisdiction. We surface only on-chain data and our own observations.</li>
886886
<li>Crediting Seneschal (<code>https://seneschal.space</code>) in derived work is appreciated but not required.</li>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@modelcontextprotocol/sdk": "^1.29.0",
2121
"@x402/core": "^2.12.0",
2222
"@x402/evm": "^2.12.0",
23+
"@x402/extensions": "^2.12.0",
2324
"@x402/fastify": "^2.12.0",
2425
"@x402/paywall": "^2.12.0",
2526
"better-sqlite3": "^12.10.0",

server.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3+
"name": "io.github.rotwang9000/seneschal-data",
4+
"title": "Seneschal Data",
5+
"description": "Live DeFi liquidation, Ethereum builder & Monero/Zcash data over MCP. Free, no key; x402 premium.",
6+
"version": "0.1.0",
7+
"websiteUrl": "https://seneschal.space",
8+
"repository": {
9+
"url": "https://github.com/Rotwang9000/seneschal-data-api",
10+
"source": "github"
11+
},
12+
"remotes": [
13+
{
14+
"type": "streamable-http",
15+
"url": "https://mcp.seneschal.space/"
16+
}
17+
]
18+
}

src/rest-server.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,10 +1100,29 @@ export async function buildApp(options = {}) {
11001100
...paywallSummary,
11011101
service: {
11021102
name: 'Seneschal Data API',
1103+
description: 'On-chain DeFi liquidation + Ethereum builder telemetry and Monero/Zcash payment-watching, payable per call over x402. Free read tier, no API key.',
11031104
homepage: 'https://seneschal.space',
1104-
docs: 'https://docs.seneschal.space/#premium-tier-x402',
1105+
docs: 'https://docs.seneschal.space',
11051106
api_root: 'https://api.seneschal.space',
1106-
mcp: 'https://mcp.seneschal.space'
1107+
mcp: 'https://mcp.seneschal.space',
1108+
control_panel: 'https://panel.seneschal.space',
1109+
stats: 'https://stats.seneschal.space',
1110+
contact: 'https://t.me/OrknetP',
1111+
products: [
1112+
{
1113+
name: 'Private Watch',
1114+
summary: 'Watch a Monero or Zcash view key for inbound payments and receive an HMAC-signed webhook on each one. Credit-metered (per-day + per-call), no node to run.',
1115+
start: 'POST /v1/private/watch',
1116+
info: 'https://api.seneschal.space/v1/private/info',
1117+
panel: 'https://panel.seneschal.space'
1118+
},
1119+
{
1120+
name: 'DeFi + builder data',
1121+
summary: 'Real-time at-risk borrowers across Aave/Morpho/Spark/Compound, Ethereum builder market share, and Monero/Zcash chain facts. Free read tier; x402 premium feeds + Penny Oracle atomic facts.',
1122+
start: 'GET /v1/premium/opportunities',
1123+
info: 'https://api.seneschal.space/v1/paywall'
1124+
}
1125+
]
11071126
}
11081127
};
11091128
});

src/x402.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,25 @@ function envKeyToCfg(envKey) {
195195
return parts.map((p, i) => i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)).join('');
196196
}
197197

198+
/**
199+
* Map a route key ("METHOD /path") to the minimal bazaar-discovery
200+
* config we hand `declareDiscoveryExtension`. Body methods need an
201+
* explicit `bodyType` (that's how the helper discriminates body vs
202+
* query routes); query methods need nothing. We deliberately keep the
203+
* declaration minimal — the route's own `description` + `mimeType`
204+
* already ride along in the catalog entry, and the HTTP method is
205+
* enriched at settlement time by `bazaarResourceServerExtension`, so
206+
* there's nothing to drift out of sync here.
207+
*
208+
* Pure + exported so the wiring is unit-testable without loading the
209+
* x402 payment stack.
210+
*/
211+
export function discoveryConfigForRouteKey(routeKey) {
212+
const method = String(routeKey).trim().split(/\s+/u, 1)[0].toUpperCase();
213+
const BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);
214+
return BODY_METHODS.has(method) ? { bodyType: 'json' } : {};
215+
}
216+
198217
function assertPrice(price, name) {
199218
// Accept either Money strings ("$0.05") or atomic-unit strings
200219
// ("50000" = 0.05 USDC at 6 decimals). The facilitator handles
@@ -226,13 +245,33 @@ export async function registerX402(app, x402Cfg) {
226245
// premium URL in their browser we'd rather they see the structured
227246
// 402 JSON than an opinionated wallet-connect dialog they can't
228247
// use anyway.
229-
const [{ paymentMiddlewareFromConfig }, { HTTPFacilitatorClient }, { ExactEvmScheme }] = await Promise.all([
248+
const [{ paymentMiddlewareFromConfig }, { HTTPFacilitatorClient }, { ExactEvmScheme }, { declareDiscoveryExtension }] = await Promise.all([
230249
import('@x402/fastify'),
231250
import('@x402/core/server'),
232-
import('@x402/evm/exact/server')
251+
import('@x402/evm/exact/server'),
252+
// Bazaar discovery: lets the facilitator we already use
253+
// (openx402) catalogue our paid routes in its public
254+
// `/discovery/resources` index once each route has settled at
255+
// least one payment. Loaded lazily with the rest of the x402
256+
// stack so the API still boots clean when the paywall is off.
257+
import('@x402/extensions/bazaar')
233258
]);
234259
const facilitatorClient = new HTTPFacilitatorClient({ url: x402Cfg.facilitatorUrl });
235260
const schemes = [{ network: x402Cfg.network, server: new ExactEvmScheme() }];
261+
// Decorate each route with a bazaar discovery extension. Additive
262+
// metadata only — @x402/fastify detects `extensions.bazaar`, enriches
263+
// the HTTP method at request time, and the facilitator soft-drops
264+
// anything malformed, so this can't break verification/settlement.
265+
const routesWithDiscovery = {};
266+
for (const [key, routeCfg] of Object.entries(x402Cfg.routes)) {
267+
routesWithDiscovery[key] = {
268+
...routeCfg,
269+
extensions: {
270+
...(routeCfg.extensions ?? {}),
271+
...declareDiscoveryExtension(discoveryConfigForRouteKey(key))
272+
}
273+
};
274+
}
236275
// `syncFacilitatorOnStart: true` is required — it fetches the
237276
// /supported manifest from the facilitator so the resource server
238277
// knows which (scheme, network) tuples it can issue
@@ -241,7 +280,7 @@ export async function registerX402(app, x402Cfg) {
241280
// exact on eip155:8453".
242281
paymentMiddlewareFromConfig(
243282
app,
244-
x402Cfg.routes,
283+
routesWithDiscovery,
245284
facilitatorClient,
246285
schemes,
247286
/* paywallConfig */ undefined,

test/rest-server.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,14 @@ describe('paywall surface (x402 enabled, in-process)', () => {
533533
expect(body.service.name).toMatch(/Seneschal/);
534534
expect(body.service.homepage).toBe('https://seneschal.space');
535535
expect(body.service.api_root).toBe('https://api.seneschal.space');
536+
// Discovery surface advertises the control panel + Telegram
537+
// contact (operator no longer monitors Discord) and lists the
538+
// flagship Private Watch product first.
539+
expect(body.service.control_panel).toBe('https://panel.seneschal.space');
540+
expect(body.service.contact).toBe('https://t.me/OrknetP');
541+
expect(Array.isArray(body.service.products)).toBe(true);
542+
expect(body.service.products[0].name).toBe('Private Watch');
543+
expect(body.service.products[0].start).toBe('POST /v1/private/watch');
536544
expect(body.routes.length).toBe(1);
537545
});
538546

test/x402.test.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
// REST integration tests.
55

66
import { describe, test, expect } from '@jest/globals';
7-
import { buildX402Config, describePaywall, PREMIUM_ROUTES } from '../src/x402.js';
7+
import { buildX402Config, describePaywall, discoveryConfigForRouteKey, PREMIUM_ROUTES } from '../src/x402.js';
8+
import { declareDiscoveryExtension, validateDiscoveryExtension } from '@x402/extensions/bazaar';
9+
import { checkIfBazaarNeeded } from '@x402/core/server';
810

911
const PAY_TO = '0x1234567890abcdef1234567890abcdef12345678';
1012

@@ -135,3 +137,47 @@ describe('describePaywall', () => {
135137
expect(typeof route.description).toBe('string');
136138
});
137139
});
140+
141+
describe('discoveryConfigForRouteKey (bazaar discovery wiring)', () => {
142+
test('query (GET/HEAD/DELETE) routes get an empty config', () => {
143+
expect(discoveryConfigForRouteKey('GET /v1/q/xmr/height')).toEqual({});
144+
expect(discoveryConfigForRouteKey('GET /v1/premium/opportunities')).toEqual({});
145+
});
146+
147+
test('body methods (POST/PUT/PATCH) declare bodyType json', () => {
148+
expect(discoveryConfigForRouteKey('POST /v1/private/watch')).toEqual({ bodyType: 'json' });
149+
expect(discoveryConfigForRouteKey('PUT /x')).toEqual({ bodyType: 'json' });
150+
expect(discoveryConfigForRouteKey('PATCH /x')).toEqual({ bodyType: 'json' });
151+
});
152+
153+
test('is tolerant of extra whitespace and case in the route key', () => {
154+
expect(discoveryConfigForRouteKey('post /v1/private/topup')).toEqual({ bodyType: 'json' });
155+
expect(discoveryConfigForRouteKey(' get /v1/q/zec/height ')).toEqual({});
156+
});
157+
158+
// The decoration logic mirrors registerX402: produce a bazaar
159+
// extension per route from the pure config. We assert the result is
160+
// shaped such that (a) the middleware will detect it and (b) it
161+
// validates once the HTTP method is enriched at settlement time —
162+
// exactly what bazaarResourceServerExtension does on the live path.
163+
test('every PREMIUM_ROUTES key yields a bazaar extension the middleware will catalogue', () => {
164+
const cfg = buildX402Config({ cfg: baseCfg(), env: {} });
165+
const routesWithDiscovery = {};
166+
for (const [key, routeCfg] of Object.entries(cfg.routes)) {
167+
routesWithDiscovery[key] = {
168+
...routeCfg,
169+
extensions: { ...declareDiscoveryExtension(discoveryConfigForRouteKey(key)) }
170+
};
171+
}
172+
expect(checkIfBazaarNeeded(routesWithDiscovery)).toBe(true);
173+
174+
for (const [key, routeCfg] of Object.entries(routesWithDiscovery)) {
175+
const bazaar = routeCfg.extensions.bazaar;
176+
expect(bazaar).toBeDefined();
177+
// Enrich the method the way the runtime server extension does,
178+
// then it must pass the facilitator's schema validation.
179+
bazaar.info.input.method = key.split(' ', 1)[0];
180+
expect(validateDiscoveryExtension(bazaar)).toEqual({ valid: true });
181+
}
182+
});
183+
});

0 commit comments

Comments
 (0)