Skip to content

Commit 47dc75b

Browse files
author
S. K. Rotwang MMMMMMMMM
committed
private watch (Phase 2): view-key XMR/ZEC payment monitoring
POST /v1/private/watch ($0.10 via x402) creates a 7-day watch tied to a webhook URL. Server polls the local NFPT scanner (Monero LWS / Orchard UFVK) every 3 minutes; on balance change, POSTs an HMAC-SHA256 signed event to the receiver. View keys AES-256-GCM encrypted at rest under PRIVATE_WATCH_ENCRYPTION_KEY; webhook URLs SSRF-guarded. GET /v1/private/watch/:id status + DELETE cancel are free. MCP tools seneschal_private_watch_{create,info} mirror the REST surface. Adds private-watch-{crypto,store,nfpt,poller}.js + private-watch.js plus 5 test files. 354 tests passing.
1 parent c7620cd commit 47dc75b

20 files changed

Lines changed: 4529 additions & 32 deletions

docs/index.html

Lines changed: 204 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -250,34 +250,221 @@ <h2>MCP tool reference</h2>
250250
<tr><td><code>seneschal_premium_opportunities</code></td><td><strong>Premium.</strong> Uncapped at-risk catalogue ranked by expected value, with realised 7d market intel. Paid via x402 at the REST surface.</td></tr>
251251
</table>
252252

253-
<h2>Premium tier (x402)</h2>
253+
<h2 id="premium-tier-x402">Premium tier (x402)<strong>LIVE</strong></h2>
254254
<p>
255-
A small per-call USDC fee on Base mainnet unlocks
256-
<code>GET /v1/premium/opportunities</code>: the uncapped at-risk borrower
257-
catalogue plus realised 7-day market intel (top liquidators, win rate
258-
per market, our own attempt outcomes), ranked by expected liquidation
259-
value. Honest value-add over the free 500-row, snapshot-only
260-
<code>/v1/liquidations/atrisk</code>.
255+
Pay-per-call endpoints on Base mainnet, settled in USDC via the
256+
x402 protocol. Three price tiers — bulk feeds, builder analytics,
257+
and atomic single-fact queries:
258+
</p>
259+
<table>
260+
<thead>
261+
<tr><th>Endpoint</th><th>Price</th><th>What you get</th></tr>
262+
</thead>
263+
<tbody>
264+
<tr>
265+
<td><code>GET /v1/premium/opportunities</code></td>
266+
<td><strong>$0.05</strong></td>
267+
<td>Uncapped at-risk borrower catalogue plus realised 7-day market
268+
intel (top liquidators, per-market win rate, our own attempt outcomes),
269+
ranked by expected liquidation value. Honest value-add over the free
270+
500-row, snapshot-only <code>/v1/liquidations/atrisk</code>.</td>
271+
</tr>
272+
<tr>
273+
<td><code>GET /v1/premium/builder-stats</code></td>
274+
<td><strong>$0.10</strong></td>
275+
<td>Per-builder bid distribution (p25/median/p75/p90/p99/max ETH) plus
276+
a 24-element hourly slot histogram over a configurable window. Answers
277+
"what value do I need to land in builder X's bundle right now?" for
278+
searchers tuning bundle pricing. Sourced from our own shadow recorder
279+
so it covers every observed slot, not just landed blocks.</td>
280+
</tr>
281+
<tr>
282+
<td><code>GET /v1/q/*</code> (15 endpoints)</td>
283+
<td><strong>$0.001</strong></td>
284+
<td><strong>Penny Oracle.</strong> Atomic single-fact endpoints —
285+
each answers ONE yes/no or one number, returns in &lt;50 ms, and is
286+
designed for tight agent loops where bulk JSON is overkill. See the
287+
<a href="#penny-oracle">catalogue below</a>.</td>
288+
</tr>
289+
<tr>
290+
<td><code>POST /v1/private/watch</code></td>
291+
<td><strong>$0.10</strong></td>
292+
<td><strong>Private Watch.</strong> One paid call subscribes a Monero
293+
or Zcash address to view-key based payment monitoring; the receiver
294+
gets a HMAC-signed webhook on every balance change for 7 days (1&ndash;30
295+
configurable). View keys are AES-256-GCM encrypted at rest with an
296+
operator-supplied master key. See
297+
<a href="#private-watch">private watch below</a>.</td>
298+
</tr>
299+
</tbody>
300+
</table>
301+
302+
<h3 id="penny-oracle">Penny Oracle — /v1/q/* atomic-fact endpoints</h3>
303+
<p>
304+
Eight micro-priced endpoints, all <code>GET</code>, all <strong>$0.001</strong>
305+
per call (1/100th of a cent). Each returns a flat object so an agent
306+
can branch directly on the answer without nested-field gymnastics.
307+
The free catalogue at <code>GET /v1/q</code> enumerates every question
308+
and its input parameters.
309+
</p>
310+
<table>
311+
<thead>
312+
<tr><th>Endpoint</th><th>Inputs</th><th>Answers</th></tr>
313+
</thead>
314+
<tbody>
315+
<tr><td><code>/v1/q/liquidatable</code></td><td><code>addr</code>, <code>protocol?</code></td><td>Is borrower X liquidatable right now? Returns <code>{found, liquidatable, hf, debt_usd, last_seen_ms}</code> across Aave + Morpho.</td></tr>
316+
<tr><td><code>/v1/q/at-risk-count</code></td><td><code>max_hf?</code>, <code>min_debt_usd?</code>, <code>protocol?</code></td><td>Count + total debt of borrowers below the named HF and above the named debt floor.</td></tr>
317+
<tr><td><code>/v1/q/recent-liquidations</code></td><td><code>since_min?</code>, <code>protocol?</code></td><td>Number of liquidations observed in the last N minutes and their aggregate debt.</td></tr>
318+
<tr><td><code>/v1/q/top-builder</code></td><td><code>window?</code></td><td>Highest-share builder in the named window (<code>24h</code>/<code>7d</code>/<code>30d</code>).</td></tr>
319+
<tr><td><code>/v1/q/builder-share</code></td><td><code>builder</code>, <code>window?</code></td><td>Slot share of a specific builder (substring match) over the window.</td></tr>
320+
<tr><td><code>/v1/q/builder-bid</code></td><td><code>builder</code>, <code>pct?</code>, <code>window?</code></td><td>Percentile bid value (in ETH) for that builder over the window. Use <code>pct</code> 25/50/75/90/99.</td></tr>
321+
<tr><td><code>/v1/q/cheapest-flashloan</code></td><td><code>asset</code>, <code>chain?</code></td><td>Cheapest flash-loan provider for that asset on that chain. Defaults to Ethereum mainnet.</td></tr>
322+
<tr><td><code>/v1/q/data-freshness</code></td><td><code>source</code></td><td>Age in seconds of the freshest row in one of: <code>shadow_blocks</code>, <code>borrower_snapshot</code>, <code>morpho_borrower_snapshot</code>, <code>missed_liquidations</code>, <code>executions</code>.</td></tr>
323+
<tr><th colspan="3" style="background:#fff7e6;border-top:2px solid #b35900">Privacy-chain facts &mdash; sourced from Seneschal-operated full nodes</th></tr>
324+
<tr><td><code>/v1/q/xmr/height</code></td><td>&mdash;</td><td>Current Monero chain height + sync state. Sourced from a synced <code>monerod</code> v0.18 instance.</td></tr>
325+
<tr><td><code>/v1/q/xmr/mempool</code></td><td>&mdash;</td><td>Number of pending transactions in the Monero mempool right now.</td></tr>
326+
<tr><td><code>/v1/q/xmr/fee</code></td><td>&mdash;</td><td>Recommended per-byte fee in piconero (also per-kB) so wallets can budget the next-block tx.</td></tr>
327+
<tr><td><code>/v1/q/xmr/last-block</code></td><td>&mdash;</td><td>Timestamp + age of the most recent Monero block, plus hash, difficulty, size.</td></tr>
328+
<tr><td><code>/v1/q/zec/height</code></td><td>&mdash;</td><td>Current Zcash chain height + verification progress + best block hash. Sourced from a synced <code>zebra</code> node.</td></tr>
329+
<tr><td><code>/v1/q/zec/mempool</code></td><td>&mdash;</td><td>Zcash mempool count + bytes.</td></tr>
330+
<tr><td><code>/v1/q/zec/last-block</code></td><td>&mdash;</td><td>Timestamp + age of the most recent Zcash block, plus hash, difficulty, size.</td></tr>
331+
</tbody>
332+
</table>
333+
<p>
334+
The privacy-chain set is the "joining the dots" of this product line:
335+
Monero and Zcash are the two chains where running a node yourself is
336+
genuinely awkward (~108 GB and ~270 GB respectively, plus syncing
337+
hours), public RPC endpoints rate-limit aggressively, and there is no
338+
Etherscan-style hosted explorer to fall back on. We run synced nodes
339+
already; the x402 sub-cent paywall finally makes selling individual
340+
reads worth the trouble. All responses are cached server-side for
341+
10&thinsp;s so an agent in a tight loop costs the daemon nothing extra.
342+
</p>
343+
<p>
344+
Hand-rolled flow:
345+
</p>
346+
<pre><code>curl -s 'https://api.seneschal.space/v1/q/liquidatable?addr=0x...&amp;protocol=aave'
347+
# Pays $0.001 USDC on Base via x402-fetch and returns
348+
# {"found":true,"protocol":"aave","liquidatable":false,"hf":1.03,"debt_usd":24500,"last_seen_ms":1716314400000}</code></pre>
349+
<p>
350+
MCP agents can use the single dispatcher tool <code>seneschal_q</code>
351+
with <code>{question, params}</code> rather than wiring up eight
352+
separate tool calls. The MCP path is currently free; the HTTP path
353+
is paywalled.
261354
</p>
262355
<p>
263356
Payment uses the <a href="https://docs.x402.org">x402 protocol</a>
264-
(spec v2). Unsigned requests get HTTP 402 with machine-readable
357+
(spec v2). Unsigned requests get HTTP 402 with a base64-encoded
358+
<code>Payment-Required</code> header carrying machine-readable
265359
<code>PaymentRequirements</code>; the client signs an EIP-3009
266-
<code>transferWithAuthorization</code> and retries with a
267-
<code>PAYMENT-SIGNATURE</code> header. The <a href="https://x402.org/facilitator">facilitator</a>
268-
verifies + settles on Base. No API keys, no subscriptions, gasless
269-
for the payer.
360+
<code>transferWithAuthorization</code> off-chain and retries with a
361+
<code>X-PAYMENT</code> header. We delegate verify + settle to the
362+
<a href="https://facilitator.openx402.ai">openx402.ai</a> facilitator,
363+
which broadcasts the EIP-3009 transfer on Base. No API keys, no
364+
subscriptions, no gas for the payer — the facilitator pays the gas
365+
and stays solvent from a small spread on the underlying USDC.
270366
</p>
271367
<p>
272368
Free metadata endpoint for budgeting before any paid call:
273369
</p>
274-
<pre><code>curl https://api.seneschal.space/v1/paywall</code></pre>
370+
<pre><code>curl https://api.seneschal.space/v1/paywall
371+
# → {"protocol":"x402","network":"eip155:8453",
372+
# "facilitator":"https://facilitator.openx402.ai",
373+
# "payTo":"0x46Ba634261566CF242c853d1f49511f9268ba674",
374+
# "scheme":"exact (EIP-3009 transferWithAuthorization)",
375+
# "routes":[{"endpoint":"GET /v1/premium/opportunities","price":"$0.05",...},
376+
# {"endpoint":"GET /v1/premium/builder-stats","price":"$0.10",...},
377+
# {"endpoint":"GET /v1/q/liquidatable","price":"$0.001",...}, … ]}</code></pre>
275378
<p>
276-
Returns <code>enabled: false</code> when the operator hasn't
277-
configured a recipient yet (current state) or the full network /
278-
recipient / route / price table once configured. Agents using
379+
Agents using <code>x402-axios</code>, <code>x402-fetch</code>,
279380
<code>@x402/express</code>, <code>@x402/fastify</code> or the
280-
official client libraries pay automatically on 402.
381+
official client libraries pay automatically on 402. Hand-rolled flow,
382+
in three lines of TypeScript:
383+
</p>
384+
<pre><code>import { wrapFetchWithPayment } from 'x402-fetch';
385+
import { createWalletClient, http } from 'viem';
386+
import { base } from 'viem/chains';
387+
388+
const wallet = createWalletClient({ account, transport: http(), chain: base });
389+
const fetchPaid = wrapFetchWithPayment(fetch, wallet);
390+
const r = await fetchPaid('https://api.seneschal.space/v1/premium/opportunities');
391+
console.log(await r.json());</code></pre>
392+
<p>
393+
The <code>account</code> is any viem-compatible signer holding a small
394+
USDC balance on Base — a fresh EOA + 1 USDC will fund ~20 paid calls.
395+
</p>
396+
397+
<h3 id="private-watch">Private Watch &mdash; /v1/private/watch (XMR/ZEC payment monitoring)</h3>
398+
<p>
399+
This is Phase&nbsp;2 of the privacy-chain product line. The Penny Oracle answers
400+
chain-tip questions; the Private Watch answers <em>"did <strong>I</strong> just get
401+
paid?"</em>. The agent hands us a public address, a view key, and a webhook URL.
402+
We do the heavy lifting (scanning every new block against the view key on a
403+
local <code>monero-lws</code> or Zcash UFVK scanner) and POST a signed event to the
404+
webhook the moment the balance changes.
405+
</p>
406+
<table>
407+
<thead>
408+
<tr><th>Endpoint</th><th>Method</th><th>Auth</th><th>Description</th></tr>
409+
</thead>
410+
<tbody>
411+
<tr><td><code>/v1/private/watch</code></td><td><code>POST</code></td><td>x402 <strong>$0.10</strong></td><td>Create a watch. Body: <code>{chain, address, viewKey, webhookUrl, durationDays?, birthdayHeight?}</code>. Returns <code>{watchId, watchToken, webhookSecret, expiresAt}</code>.</td></tr>
412+
<tr><td><code>/v1/private/watch/:id</code></td><td><code>GET</code></td><td>header <code>x-watch-token</code></td><td>Read current status: last polled time, delivery count, last known balance snapshot. <strong>Free</strong>.</td></tr>
413+
<tr><td><code>/v1/private/watch/:id</code></td><td><code>DELETE</code></td><td>header <code>x-watch-token</code></td><td>Cancel a watch. <strong>Free</strong>.</td></tr>
414+
<tr><td><code>/v1/private/info</code></td><td><code>GET</code></td><td>&mdash;</td><td>Free metadata: current price, supported chains, upstream NFPT health, security notes.</td></tr>
415+
<tr><td><code>/v1/private/health</code></td><td><code>GET</code></td><td>&mdash;</td><td>Counters only (no PII) for ops monitoring.</td></tr>
416+
</tbody>
417+
</table>
418+
<p>
419+
<strong>Webhook contract.</strong> Every delivery is a POST with
420+
<code>content-type: application/json</code> and three Seneschal headers:
421+
</p>
422+
<pre><code>X-Seneschal-Watch-Id: &lt;uuid&gt;
423+
X-Seneschal-Event: balance_change | scan_complete | status_change
424+
X-Seneschal-Signature: sha256=&lt;HMAC-SHA256(webhookSecret, exact request body)&gt;</code></pre>
425+
<p>
426+
The body is plain JSON:
427+
</p>
428+
<pre><code>{
429+
"watchId": "fb1a29bd-…",
430+
"chain": "monero",
431+
"address": "46FzbF…",
432+
"event": "balance_change",
433+
"timestamp": "2026-05-21T08:06:54.626Z",
434+
"nonce": "mpf7j7he-30s7yx",
435+
"previous": { "balanceAtomic": "0", "scannedHeight": 3349800, … },
436+
"current": { "balanceAtomic": "1234567890", "scannedHeight": 3349881, … },
437+
"delta": {
438+
"balance_atomic": "1234567890",
439+
"before_atomic": "0",
440+
"after_atomic": "1234567890"
441+
}
442+
}</code></pre>
443+
<p>
444+
Receivers verify the signature with the per-watch
445+
<code>webhookSecret</code> returned at creation. Constant-time HMAC comparison
446+
defeats forgery; the secret is generated server-side and never reused across
447+
watches. Bodies are capped at 64&nbsp;KB.
448+
</p>
449+
<p>
450+
<strong>Security model.</strong> View keys decrypt incoming transactions but
451+
<em>cannot spend</em> &mdash; you can hand us one without giving up custody. We
452+
nonetheless encrypt the key at rest with AES-256-GCM under an operator-held
453+
master key (<code>PRIVATE_WATCH_ENCRYPTION_KEY</code>); plaintext only exists in
454+
memory while the poller is decrypting it for the next scan call. Webhook URLs
455+
pointing at private IPs (RFC1918, loopback, link-local, cloud metadata) are
456+
rejected at creation time to defeat SSRF.
457+
</p>
458+
<p>
459+
<strong>Tunables.</strong> Polling cadence is 3&thinsp;min by default (stays inside
460+
NFPT's 5&thinsp;min scanner idle window). On webhook failure we retry on the next
461+
tick; 50 consecutive failures retire the watch with a <code>dead</code> flag.
462+
After expiry the row is hard-deleted on the next tick.
463+
</p>
464+
<p>
465+
<strong>MCP.</strong> Agents already inside a paid MCP session can use
466+
<code>seneschal_private_watch_create</code> (same input shape) and
467+
<code>seneschal_private_watch_info</code> for budgeting.
281468
</p>
282469

283470
<h2>Data freshness &amp; coverage</h2>

docs/landing.html

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,55 @@ <h2>For smart accounts: pay gas in stablecoins on Base</h2>
6161
<a href="https://github.com/Rotwang9000/seneschal-paymaster">GitHub</a>
6262
</div>
6363

64-
<h2>For agents: premium feed via x402</h2>
64+
<h2>For agents: premium feed via x402 &mdash; <strong>LIVE</strong></h2>
6565
<p>
66-
A small per-call USDC fee on Base unlocks
67-
<code>/v1/premium/opportunities</code>: the uncapped at-risk catalogue
68-
joined with realised 7-day market intel (top liquidators, win rate,
69-
our own attempt outcomes per market), pre-ranked by expected value.
70-
Standard x402 paywall &mdash; signed
71-
<code>transferWithAuthorization</code>, settlement by an x402
72-
facilitator, no API keys.
66+
Pay-per-call endpoints on Base mainnet, settled in USDC via the x402
67+
protocol &mdash; no API keys, no subscriptions, no gas for the payer
68+
(the <a href="https://facilitator.openx402.ai">openx402.ai</a>
69+
facilitator pays the EIP-3009 transfer gas). Three price tiers:
7370
</p>
71+
<ul>
72+
<li><code>GET /v1/premium/opportunities</code> &mdash; <strong>$0.05</strong>
73+
&mdash; uncapped at-risk catalogue joined with realised 7-day market
74+
intel (top liquidators, win rate, our own attempt outcomes per
75+
market), pre-ranked by expected value.</li>
76+
<li><code>GET /v1/premium/builder-stats</code> &mdash; <strong>$0.10</strong>
77+
&mdash; per-builder bid distribution (p25/median/p90/p99/max ETH) and
78+
hourly slot histogram from our shadow recorder. For searchers tuning
79+
bundle pricing.</li>
80+
<li><code>GET /v1/q/*</code> &mdash; <strong>$0.001</strong>
81+
&mdash; <strong>Penny Oracle.</strong> Fifteen atomic single-fact
82+
endpoints across two families:
83+
<ul>
84+
<li><strong>DeFi facts</strong> (eight) &mdash; liquidatable? at-risk
85+
count? top builder? builder p90 bid? cheapest flash-loan provider?
86+
data freshness?</li>
87+
<li><strong>Privacy-chain facts</strong> (seven) &mdash; Monero and
88+
Zcash chain-tip atomic facts sourced from Seneschal-operated full
89+
nodes. The chains where running your own node is annoying and
90+
where free RPC endpoints rate-limit. <code>/v1/q/xmr/*</code> and
91+
<code>/v1/q/zec/*</code>.</li>
92+
</ul>
93+
Free catalogue with live chain availability at <code>/v1/q</code>.</li>
94+
<li><code>POST /v1/private/watch</code> &mdash; <strong>$0.10</strong>
95+
&mdash; <strong>Private Watch.</strong> One paid call subscribes a
96+
Monero or Zcash address to view-key-based payment monitoring. We
97+
poll our local <code>monero-lws</code> / Orchard scanner against the
98+
view key every 3 minutes and POST an HMAC-signed webhook on every
99+
balance change for 7 days (1&ndash;30 configurable). View keys are
100+
AES-256-GCM encrypted at rest. Status + cancel are free; webhook
101+
verification uses HMAC-SHA256 with a per-watch secret. This is the
102+
infrastructure for accepting XMR/ZEC payments without exposing a
103+
spend key.</li>
104+
</ul>
74105
<div class="row">
75-
<a href="https://api.seneschal.space/v1/paywall">Paywall metadata</a>
106+
<a href="https://api.seneschal.space/v1/paywall" style="background:#e7f1ff;border-color:#1462b3;color:#1462b3"><strong>Paywall metadata &raquo;</strong></a>
107+
<a href="https://docs.seneschal.space/#premium-tier-x402">How to pay (TS / curl)</a>
76108
<a href="https://docs.x402.org">x402 spec</a>
77-
<a href="https://api.seneschal.space/v1/premium/opportunities">Premium endpoint</a>
109+
<a href="https://api.seneschal.space/v1/premium/opportunities">/v1/premium/opportunities</a>
110+
<a href="https://api.seneschal.space/v1/premium/builder-stats">/v1/premium/builder-stats</a>
111+
<a href="https://api.seneschal.space/v1/q">/v1/q (catalogue)</a>
112+
<a href="https://api.seneschal.space/v1/private/info">/v1/private/info</a>
78113
</div>
79114

80115
<p class=muted>

src/config.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,45 @@ export const config = Object.freeze({
131131
// daily snapshots to. The REST server reads this read-only to
132132
// power historical charts. Empty path = "no history yet, frontend
133133
// hides the chart".
134-
incomeSnapshotsPath: asString('SENESCHAL_INCOME_SNAPSHOTS', '/var/lib/seneschal-income/snapshots.jsonl')
134+
incomeSnapshotsPath: asString('SENESCHAL_INCOME_SNAPSHOTS', '/var/lib/seneschal-income/snapshots.jsonl'),
135+
136+
// ── Private watch (Phase 2 — view-key payment monitoring) ─────────
137+
// The data-api wraps the local NFPT wallet-scanner so agents can
138+
// pay one x402 fee for a 7-day "ping me when XMR/ZEC lands at this
139+
// address" subscription. View keys are encrypted at rest with a
140+
// 32-byte master key (PRIVATE_WATCH_ENCRYPTION_KEY); webhooks are
141+
// HMAC-SHA256 signed with a per-watch secret.
142+
//
143+
// NFPT runs on the same host as monerod/zebra (local box, port
144+
// 3555) and is reverse-tunnelled onto fin4's loopback by the same
145+
// systemd unit that exposes 18081 + 8232.
146+
nfptBaseUrl: asString('NFPT_BASE_URL', 'http://127.0.0.1:3555'),
147+
nfptApiKey: asString('NFPT_API_KEY', 'development-key-for-testing'),
148+
nfptTimeoutMs: asInt('NFPT_TIMEOUT_MS', 30_000),
149+
// Path to the watch SQLite DB. Writers: rest-server (new watch) +
150+
// poller (state updates). Reader: rest-server (status reads).
151+
// Defaults to the same /var/lib base as income-history so the
152+
// systemd unit can grant a single writable directory.
153+
privateWatchDbPath: asString('PRIVATE_WATCH_DB', '/var/lib/seneschal-data-api/private-watches.db'),
154+
// 64 hex chars = 32 bytes. The watch creation endpoint refuses to
155+
// accept requests until this is set, so a misconfigured deploy
156+
// can't silently store view keys in cleartext.
157+
privateWatchEncryptionKey: asString('PRIVATE_WATCH_ENCRYPTION_KEY', ''),
158+
// Allow http://127.0.0.1 / private RFC1918 webhook URLs — strictly
159+
// for local development. Production deployments leave this off so
160+
// SSRF protection is in force.
161+
privateWatchAllowPrivateWebhooks: asString('PRIVATE_WATCH_ALLOW_PRIVATE_WEBHOOKS', '') === '1',
162+
// How often the poller drives a tick. Each tick polls every active
163+
// watch; NFPT detaches scanners after 5 min idle, so we stay
164+
// comfortably below.
165+
privateWatchPollIntervalSec: asInt('PRIVATE_WATCH_POLL_INTERVAL_SEC', 180),
166+
// HTTP timeout for outbound webhook POSTs. Set short — receivers
167+
// should accept fast and process async.
168+
privateWatchWebhookTimeoutMs: asInt('PRIVATE_WATCH_WEBHOOK_TIMEOUT_MS', 8_000),
169+
// x402 price for POST /v1/private/watch. One tier, $0.10 = 7-day
170+
// monitor for one (chain, address, viewKey, webhookUrl). Override
171+
// via env without touching code if we want a promo price.
172+
x402PrivateWatchPrice: asString('X402_PRIVATE_WATCH_PRICE', '$0.10')
135173
});
136174

137175
export default config;

0 commit comments

Comments
 (0)