@blockrun/franklin 3.15.13 → 3.15.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/context.js +8 -0
- package/dist/agent/loop.js +2 -0
- package/dist/session/storage.js +36 -0
- package/dist/storage/hygiene.d.ts +28 -0
- package/dist/storage/hygiene.js +134 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/prediction.d.ts +24 -0
- package/dist/tools/prediction.js +340 -0
- package/dist/tools/tool-categories.js +5 -0
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -315,6 +315,7 @@ function getToolPatternsSection() {
|
|
|
315
315
|
Your training data is frozen in the past. Live-world questions MUST be answered from tool results, not memory.
|
|
316
316
|
- Any question about a current price, quote, market state, or "should I buy/sell/hold X" → use **TradingMarket** (crypto/FX/commodity are free; stocks cost \$0.001 via the wallet).
|
|
317
317
|
- Any "what happened / why did it change / latest news on X" → use **ExaAnswer** for a cited synthesized answer, or **ExaSearch** + **ExaReadUrls** when you need more depth.
|
|
318
|
+
- Any "what are the odds of X / will Y happen / Polymarket on Z / Kalshi market for W" → use **PredictionMarket** (\$0.001 search; \$0.005 cross-platform / smart money).
|
|
318
319
|
- If the user names a thing you don't recognize (a company, ticker, project), don't demand clarification — call the research tools and figure it out. You have a wallet to spend on exactly this.
|
|
319
320
|
- If a tool returns an error (rate-limit, 404, insufficient funds), say so plainly and suggest the next action. Don't silently fall back to memory.
|
|
320
321
|
|
|
@@ -326,6 +327,13 @@ Your training data is frozen in the past. Live-world questions MUST be answered
|
|
|
326
327
|
|
|
327
328
|
If you find yourself about to emit one of these, stop and call the tool instead. If you don't know which ticker the user means, call ExaSearch or AskUser — never deflect.
|
|
328
329
|
|
|
330
|
+
**Prediction markets (PredictionMarket).** When the user asks about real-world odds — elections, "will X happen by year-end", "Polymarket on Y", "Kalshi market for Z", "what are the odds of recession" — use **PredictionMarket** instead of guessing. Four actions:
|
|
331
|
+
- \`searchPolymarket\` (\$0.001) and \`searchKalshi\` (\$0.001) — search markets by keyword. Run them **in parallel** when the user wants the current odds; comparing implied probability across two venues is the high-value answer.
|
|
332
|
+
- \`crossPlatform\` (\$0.005) — pre-matched pairs of equivalent markets across Polymarket and Kalshi. Use when the user wants arbitrage candidates or wants to know "where does the consensus disagree".
|
|
333
|
+
- \`smartMoney\` (\$0.005) — top-wallet flow on a specific Polymarket \`condition_id\`. Get the \`condition_id\` from a prior \`searchPolymarket\` call.
|
|
334
|
+
|
|
335
|
+
NEVER answer "what are the odds of X" from training-data memory — these are live markets that move every minute. NEVER claim "Polymarket doesn't have a market on this" without running \`searchPolymarket\` first. If both Polymarket and Kalshi return zero markets, say so explicitly with the searches you tried, then offer to broaden the query.
|
|
336
|
+
|
|
329
337
|
**Trading verdicts (TradingSignal).** When the user asks "how does $TICKER look" / "should I buy X" / "is BTC overbought":
|
|
330
338
|
- Run **TradingSignal** with default lookback (90d). Lower values leave MACD undefined.
|
|
331
339
|
- The tool returns a **Verdict** section with \`Direction\`, \`Bull signals\`, \`Bear signals\`. Echo it directly. Do not soften "bullish" to "leaning slightly positive" — say what the data says.
|
package/dist/agent/loop.js
CHANGED
|
@@ -21,6 +21,7 @@ import { recordUsage } from '../stats/tracker.js';
|
|
|
21
21
|
import { recordSessionUsage } from '../stats/session-tracker.js';
|
|
22
22
|
import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
|
|
23
23
|
import { logger, setDebugMode } from '../logger.js';
|
|
24
|
+
import { runDataHygiene } from '../storage/hygiene.js';
|
|
24
25
|
import { estimateCost, OPUS_PRICING } from '../pricing.js';
|
|
25
26
|
import { maybeMidSessionExtract } from '../learnings/extractor.js';
|
|
26
27
|
import { extractMentions, buildEntityContext, loadEntities } from '../brain/store.js';
|
|
@@ -437,6 +438,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
437
438
|
persistSessionMeta();
|
|
438
439
|
};
|
|
439
440
|
pruneOldSessions(sessionId); // Cleanup old sessions on start, protect current
|
|
441
|
+
runDataHygiene(); // Trim ~/.blockrun/data + cost_log + remove legacy files
|
|
440
442
|
persistSessionMeta();
|
|
441
443
|
// Flush session meta on SIGINT/SIGTERM so mid-stream Ctrl+C doesn't
|
|
442
444
|
// leave a stale .meta.json (wrong turnCount/messageCount/cost).
|
package/dist/session/storage.js
CHANGED
|
@@ -233,4 +233,40 @@ export function pruneOldSessions(activeSessionId) {
|
|
|
233
233
|
catch { /* ok */ }
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
// Sweep orphan jsonl files (left over from a session-id format change in
|
|
237
|
+
// earlier releases — meta deleted, jsonl stranded). The pre-3.x naming
|
|
238
|
+
// didn't include the random suffix, so the meta-driven prune above has
|
|
239
|
+
// no record of them and they accumulate forever. Verified on a real
|
|
240
|
+
// user machine: 21 metas, 121 jsonl, 100 orphans = ~1 MB stranded.
|
|
241
|
+
pruneOrphanJsonlFiles(activeSessionId);
|
|
242
|
+
}
|
|
243
|
+
function pruneOrphanJsonlFiles(activeSessionId) {
|
|
244
|
+
const dir = getSessionsDir();
|
|
245
|
+
let entries;
|
|
246
|
+
try {
|
|
247
|
+
entries = fs.readdirSync(dir);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return; // Sessions dir doesn't exist yet — nothing to prune.
|
|
251
|
+
}
|
|
252
|
+
const knownIds = new Set();
|
|
253
|
+
for (const f of entries) {
|
|
254
|
+
if (f.endsWith('.meta.json')) {
|
|
255
|
+
knownIds.add(f.slice(0, -'.meta.json'.length));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const f of entries) {
|
|
259
|
+
if (!f.endsWith('.jsonl'))
|
|
260
|
+
continue;
|
|
261
|
+
const id = f.slice(0, -'.jsonl'.length);
|
|
262
|
+
if (id === activeSessionId)
|
|
263
|
+
continue;
|
|
264
|
+
if (knownIds.has(id))
|
|
265
|
+
continue;
|
|
266
|
+
// No meta partner — orphan. Delete the jsonl.
|
|
267
|
+
try {
|
|
268
|
+
fs.unlinkSync(path.join(dir, f));
|
|
269
|
+
}
|
|
270
|
+
catch { /* ok */ }
|
|
271
|
+
}
|
|
236
272
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data hygiene for ~/.blockrun/.
|
|
3
|
+
*
|
|
4
|
+
* Several files in this directory are written by the @blockrun/llm SDK or
|
|
5
|
+
* by older Franklin versions that didn't ship retention. Without periodic
|
|
6
|
+
* trimming they grow unbounded:
|
|
7
|
+
*
|
|
8
|
+
* - ~/.blockrun/data/ — every paid API call gets a JSON blob
|
|
9
|
+
* dropped here for forensic replay. SDK
|
|
10
|
+
* has no rotation; verified 5.7 MB across
|
|
11
|
+
* ~2 months of light use, will be 30 MB
|
|
12
|
+
* by year-end and slow `franklin insights`.
|
|
13
|
+
* - ~/.blockrun/cost_log.jsonl — append-only ledger of every paid call's
|
|
14
|
+
* cost. Same SDK; no rotation.
|
|
15
|
+
* - brcc-debug.log / brcc-stats.json / 0xcode-stats.json
|
|
16
|
+
* — legacy stats / log files from earlier
|
|
17
|
+
* product names. Not written by any
|
|
18
|
+
* current code path.
|
|
19
|
+
*
|
|
20
|
+
* Hygiene runs once per session start (cheap — just stat() + filter +
|
|
21
|
+
* unlinkSync). Best-effort: every operation is wrapped so a single failure
|
|
22
|
+
* never breaks agent boot.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Top-level entry. Call once at agent session start. Catches its own
|
|
26
|
+
* errors so a bad disk never blocks startup.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runDataHygiene(): void;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data hygiene for ~/.blockrun/.
|
|
3
|
+
*
|
|
4
|
+
* Several files in this directory are written by the @blockrun/llm SDK or
|
|
5
|
+
* by older Franklin versions that didn't ship retention. Without periodic
|
|
6
|
+
* trimming they grow unbounded:
|
|
7
|
+
*
|
|
8
|
+
* - ~/.blockrun/data/ — every paid API call gets a JSON blob
|
|
9
|
+
* dropped here for forensic replay. SDK
|
|
10
|
+
* has no rotation; verified 5.7 MB across
|
|
11
|
+
* ~2 months of light use, will be 30 MB
|
|
12
|
+
* by year-end and slow `franklin insights`.
|
|
13
|
+
* - ~/.blockrun/cost_log.jsonl — append-only ledger of every paid call's
|
|
14
|
+
* cost. Same SDK; no rotation.
|
|
15
|
+
* - brcc-debug.log / brcc-stats.json / 0xcode-stats.json
|
|
16
|
+
* — legacy stats / log files from earlier
|
|
17
|
+
* product names. Not written by any
|
|
18
|
+
* current code path.
|
|
19
|
+
*
|
|
20
|
+
* Hygiene runs once per session start (cheap — just stat() + filter +
|
|
21
|
+
* unlinkSync). Best-effort: every operation is wrapped so a single failure
|
|
22
|
+
* never breaks agent boot.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
27
|
+
// Retention knobs. Tuned conservatively — a power user with 50+ calls/day
|
|
28
|
+
// for 30 days still fits in DATA_DIR_MAX_FILES, and 5000 cost-log entries
|
|
29
|
+
// covers months of normal use without truncating the running totals.
|
|
30
|
+
const DATA_DIR_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
31
|
+
const DATA_DIR_MAX_FILES = 2000;
|
|
32
|
+
const COST_LOG_MAX_ENTRIES = 5000;
|
|
33
|
+
// Cost log entries are tiny (~60 bytes — ts, endpoint, cost only). 40 bytes
|
|
34
|
+
// per entry keeps the probe under the real average so a slightly-overlong
|
|
35
|
+
// file always triggers the rescan rather than silently growing past cap.
|
|
36
|
+
const COST_LOG_PROBE_BYTES = COST_LOG_MAX_ENTRIES * 40;
|
|
37
|
+
// Legacy file names from earlier product iterations. All live directly in
|
|
38
|
+
// BLOCKRUN_DIR (only Franklin writes here, so these are safe to remove).
|
|
39
|
+
// `runcode-debug.log` is also handled by logs.ts's migration path; we
|
|
40
|
+
// delete the residual after migration in case it lingered.
|
|
41
|
+
const LEGACY_FILENAMES = [
|
|
42
|
+
'brcc-debug.log',
|
|
43
|
+
'brcc-stats.json',
|
|
44
|
+
'0xcode-stats.json',
|
|
45
|
+
'runcode-debug.log',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Top-level entry. Call once at agent session start. Catches its own
|
|
49
|
+
* errors so a bad disk never blocks startup.
|
|
50
|
+
*/
|
|
51
|
+
export function runDataHygiene() {
|
|
52
|
+
try {
|
|
53
|
+
trimDataDir();
|
|
54
|
+
}
|
|
55
|
+
catch { /* best effort */ }
|
|
56
|
+
try {
|
|
57
|
+
trimCostLog();
|
|
58
|
+
}
|
|
59
|
+
catch { /* best effort */ }
|
|
60
|
+
try {
|
|
61
|
+
removeLegacyFiles();
|
|
62
|
+
}
|
|
63
|
+
catch { /* best effort */ }
|
|
64
|
+
}
|
|
65
|
+
function trimDataDir() {
|
|
66
|
+
const dir = path.join(BLOCKRUN_DIR, 'data');
|
|
67
|
+
if (!fs.existsSync(dir))
|
|
68
|
+
return;
|
|
69
|
+
const entries = fs.readdirSync(dir);
|
|
70
|
+
if (entries.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
const cutoff = Date.now() - DATA_DIR_MAX_AGE_MS;
|
|
73
|
+
const stats = [];
|
|
74
|
+
for (const name of entries) {
|
|
75
|
+
try {
|
|
76
|
+
const st = fs.statSync(path.join(dir, name));
|
|
77
|
+
if (!st.isFile())
|
|
78
|
+
continue;
|
|
79
|
+
stats.push({ name, mtime: st.mtimeMs });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Best effort — skip unreadable entries.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Pass 1: age-based delete.
|
|
86
|
+
for (const e of stats) {
|
|
87
|
+
if (e.mtime < cutoff) {
|
|
88
|
+
try {
|
|
89
|
+
fs.unlinkSync(path.join(dir, e.name));
|
|
90
|
+
}
|
|
91
|
+
catch { /* ok */ }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Pass 2: file-count cap. After age trim, if we still have too many,
|
|
95
|
+
// drop the oldest until we're under the cap. Power users can hit this
|
|
96
|
+
// when running multiple paid tools in tight loops.
|
|
97
|
+
const survivors = stats
|
|
98
|
+
.filter(e => e.mtime >= cutoff)
|
|
99
|
+
.sort((a, b) => a.mtime - b.mtime); // oldest first
|
|
100
|
+
const excess = survivors.length - DATA_DIR_MAX_FILES;
|
|
101
|
+
if (excess > 0) {
|
|
102
|
+
for (let i = 0; i < excess; i++) {
|
|
103
|
+
try {
|
|
104
|
+
fs.unlinkSync(path.join(dir, survivors[i].name));
|
|
105
|
+
}
|
|
106
|
+
catch { /* ok */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function trimCostLog() {
|
|
111
|
+
const file = path.join(BLOCKRUN_DIR, 'cost_log.jsonl');
|
|
112
|
+
if (!fs.existsSync(file))
|
|
113
|
+
return;
|
|
114
|
+
// Cheap probe — skip the full read+rewrite when the file is small.
|
|
115
|
+
const stat = fs.statSync(file);
|
|
116
|
+
if (stat.size < COST_LOG_PROBE_BYTES)
|
|
117
|
+
return;
|
|
118
|
+
const lines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean);
|
|
119
|
+
if (lines.length <= COST_LOG_MAX_ENTRIES)
|
|
120
|
+
return;
|
|
121
|
+
const kept = lines.slice(lines.length - COST_LOG_MAX_ENTRIES);
|
|
122
|
+
fs.writeFileSync(file, kept.join('\n') + '\n');
|
|
123
|
+
}
|
|
124
|
+
function removeLegacyFiles() {
|
|
125
|
+
for (const name of LEGACY_FILENAMES) {
|
|
126
|
+
const p = path.join(BLOCKRUN_DIR, name);
|
|
127
|
+
if (!fs.existsSync(p))
|
|
128
|
+
continue;
|
|
129
|
+
try {
|
|
130
|
+
fs.unlinkSync(p);
|
|
131
|
+
}
|
|
132
|
+
catch { /* ok */ }
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { jupiterQuoteCapability, jupiterSwapCapability } from './jupiter.js';
|
|
|
29
29
|
import { base0xQuoteCapability, base0xSwapCapability } from './zerox-base.js';
|
|
30
30
|
import { base0xGaslessSwapCapability } from './zerox-gasless.js';
|
|
31
31
|
import { defiLlamaProtocolsCapability, defiLlamaProtocolCapability, defiLlamaChainsCapability, defiLlamaYieldsCapability, defiLlamaPriceCapability, } from './defillama.js';
|
|
32
|
+
import { predictionMarketCapability } from './prediction.js';
|
|
32
33
|
import { modalCapabilities } from './modal.js';
|
|
33
34
|
import { createTradingCapabilities } from './trading-execute.js';
|
|
34
35
|
import { Portfolio } from '../trading/portfolio.js';
|
|
@@ -159,6 +160,7 @@ export const allCapabilities = [
|
|
|
159
160
|
defiLlamaChainsCapability,
|
|
160
161
|
defiLlamaYieldsCapability,
|
|
161
162
|
defiLlamaPriceCapability,
|
|
163
|
+
predictionMarketCapability, // Polymarket / Kalshi / matching / smart money via Predexon
|
|
162
164
|
// Modal GPU sandbox tools — registered but hidden by default (not in
|
|
163
165
|
// CORE_TOOL_NAMES). Agent must `ActivateTool({names:["ModalCreate",...]})`
|
|
164
166
|
// before they appear in its tool inventory. High-cost ($0.40/H100 create)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PredictionMarket — unified access to Polymarket / Kalshi / cross-platform
|
|
3
|
+
* matching / smart-money endpoints via the BlockRun gateway. Each call
|
|
4
|
+
* settles via x402 against the user's USDC wallet.
|
|
5
|
+
*
|
|
6
|
+
* Powered server-side by Predexon; surfaced to the agent as a single
|
|
7
|
+
* action-dispatched tool so the inventory stays small. Keep one cohesive
|
|
8
|
+
* tool — the way TradingMarket bundles 6 actions — instead of forty
|
|
9
|
+
* one-shot capabilities, otherwise weak models start hallucinating tool
|
|
10
|
+
* names.
|
|
11
|
+
*
|
|
12
|
+
* searchPolymarket $0.001 query Polymarket markets (event filter, sort)
|
|
13
|
+
* searchKalshi $0.001 query Kalshi markets
|
|
14
|
+
* crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
|
|
15
|
+
* (the arbitrage / consensus signal)
|
|
16
|
+
* smartMoney $0.005 smart-money positioning on one Polymarket
|
|
17
|
+
* condition_id (top wallet flow + side bias)
|
|
18
|
+
*
|
|
19
|
+
* Output is filtered + truncated on the way back so a single call never
|
|
20
|
+
* dumps 100 markets into the agent's context. Default 20 rows; agents that
|
|
21
|
+
* need more should narrow the search.
|
|
22
|
+
*/
|
|
23
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
24
|
+
export declare const predictionMarketCapability: CapabilityHandler;
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PredictionMarket — unified access to Polymarket / Kalshi / cross-platform
|
|
3
|
+
* matching / smart-money endpoints via the BlockRun gateway. Each call
|
|
4
|
+
* settles via x402 against the user's USDC wallet.
|
|
5
|
+
*
|
|
6
|
+
* Powered server-side by Predexon; surfaced to the agent as a single
|
|
7
|
+
* action-dispatched tool so the inventory stays small. Keep one cohesive
|
|
8
|
+
* tool — the way TradingMarket bundles 6 actions — instead of forty
|
|
9
|
+
* one-shot capabilities, otherwise weak models start hallucinating tool
|
|
10
|
+
* names.
|
|
11
|
+
*
|
|
12
|
+
* searchPolymarket $0.001 query Polymarket markets (event filter, sort)
|
|
13
|
+
* searchKalshi $0.001 query Kalshi markets
|
|
14
|
+
* crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
|
|
15
|
+
* (the arbitrage / consensus signal)
|
|
16
|
+
* smartMoney $0.005 smart-money positioning on one Polymarket
|
|
17
|
+
* condition_id (top wallet flow + side bias)
|
|
18
|
+
*
|
|
19
|
+
* Output is filtered + truncated on the way back so a single call never
|
|
20
|
+
* dumps 100 markets into the agent's context. Default 20 rows; agents that
|
|
21
|
+
* need more should narrow the search.
|
|
22
|
+
*/
|
|
23
|
+
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
24
|
+
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
25
|
+
import { logger } from '../logger.js';
|
|
26
|
+
const TIMEOUT_MS = 30_000;
|
|
27
|
+
const DEFAULT_LIMIT = 20;
|
|
28
|
+
const MAX_LIMIT = 50;
|
|
29
|
+
// ─── Shared GET-with-x402 flow ────────────────────────────────────────────
|
|
30
|
+
async function getWithPayment(path, query, ctx) {
|
|
31
|
+
const chain = loadChain();
|
|
32
|
+
const apiUrl = API_URLS[chain];
|
|
33
|
+
const qs = new URLSearchParams();
|
|
34
|
+
for (const [k, v] of Object.entries(query)) {
|
|
35
|
+
if (v == null || v === '')
|
|
36
|
+
continue;
|
|
37
|
+
qs.set(k, String(v));
|
|
38
|
+
}
|
|
39
|
+
const queryStr = qs.toString();
|
|
40
|
+
const endpoint = `${apiUrl}${path}${queryStr ? `?${queryStr}` : ''}`;
|
|
41
|
+
const headers = {
|
|
42
|
+
Accept: 'application/json',
|
|
43
|
+
'User-Agent': `franklin/${VERSION}`,
|
|
44
|
+
};
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
47
|
+
const onAbort = () => controller.abort();
|
|
48
|
+
ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
49
|
+
try {
|
|
50
|
+
let response = await fetch(endpoint, { method: 'GET', signal: controller.signal, headers });
|
|
51
|
+
if (response.status === 402) {
|
|
52
|
+
const paymentHeaders = await signPayment(response, chain, endpoint);
|
|
53
|
+
if (!paymentHeaders) {
|
|
54
|
+
throw new Error('Payment signing failed — check wallet balance');
|
|
55
|
+
}
|
|
56
|
+
response = await fetch(endpoint, {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
headers: { ...headers, ...paymentHeaders },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const errText = await response.text().catch(() => '');
|
|
64
|
+
throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0, 200)}`);
|
|
65
|
+
}
|
|
66
|
+
return (await response.json());
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function signPayment(response, chain, endpoint) {
|
|
74
|
+
try {
|
|
75
|
+
const paymentHeader = await extractPaymentReq(response);
|
|
76
|
+
if (!paymentHeader)
|
|
77
|
+
return null;
|
|
78
|
+
if (chain === 'solana') {
|
|
79
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
80
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
81
|
+
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
82
|
+
const secretBytes = await solanaKeyToBytes(wallet.privateKey);
|
|
83
|
+
const feePayer = details.extra?.feePayer || details.recipient;
|
|
84
|
+
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
85
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
86
|
+
resourceDescription: details.resource?.description || 'Franklin PredictionMarket call',
|
|
87
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
88
|
+
extra: details.extra,
|
|
89
|
+
});
|
|
90
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
91
|
+
}
|
|
92
|
+
const wallet = await getOrCreateWallet();
|
|
93
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
94
|
+
const details = extractPaymentDetails(paymentRequired);
|
|
95
|
+
const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
96
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
97
|
+
resourceDescription: details.resource?.description || 'Franklin PredictionMarket call',
|
|
98
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
99
|
+
extra: details.extra,
|
|
100
|
+
});
|
|
101
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.warn(`[franklin] PredictionMarket payment error: ${err.message}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function extractPaymentReq(response) {
|
|
109
|
+
let header = response.headers.get('payment-required');
|
|
110
|
+
if (!header) {
|
|
111
|
+
try {
|
|
112
|
+
const body = (await response.json());
|
|
113
|
+
if (body.x402 || body.accepts)
|
|
114
|
+
header = btoa(JSON.stringify(body));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* ignore */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return header;
|
|
121
|
+
}
|
|
122
|
+
// ─── Formatting helpers ────────────────────────────────────────────────────
|
|
123
|
+
function formatUsd(value) {
|
|
124
|
+
if (value == null || !Number.isFinite(value))
|
|
125
|
+
return 'n/a';
|
|
126
|
+
if (value >= 1e9)
|
|
127
|
+
return `$${(value / 1e9).toFixed(2)}B`;
|
|
128
|
+
if (value >= 1e6)
|
|
129
|
+
return `$${(value / 1e6).toFixed(2)}M`;
|
|
130
|
+
if (value >= 1e3)
|
|
131
|
+
return `$${(value / 1e3).toFixed(1)}K`;
|
|
132
|
+
return `$${value.toFixed(2)}`;
|
|
133
|
+
}
|
|
134
|
+
function formatPct(value, digits = 1) {
|
|
135
|
+
if (value == null || !Number.isFinite(value))
|
|
136
|
+
return 'n/a';
|
|
137
|
+
return `${(value * 100).toFixed(digits)}%`;
|
|
138
|
+
}
|
|
139
|
+
// API responses sometimes come wrapped as `{data: [...], pagination: ...}`,
|
|
140
|
+
// other times as a bare array. Normalise to an array.
|
|
141
|
+
function unwrapList(raw) {
|
|
142
|
+
if (Array.isArray(raw))
|
|
143
|
+
return raw;
|
|
144
|
+
if (raw && typeof raw === 'object') {
|
|
145
|
+
const obj = raw;
|
|
146
|
+
if (Array.isArray(obj.data))
|
|
147
|
+
return obj.data;
|
|
148
|
+
if (Array.isArray(obj.markets))
|
|
149
|
+
return obj.markets;
|
|
150
|
+
if (Array.isArray(obj.pairs))
|
|
151
|
+
return obj.pairs;
|
|
152
|
+
if (Array.isArray(obj.results))
|
|
153
|
+
return obj.results;
|
|
154
|
+
}
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
async function execute(input, ctx) {
|
|
158
|
+
const { action, search, status, sort, limit, conditionId } = input;
|
|
159
|
+
const cappedLimit = Math.min(Math.max(1, limit ?? DEFAULT_LIMIT), MAX_LIMIT);
|
|
160
|
+
if (!action) {
|
|
161
|
+
return { output: 'Error: action is required (searchPolymarket | searchKalshi | crossPlatform | smartMoney)', isError: true };
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
switch (action) {
|
|
165
|
+
case 'searchPolymarket': {
|
|
166
|
+
const raw = await getWithPayment('/api/v1/pm/polymarket/markets', {
|
|
167
|
+
search,
|
|
168
|
+
status: status ?? 'active',
|
|
169
|
+
sort: sort ?? 'volume',
|
|
170
|
+
limit: cappedLimit,
|
|
171
|
+
}, ctx);
|
|
172
|
+
const markets = unwrapList(raw);
|
|
173
|
+
if (markets.length === 0) {
|
|
174
|
+
return { output: 'No Polymarket markets matched the filters.' };
|
|
175
|
+
}
|
|
176
|
+
const lines = [
|
|
177
|
+
`## Polymarket — ${markets.length} market${markets.length === 1 ? '' : 's'}` +
|
|
178
|
+
(search ? ` · search="${search}"` : '') +
|
|
179
|
+
(status ? ` · status=${status}` : '') +
|
|
180
|
+
` · sort=${sort ?? 'volume'}`,
|
|
181
|
+
'',
|
|
182
|
+
];
|
|
183
|
+
markets.forEach((m, i) => {
|
|
184
|
+
const yesPx = m.outcomes && m.outcome_prices && m.outcomes.length === m.outcome_prices.length
|
|
185
|
+
? m.outcomes.map((o, j) => `${o}=${formatPct(m.outcome_prices[j])}`).join(' / ')
|
|
186
|
+
: 'n/a';
|
|
187
|
+
const cid = m.condition_id ? ` · condition_id=\`${m.condition_id.slice(0, 14)}…\`` : '';
|
|
188
|
+
lines.push(`${i + 1}. **${m.question || m.market_slug || 'untitled'}**${cid}\n` +
|
|
189
|
+
` prices: ${yesPx} · vol: ${formatUsd(m.volume)} · liq: ${formatUsd(m.liquidity)}` +
|
|
190
|
+
(m.end_date ? ` · ends ${m.end_date.slice(0, 10)}` : ''));
|
|
191
|
+
});
|
|
192
|
+
lines.push('', `_$0.001 paid via x402._`);
|
|
193
|
+
return { output: lines.join('\n') };
|
|
194
|
+
}
|
|
195
|
+
case 'searchKalshi': {
|
|
196
|
+
const raw = await getWithPayment('/api/v1/pm/kalshi/markets', {
|
|
197
|
+
search,
|
|
198
|
+
status: status ?? 'open',
|
|
199
|
+
sort: sort ?? 'volume',
|
|
200
|
+
limit: cappedLimit,
|
|
201
|
+
}, ctx);
|
|
202
|
+
const markets = unwrapList(raw);
|
|
203
|
+
if (markets.length === 0) {
|
|
204
|
+
return { output: 'No Kalshi markets matched the filters.' };
|
|
205
|
+
}
|
|
206
|
+
const lines = [
|
|
207
|
+
`## Kalshi — ${markets.length} market${markets.length === 1 ? '' : 's'}` +
|
|
208
|
+
(search ? ` · search="${search}"` : '') +
|
|
209
|
+
` · status=${status ?? 'open'} · sort=${sort ?? 'volume'}`,
|
|
210
|
+
'',
|
|
211
|
+
];
|
|
212
|
+
markets.forEach((m, i) => {
|
|
213
|
+
// Kalshi quotes prices in cents (0–100). Surface them as a tight
|
|
214
|
+
// bid/ask so the agent can read implied probability at a glance.
|
|
215
|
+
const bid = m.yes_bid != null ? `${m.yes_bid}¢` : 'n/a';
|
|
216
|
+
const ask = m.yes_ask != null ? `${m.yes_ask}¢` : 'n/a';
|
|
217
|
+
const ticker = m.ticker ? ` · ticker=\`${m.ticker}\`` : '';
|
|
218
|
+
lines.push(`${i + 1}. **${m.title || m.ticker || 'untitled'}**${ticker}\n` +
|
|
219
|
+
` yes ${bid}/${ask} · vol: ${m.volume?.toLocaleString() ?? 'n/a'} · OI: ${m.open_interest?.toLocaleString() ?? 'n/a'}` +
|
|
220
|
+
(m.close_time ? ` · closes ${m.close_time.slice(0, 10)}` : ''));
|
|
221
|
+
});
|
|
222
|
+
lines.push('', `_$0.001 paid via x402._`);
|
|
223
|
+
return { output: lines.join('\n') };
|
|
224
|
+
}
|
|
225
|
+
case 'crossPlatform': {
|
|
226
|
+
const raw = await getWithPayment('/api/v1/pm/matching-markets/pairs', {
|
|
227
|
+
limit: cappedLimit,
|
|
228
|
+
}, ctx);
|
|
229
|
+
const pairs = unwrapList(raw);
|
|
230
|
+
if (pairs.length === 0) {
|
|
231
|
+
return { output: 'No matched market pairs available right now.' };
|
|
232
|
+
}
|
|
233
|
+
const lines = [
|
|
234
|
+
`## Cross-platform matched pairs — ${pairs.length}`,
|
|
235
|
+
'_Polymarket ↔ Kalshi equivalent markets. Use these to compare implied probabilities across venues._',
|
|
236
|
+
'',
|
|
237
|
+
];
|
|
238
|
+
pairs.forEach((p, i) => {
|
|
239
|
+
const sim = p.similarity != null ? ` · similarity ${formatPct(p.similarity, 0)}` : '';
|
|
240
|
+
lines.push(`${i + 1}. **Polymarket:** ${p.polymarket_question || '(untitled)'}\n` +
|
|
241
|
+
` **Kalshi:** ${p.kalshi_title || '(untitled)'}` +
|
|
242
|
+
(p.kalshi_ticker ? ` · ticker=\`${p.kalshi_ticker}\`` : '') +
|
|
243
|
+
sim);
|
|
244
|
+
});
|
|
245
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
246
|
+
return { output: lines.join('\n') };
|
|
247
|
+
}
|
|
248
|
+
case 'smartMoney': {
|
|
249
|
+
if (!conditionId) {
|
|
250
|
+
return { output: 'Error: conditionId is required for smartMoney (Polymarket condition_id from a prior searchPolymarket call)', isError: true };
|
|
251
|
+
}
|
|
252
|
+
const path = `/api/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
|
|
253
|
+
const data = await getWithPayment(path, {}, ctx);
|
|
254
|
+
const buyers = (data.buyers ?? []).slice(0, 5);
|
|
255
|
+
const sellers = (data.sellers ?? []).slice(0, 5);
|
|
256
|
+
const lines = [
|
|
257
|
+
`## Smart money — \`${conditionId.slice(0, 14)}…\``,
|
|
258
|
+
];
|
|
259
|
+
if (data.net_yes_size != null || data.net_no_size != null) {
|
|
260
|
+
const yesSize = formatUsd(data.net_yes_size);
|
|
261
|
+
const noSize = formatUsd(data.net_no_size);
|
|
262
|
+
lines.push(`**Net flow:** YES ${yesSize} / NO ${noSize}`);
|
|
263
|
+
}
|
|
264
|
+
if (buyers.length > 0) {
|
|
265
|
+
lines.push('', '**Top buyers**');
|
|
266
|
+
buyers.forEach((b, i) => {
|
|
267
|
+
const w = b.wallet ? `${b.wallet.slice(0, 8)}…${b.wallet.slice(-4)}` : 'unknown';
|
|
268
|
+
lines.push(`${i + 1}. ${w} — ${formatUsd(b.size)} on ${b.outcome ?? 'unknown side'}`);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (sellers.length > 0) {
|
|
272
|
+
lines.push('', '**Top sellers**');
|
|
273
|
+
sellers.forEach((s, i) => {
|
|
274
|
+
const w = s.wallet ? `${s.wallet.slice(0, 8)}…${s.wallet.slice(-4)}` : 'unknown';
|
|
275
|
+
lines.push(`${i + 1}. ${w} — ${formatUsd(s.size)} on ${s.outcome ?? 'unknown side'}`);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (buyers.length === 0 && sellers.length === 0) {
|
|
279
|
+
lines.push('No smart-money flow recorded for this market yet.');
|
|
280
|
+
}
|
|
281
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
282
|
+
return { output: lines.join('\n') };
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
return {
|
|
286
|
+
output: `Error: unknown action "${action}". Use: searchPolymarket, searchKalshi, crossPlatform, smartMoney`,
|
|
287
|
+
isError: true,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
export const predictionMarketCapability = {
|
|
296
|
+
spec: {
|
|
297
|
+
name: 'PredictionMarket',
|
|
298
|
+
description: 'Real prediction market data via the BlockRun gateway (powered by Predexon). Use for any "what are the odds of X" / "Polymarket on Y" / "Kalshi market for Z" question. ' +
|
|
299
|
+
'Actions: ' +
|
|
300
|
+
'`searchPolymarket` (search Polymarket markets — $0.001), ' +
|
|
301
|
+
'`searchKalshi` (search Kalshi markets — $0.001), ' +
|
|
302
|
+
'`crossPlatform` (matched pairs across Polymarket+Kalshi for arbitrage / consensus — $0.005), ' +
|
|
303
|
+
'`smartMoney` (smart-money positioning on a Polymarket condition_id — $0.005). ' +
|
|
304
|
+
'Polymarket and Kalshi are the two largest legit prediction markets; cross-platform matching is unique to BlockRun. ' +
|
|
305
|
+
'For "should I bet on X" / "what does the market price imply": run searchPolymarket AND searchKalshi in parallel and compare implied probabilities — divergence is the signal.',
|
|
306
|
+
input_schema: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
action: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
enum: ['searchPolymarket', 'searchKalshi', 'crossPlatform', 'smartMoney'],
|
|
312
|
+
description: 'Which prediction-market query to run. See tool description for cost per action.',
|
|
313
|
+
},
|
|
314
|
+
search: {
|
|
315
|
+
type: 'string',
|
|
316
|
+
description: 'Search query (3-100 chars). Used by searchPolymarket / searchKalshi. Skip for crossPlatform/smartMoney.',
|
|
317
|
+
},
|
|
318
|
+
status: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
description: 'Polymarket: active | closed | archived (default active). Kalshi: open | closed (default open).',
|
|
321
|
+
},
|
|
322
|
+
sort: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'Polymarket: volume | liquidity | created (default volume). Kalshi: volume | open_interest | price_desc | price_asc | close_time (default volume).',
|
|
325
|
+
},
|
|
326
|
+
limit: {
|
|
327
|
+
type: 'number',
|
|
328
|
+
description: `Max results (default ${DEFAULT_LIMIT}, hard cap ${MAX_LIMIT}).`,
|
|
329
|
+
},
|
|
330
|
+
conditionId: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description: 'Polymarket condition_id. Required for smartMoney. Get one from a prior searchPolymarket call.',
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
required: ['action'],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
execute,
|
|
339
|
+
concurrent: true,
|
|
340
|
+
};
|
|
@@ -44,6 +44,11 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
44
44
|
// training-data guessing.
|
|
45
45
|
'TradingMarket',
|
|
46
46
|
'TradingSignal',
|
|
47
|
+
// Prediction market data — Polymarket, Kalshi, cross-platform matching,
|
|
48
|
+
// smart money. The "what are the odds of X" / "Polymarket on Y"
|
|
49
|
+
// category. Cross-platform pair lookup is unique to the gateway and
|
|
50
|
+
// is the kind of data a non-wallet agent fundamentally cannot reach.
|
|
51
|
+
'PredictionMarket',
|
|
47
52
|
// Research — synthesized answers with real citations, semantic web
|
|
48
53
|
// search, and clean URL fetching. Any factual current-events question
|
|
49
54
|
// ("why did X drop?") should route here rather than the model's prior.
|
package/package.json
CHANGED