Skip to content

Commit cdca219

Browse files
author
Rotwang9000
committed
feat(x402): pay-per-call premium tier on /v1/premium/* + MCP
Adds an honest revenue mechanism for the data API: per-request USDC payment via the x402 protocol on Base mainnet. The paywall is off by default (X402_RECIPIENT_ADDRESS unset → 503 paywall_not_configured) so the public service keeps shipping the free surface untouched. When the operator sets a recipient, unsigned requests get HTTP 402 + machine readable PaymentRequirements; the @x402/fastify middleware delegates verification + EIP-3009 settlement to the configured facilitator (default https://x402.org/facilitator). First paid endpoint: GET /v1/premium/opportunities. Returns the uncapped at-risk borrower catalogue joined with realised 7d market intel (top liquidators, win rate per (collateral,debt) pair, our own attempt outcomes per Morpho market) and ranked by expected value (debt × bonus × historical win-rate). Pure SQL over data we already collect; no live RPC. MCP surface gains two tools mirrored from the REST routes: - seneschal_paywall_info (free, exposes price/recipient/network) - seneschal_premium_opportunities (returns the feed; MCP transports don't yet negotiate payment, so this stays free until the spec lands a transport-level 402) Dashboard + landing page surface the premium tier when configured. .github/FUNDING.yml adds GitHub Sponsors visibility + a custom link to /v1/paywall. Tests: 108 passing (+33 new). Unit-tests cover config validation, describePaywall projection, market-intel aggregation, EV ranking, parameter validation, and REST 402/503 wire-up.
1 parent 6b36277 commit cdca219

17 files changed

Lines changed: 15720 additions & 3961 deletions

.github/FUNDING.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Configure the Sponsor button at the top of the GitHub repo. The
2+
# data-api lives on a single Helsinki box and is free to use; tips
3+
# keep it lit. Per-call agent payments via x402 are the preferred
4+
# revenue path — see /v1/paywall on the live service for the
5+
# machine-readable price + recipient.
6+
7+
github: [Rotwang9000]
8+
# Add the operator's ENS / 0x address(es) below once a dedicated
9+
# donation wallet is provisioned. We intentionally don't list the
10+
# bot's executor wallets here because those keys are hot.
11+
custom:
12+
- 'https://api.seneschal.space/v1/paywall'

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,50 @@ Shared design rule: both REST and MCP layers are *thin* wrappers around
150150
`queries.js`. Any new endpoint goes in `queries.js` first (with tests),
151151
then both wrappers in the same commit.
152152

153+
## Premium tier (x402 paywall)
154+
155+
`src/queries-premium.js` plus `src/x402.js` add per-call payment to a
156+
small family of `/v1/premium/*` endpoints (and `seneschal_premium_*`
157+
MCP tools). The paywall is off unless the operator sets
158+
`X402_RECIPIENT_ADDRESS`. Once set, unsigned requests get HTTP 402 with
159+
machine-readable payment requirements, and an
160+
[x402 facilitator](https://docs.x402.org) settles a signed
161+
EIP-3009 `transferWithAuthorization` for USDC on Base mainnet.
162+
163+
### Configure
164+
165+
| Env var | Default | Notes |
166+
| -------------------------- | -------------------------------- | ---------------------------------------------------------------------- |
167+
| `X402_RECIPIENT_ADDRESS` | (empty — paywall off) | Recipient wallet on the chosen network. 0x-prefixed 20-byte hex. |
168+
| `X402_NETWORK` | `eip155:8453` (Base mainnet) | Any CAIP-2 EVM network the facilitator supports. |
169+
| `X402_FACILITATOR_URL` | `https://x402.org/facilitator` | Use a production facilitator for mainnet (see x402.org/ecosystem). |
170+
| `X402_FEED_PRICE` | `$0.05` | Money-formatted (`$0.05`) or atomic units (`50000`). |
171+
| `X402_PAYWALL_DESCRIPTION` || Shown on `/`, `/v1/paywall`, and the stats dashboard. |
172+
| `X402_MAX_TIMEOUT_SECONDS` | `120` | Maximum settlement window per call. |
173+
174+
Free metadata endpoint (zero cost, no signature required):
175+
176+
```
177+
curl https://api.seneschal.space/v1/paywall
178+
```
179+
180+
returns the live network/recipient/price/route table so agents can
181+
budget a session before opening a paid request.
182+
183+
## Support
184+
185+
- **Per-call payments** (preferred for agents): pay $0.05 USDC on Base
186+
to call `GET /v1/premium/opportunities`. See `/v1/paywall` for the
187+
live recipient + rails.
188+
- **GitHub Sponsors**: the Sponsor button at the top of the repo
189+
(`.github/FUNDING.yml`).
190+
- **Direct tips**: ETH / BTC addresses are surfaced on
191+
[stats.seneschal.space](https://stats.seneschal.space) once the
192+
operator sets `SENESCHAL_DONATE_ETH` / `SENESCHAL_DONATE_BTC`.
193+
194+
Seneschal runs on a single Helsinki box; every cent helps keep it
195+
online.
196+
153197
## License
154198

155199
MIT — see [LICENSE](LICENSE).

docs/landing.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ <h2>For everyone else: free liquidation &amp; builder data</h2>
4747
<a href="https://mcp.seneschal.space/health">MCP <code>/health</code></a>
4848
</div>
4949

50+
<h2>For agents: premium feed via x402</h2>
51+
<p>
52+
A small per-call USDC fee on Base unlocks
53+
<code>/v1/premium/opportunities</code>: the uncapped at-risk catalogue
54+
joined with realised 7-day market intel (top liquidators, win rate,
55+
our own attempt outcomes per market), pre-ranked by expected value.
56+
Standard x402 paywall &mdash; signed
57+
<code>transferWithAuthorization</code>, settlement by an x402
58+
facilitator, no API keys.
59+
</p>
60+
<div class="row">
61+
<a href="https://api.seneschal.space/v1/paywall">Paywall metadata</a>
62+
<a href="https://docs.x402.org">x402 spec</a>
63+
<a href="https://api.seneschal.space/v1/premium/opportunities">Premium endpoint</a>
64+
</div>
65+
5066
<p class=muted>
5167
Contact: Rotwang9000 on Discord.
5268
</p>

docs/stats.html

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,13 @@ <h2>Recent on-chain liquidations (last 24 h)</h2>
261261
</div>
262262
</div>
263263

264+
<!-- Premium tier (x402 paywall) -->
265+
<div id="premium-panel" class="panel span-12" style="display:none">
266+
<h2>Premium tier — pay-per-call</h2>
267+
<p id="premium-blurb" style="color: var(--muted); margin-top: 0"></p>
268+
<div id="premium-details" style="display:flex; flex-wrap: wrap; gap: 1em; margin-top: 0.8em; font-size: 0.95em"></div>
269+
</div>
270+
264271
<!-- Support development -->
265272
<div id="support-panel" class="panel span-12" style="display:none">
266273
<h2>Support development</h2>
@@ -580,6 +587,41 @@ <h2>Support development</h2>
580587
`).join('');
581588
}
582589

590+
function renderPremium(d) {
591+
const panel = document.getElementById('premium-panel');
592+
const p = d.premium_tier;
593+
if (!p || !p.enabled) {
594+
panel.style.display = 'none';
595+
return;
596+
}
597+
panel.style.display = '';
598+
document.getElementById('premium-blurb').textContent = p.blurb
599+
|| 'Pay-per-call x402 micropayment unlocks the uncapped opportunity feed plus realised market intel.';
600+
const cells = [];
601+
cells.push(`
602+
<div class="tile" style="flex:1; min-width: 240px">
603+
<div class="kpi-label">Price per call</div>
604+
<div style="font-size: 1.4em; font-weight: 600">${escapeHtml(p.price_per_call || '$0.05')}</div>
605+
<div class="kpi-sub">USDC on ${escapeHtml((p.network || 'eip155:8453').replace('eip155:8453', 'Base mainnet'))}</div>
606+
</div>
607+
`);
608+
cells.push(`
609+
<div class="tile" style="flex:2; min-width: 320px">
610+
<div class="kpi-label">Endpoint</div>
611+
<div style="word-break: break-all; font-family: monospace; font-size: 0.9em">GET ${escapeHtml(p.endpoint || '')}</div>
612+
<div class="kpi-sub">x402 spec: <a href="${escapeHtml(p.docs || 'https://docs.x402.org')}" target="_blank" rel="noopener">docs.x402.org</a> · MCP tool: <code>${escapeHtml(p.mcp_tool || 'seneschal_premium_opportunities')}</code></div>
613+
</div>
614+
`);
615+
cells.push(`
616+
<div class="tile" style="flex:1; min-width: 240px">
617+
<div class="kpi-label">Recipient</div>
618+
<div style="word-break: break-all; font-family: monospace; font-size: 0.88em">${escapeHtml(p.payTo || '')}</div>
619+
<div class="kpi-sub">EIP-3009 transferWithAuthorization — gasless for the payer.</div>
620+
</div>
621+
`);
622+
document.getElementById('premium-details').innerHTML = cells.join('');
623+
}
624+
583625
function renderSupport(d) {
584626
const panel = document.getElementById('support-panel');
585627
const s = d.support;
@@ -640,6 +682,7 @@ <h2>Support development</h2>
640682
renderTopAtRisk(d);
641683
renderLiqDaily(d);
642684
renderRecentLiq(d);
685+
renderPremium(d);
643686
renderSupport(d);
644687
clearError();
645688
const ts = new Date(d.as_of_ms);

0 commit comments

Comments
 (0)