@onride/agent 0.0.0-stage → 0.5.1-rc.0

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.
Files changed (64) hide show
  1. package/.env.example +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -2
  4. package/dist/account_balances.js +435 -0
  5. package/dist/account_reads.js +199 -0
  6. package/dist/agent/adapter.js +115 -0
  7. package/dist/agent/analytics.js +174 -0
  8. package/dist/agent/backend.js +237 -0
  9. package/dist/agent/card_hosts.js +9 -0
  10. package/dist/agent/contracts.js +276 -0
  11. package/dist/agent/conversational.js +86 -0
  12. package/dist/agent/core_bridge.js +1155 -0
  13. package/dist/agent/deposits.js +309 -0
  14. package/dist/agent/discovery.js +205 -0
  15. package/dist/agent/domain.js +507 -0
  16. package/dist/agent/firebase.js +34 -0
  17. package/dist/agent/funding.js +230 -0
  18. package/dist/agent/http.js +40 -0
  19. package/dist/agent/human.js +298 -0
  20. package/dist/agent/human_handoffs.js +39 -0
  21. package/dist/agent/ledger.js +137 -0
  22. package/dist/agent/mcp.js +245 -0
  23. package/dist/agent/order_rules.js +61 -0
  24. package/dist/agent/perps_account.js +41 -0
  25. package/dist/agent/prediction.js +607 -0
  26. package/dist/agent/prediction_equity_history.js +910 -0
  27. package/dist/agent/prediction_history.js +336 -0
  28. package/dist/agent/preferences.js +114 -0
  29. package/dist/agent/repository.js +106 -0
  30. package/dist/agent/runner.js +683 -0
  31. package/dist/agent/service.js +1347 -0
  32. package/dist/backend.js +6 -0
  33. package/dist/bridge.js +6 -0
  34. package/dist/cli/clients.js +99 -0
  35. package/dist/cli/commands.js +332 -0
  36. package/dist/cli/onboarding.js +53 -0
  37. package/dist/cli/session.js +224 -0
  38. package/dist/cli/setup.js +256 -0
  39. package/dist/cli.js +16 -0
  40. package/dist/guide.js +92 -0
  41. package/dist/http_security.js +79 -0
  42. package/dist/mcp-cli.js +7 -0
  43. package/dist/oauth.js +1032 -0
  44. package/dist/server.js +171 -0
  45. package/dist/start.js +18 -0
  46. package/dist/version.js +25 -0
  47. package/docs/confirmation-cards.md +118 -0
  48. package/docs/conversation-v050.md +30 -0
  49. package/docs/conversational-v17.md +48 -0
  50. package/docs/deployment.md +58 -0
  51. package/docs/perps-leverage.md +43 -0
  52. package/docs/prediction-history.md +28 -0
  53. package/docs/preferences-funding-v042.md +17 -0
  54. package/docs/ride-agent-migration.md +127 -0
  55. package/docs/v17-completion-checklist.md +31 -0
  56. package/docs/v17-rollout.md +44 -0
  57. package/docs/widget-account-v043.md +17 -0
  58. package/package.json +75 -4
  59. package/public/agent.html +2313 -0
  60. package/public/confirm.html +2373 -0
  61. package/public/fund.html +563 -0
  62. package/skills/ride/SKILL.md +18 -0
  63. package/skills/ride/references/commands.md +47 -0
  64. package/skills/ride/references/workflow.md +47 -0
@@ -0,0 +1,607 @@
1
+ import { JsonRpcProvider, Contract, Interface } from "ethers";
2
+ import { AgentError } from "./contracts.js";
3
+ import { PredictionHistory } from "./prediction_history.js";
4
+ import { PredictionEquityHistory } from "./prediction_equity_history.js";
5
+ import { D, cash, fingerprint, now, fresh, } from "./domain.js";
6
+ const transfers = new Interface([
7
+ "event Transfer(address indexed from,address indexed to,uint256 value)",
8
+ "event TransferSingle(address indexed operator,address indexed from,address indexed to,uint256 id,uint256 value)",
9
+ "event TransferBatch(address indexed operator,address indexed from,address indexed to,uint256[] ids,uint256[] values)",
10
+ ]);
11
+ const erc20 = ["function balanceOf(address) view returns(uint256)"];
12
+ export class PredictionVenue {
13
+ pool;
14
+ core;
15
+ salt;
16
+ rpcUrl;
17
+ rpc = null;
18
+ poolCache = null;
19
+ history;
20
+ constructor(pool, core, salt, rpcUrl) {
21
+ this.pool = pool;
22
+ this.core = core;
23
+ this.salt = salt;
24
+ this.rpcUrl = rpcUrl;
25
+ this.history = new PredictionHistory(rpcUrl
26
+ ? {
27
+ loader: new PredictionEquityHistory(rpcUrl, () => this.core("prediction_contracts", {})),
28
+ key: (wallet) => this.id(wallet),
29
+ read: async (key) => (await this.pool.query("SELECT value FROM ride_agent_prediction_history WHERE key=$1", [key])).rows[0]?.value ?? null,
30
+ write: async (key, record) => {
31
+ await this.pool.query("INSERT INTO ride_agent_prediction_history(key,value) VALUES($1,$2) ON CONFLICT(key) DO UPDATE SET value=EXCLUDED.value", [key, record]);
32
+ },
33
+ }
34
+ : undefined);
35
+ }
36
+ async migrate() {
37
+ await this.pool.query("CREATE TABLE IF NOT EXISTS ride_agent_prediction_outcomes (key TEXT PRIMARY KEY, value JSONB NOT NULL)");
38
+ await this.pool.query("CREATE TABLE IF NOT EXISTS ride_agent_prediction_history (key TEXT PRIMARY KEY, value JSONB NOT NULL)");
39
+ }
40
+ provider() {
41
+ if (!this.rpcUrl)
42
+ throw new AgentError("VENUE_UNAVAILABLE", "Prediction RPC is not configured.", true, 503);
43
+ if (!this.rpc)
44
+ this.rpc = new JsonRpcProvider(this.rpcUrl, 137, { staticNetwork: true });
45
+ return this.rpc;
46
+ }
47
+ id(address) {
48
+ return `trader_${fingerprint([this.salt, "prediction", address.toLowerCase()]).slice(0, 32)}`;
49
+ }
50
+ async data(path, params) {
51
+ const url = new URL(path, "https://data-api.polymarket.com");
52
+ for (const [key, value] of Object.entries(params))
53
+ url.searchParams.set(key, value);
54
+ const r = await fetch(url, {
55
+ signal: AbortSignal.timeout(12000),
56
+ redirect: "error",
57
+ });
58
+ if (!r.ok)
59
+ throw new AgentError("VENUE_UNAVAILABLE", "Prediction data is unavailable.", true, 503);
60
+ return r.json();
61
+ }
62
+ async positions(address) {
63
+ const out = [];
64
+ for (let offset = 0; offset <= 10000; offset += 500) {
65
+ const rows = await this.data("/positions", {
66
+ user: address,
67
+ sizeThreshold: "0",
68
+ includeArchived: "true",
69
+ limit: "500",
70
+ offset: String(offset),
71
+ });
72
+ if (!Array.isArray(rows))
73
+ throw new AgentError("SOURCE_STALE", "Prediction positions are incomplete.", true);
74
+ out.push(...rows);
75
+ if (rows.length < 500)
76
+ return out;
77
+ }
78
+ throw new AgentError("SOURCE_STALE", "Prediction position coverage exceeds the supported page window.", true);
79
+ }
80
+ async cashBalance(address, identity, blockTag) {
81
+ const constants = identity ?? (await this.core("prediction_contracts", {}));
82
+ const balances = await Promise.all([constants.collateral, constants.wrapped_collateral].map((token) => new Contract(token, erc20, this.provider()).balanceOf(address, blockTag == null ? {} : { blockTag })));
83
+ return D(balances.reduce((n, v) => n + BigInt(v), 0n).toString())
84
+ .div(1e6)
85
+ .toString();
86
+ }
87
+ async remember(row) {
88
+ const token = String(row.asset);
89
+ if (!/^\d{1,100}$/.test(token) || typeof row.conditionId !== "string")
90
+ throw new AgentError("SOURCE_STALE", "Prediction token identity is incomplete.", true);
91
+ const value = {
92
+ key: `prediction:outcome_${fingerprint(token).slice(0, 32)}`,
93
+ token_id: token,
94
+ condition_id: row.conditionId,
95
+ asset: `EVENT${fingerprint(row.conditionId).slice(0, 8).toUpperCase()}`,
96
+ };
97
+ await this.pool.query("INSERT INTO ride_agent_prediction_outcomes(key,value) VALUES($1,$2) ON CONFLICT DO NOTHING", [value.key, value]);
98
+ return value;
99
+ }
100
+ async outcome(key) {
101
+ const r = await this.pool.query("SELECT value FROM ride_agent_prediction_outcomes WHERE key=$1", [key]);
102
+ if (!r.rowCount)
103
+ throw new AgentError("NOT_FOUND", "Prediction outcome metadata is unavailable.", false, 404);
104
+ return r.rows[0].value;
105
+ }
106
+ async book(token) {
107
+ const r = await fetch(`https://clob.polymarket.com/book?token_id=${encodeURIComponent(token)}`, { signal: AbortSignal.timeout(12000), redirect: "error" });
108
+ if (!r.ok)
109
+ throw new AgentError("STALE_PRICE", "Prediction order book unavailable.", true);
110
+ const b = (await r.json());
111
+ if (!Array.isArray(b.bids) || !Array.isArray(b.asks))
112
+ throw new AgentError("STALE_PRICE", "Prediction book is incomplete.", true);
113
+ return b;
114
+ }
115
+ async rows() {
116
+ if (this.poolCache && now() - this.poolCache.at < 30000)
117
+ return this.poolCache.rows;
118
+ const rows = await this.core("prediction_pool", {});
119
+ if (!Array.isArray(rows))
120
+ throw new AgentError("SOURCE_STALE", "Prediction pool is unavailable.", true);
121
+ this.poolCache = { at: now(), rows };
122
+ return rows;
123
+ }
124
+ async resolve(id) {
125
+ const row = (await this.rows()).find((x) => this.id(x.address) === id);
126
+ if (!row)
127
+ throw new AgentError("NOT_FOUND", "Fresh prediction trader identity is unavailable.", false, 404);
128
+ return row;
129
+ }
130
+ async recognizes(id) {
131
+ return (await this.rows()).some((r) => this.id(r.address) === id);
132
+ }
133
+ async source(id) {
134
+ const row = await this.resolve(id);
135
+ const rows = await this.positions(row.address);
136
+ const positions = [];
137
+ for (const row of rows.filter((r) => D(r.size).gt(0))) {
138
+ const meta = await this.remember(row);
139
+ positions.push({
140
+ ...meta,
141
+ market: "prediction",
142
+ quantity: String(row.size),
143
+ price: String(row.curPrice),
144
+ source_entry_price: row.avgPrice == null ? null : String(row.avgPrice),
145
+ source_leverage: 1,
146
+ observed_at: now(),
147
+ source_revision: "",
148
+ });
149
+ }
150
+ const balance = await this.cashBalance(row.address);
151
+ const equity = D(balance).plus(rows.reduce((n, r) => n.plus(r.currentValue ?? D(r.size).times(r.curPrice)), D(0)));
152
+ const revision = fingerprint(positions.map((p) => [p.key, p.quantity, p.source_entry_price]));
153
+ positions.forEach((p) => (p.source_revision = revision));
154
+ return {
155
+ observed_at: now(),
156
+ revision,
157
+ account_value_usdc: equity.toString(),
158
+ positions,
159
+ complete: true,
160
+ };
161
+ }
162
+ async profile(id) {
163
+ const row = await this.resolve(id);
164
+ const metrics = await this.history.metrics(row.address);
165
+ const expiresAt = Math.min(row.expires_at, metrics.metric_window
166
+ ? metrics.metric_window.end_at + 36 * 3600000
167
+ : Infinity);
168
+ // Discovery uses the independently refreshed score snapshot. Live inventory
169
+ // and collateral remain separate source() checks before increasing exposure;
170
+ // requiring them here hides otherwise valid candidates on an RPC outage.
171
+ const components = Object.fromEntries(Object.entries(row.components ?? {}).map(([k, v]) => [
172
+ k,
173
+ v == null ? null : Number(v),
174
+ ]));
175
+ return {
176
+ id,
177
+ handle: typeof row.handle === "string" &&
178
+ !/0x[a-fA-F0-9]{40}\b/.test(row.handle)
179
+ ? row.handle
180
+ : `Ride trader ${id.slice(-6)}`,
181
+ market: "prediction",
182
+ score: Number(row.score),
183
+ score_version: row.score_version,
184
+ components,
185
+ scored_at: row.computed_at,
186
+ expires_at: expiresAt,
187
+ eligibility: expiresAt > now() ? "PASS" : "REJECT",
188
+ assets: row.assets
189
+ ?.map((x) => x
190
+ .replace(/[^A-Za-z0-9]/g, "")
191
+ .toUpperCase()
192
+ .slice(0, 16))
193
+ .filter(Boolean) ?? ["PREDICTION"],
194
+ style: row.assets?.[0] ?? "prediction",
195
+ direction: "long",
196
+ median_hold_hours: null,
197
+ ...metrics,
198
+ atr_pct: null,
199
+ venue_max_leverage: 1,
200
+ min_notional_usdc: "5",
201
+ last_trade_at: null,
202
+ };
203
+ }
204
+ async candidates() {
205
+ const out = [];
206
+ const rows = (await this.rows()).slice(0, 20);
207
+ // Two bounded batches keep cold historical reads inside the bridge deadline.
208
+ for (let i = 0; i < rows.length; i += 10) {
209
+ const batch = await Promise.allSettled(rows.slice(i, i + 10).map((r) => this.profile(this.id(r.address))));
210
+ for (const r of batch)
211
+ if (r.status === "fulfilled")
212
+ out.push(r.value);
213
+ }
214
+ return out;
215
+ }
216
+ async fundingBalance(user) {
217
+ const identity = await this.core("prediction_wallet", {
218
+ user_id: user,
219
+ portfolio_id: null,
220
+ });
221
+ const head = await this.provider().getBlockNumber();
222
+ const block = Math.max(0, head - 2);
223
+ const balance = await this.cashBalance(identity.wallet, identity, block);
224
+ return {
225
+ trading_balance_usdc: cash(balance),
226
+ trading_observed_at: now(),
227
+ chain_block: block,
228
+ available_to_copy: identity.eligible === true && D(balance).gt(0),
229
+ trading_observation: "verified",
230
+ };
231
+ }
232
+ async account(user, p, bind = false) {
233
+ const input = {
234
+ user_id: user,
235
+ portfolio_id: p.id,
236
+ revision: p.revision,
237
+ budget_usdc: cash(p.sleeves
238
+ .filter((s) => s.trader.market === "prediction" && s.state !== "closed")
239
+ .reduce((n, s) => n.plus(s.amount_usdc), D(0))),
240
+ };
241
+ let identity = await this.core("prediction_wallet", input);
242
+ const positionsRows = await this.positions(identity.wallet);
243
+ if (bind) {
244
+ if (positionsRows.some((r) => D(r.size).gt(0)))
245
+ throw new AgentError("OWNERSHIP_CONFLICT", "Existing prediction inventory must remain with its current owner.");
246
+ await this.core("prediction_claim", input);
247
+ identity = await this.core("prediction_wallet", input);
248
+ }
249
+ const head = await this.provider().getBlockNumber();
250
+ const block = Math.max(0, head - 2);
251
+ const balances = await this.cashBalance(identity.wallet, identity, block);
252
+ const refs = new Map();
253
+ for (const row of positionsRows) {
254
+ const o = await this.remember(row);
255
+ refs.set(o.key, o);
256
+ }
257
+ for (const s of p.sleeves)
258
+ for (const key of [
259
+ ...Object.keys(s.intents),
260
+ ...Object.keys(s.lots),
261
+ ].filter((k) => k.startsWith("prediction:")))
262
+ if (!refs.has(key))
263
+ refs.set(key, await this.outcome(key));
264
+ const positions = {}, prices = {}, min = {};
265
+ const ctf = new Contract(identity.conditional_tokens, ["function balanceOf(address,uint256) view returns(uint256)"], this.provider());
266
+ for (const ref of refs.values()) {
267
+ positions[ref.key] = D(String(await ctf.balanceOf(identity.wallet, ref.token_id, {
268
+ blockTag: block,
269
+ })))
270
+ .div(1e6)
271
+ .toString();
272
+ const row = positionsRows.find((r) => String(r.asset) === ref.token_id);
273
+ try {
274
+ const book = await this.book(ref.token_id);
275
+ const bids = book.bids.map((r) => D(r.price)), asks = book.asks.map((r) => D(r.price));
276
+ const bid = bids.length ? D.max(...bids) : null, ask = asks.length ? D.min(...asks) : null;
277
+ if (bid && ask)
278
+ prices[ref.key] = bid.plus(ask).div(2).toString();
279
+ else if (row?.curPrice > 0)
280
+ prices[ref.key] = String(row.curPrice);
281
+ min[ref.key] = cash(D(book.min_order_size ?? 5).times(prices[ref.key] ?? 1));
282
+ }
283
+ catch {
284
+ if (row?.curPrice > 0)
285
+ prices[ref.key] = String(row.curPrice);
286
+ else if (!D(positions[ref.key]).isZero())
287
+ throw new AgentError("STALE_PRICE", "Resolved prediction inventory needs settlement evidence.", true);
288
+ }
289
+ }
290
+ const evidence = await this.settlement(user, p, identity, head);
291
+ const marketValue = Object.entries(positions).reduce((n, [k, q]) => n.plus(D(q).times(prices[k] ?? 0)), D(0));
292
+ const ownership = identity.ownership;
293
+ return {
294
+ account_id: `account_${fingerprint([user, p.id, "prediction"]).slice(0, 32)}`,
295
+ venue_accounts: { prediction: p.id },
296
+ chain_block: block,
297
+ observed_at: now(),
298
+ account_value_usdc: D(balances).plus(marketValue).toString(),
299
+ available_usdc: balances,
300
+ available_by_market: { prediction: balances },
301
+ dedicated: ownership?.portfolio_id === p.id ||
302
+ (!positionsRows.some((r) => D(r.size).gt(0)) && !ownership),
303
+ eligible: identity.eligible === true,
304
+ positions,
305
+ prices,
306
+ min_notionals: min,
307
+ quantity_decimals: Object.fromEntries([...refs.keys()].map((k) => [k, 2])),
308
+ max_leverages: Object.fromEntries([...refs.keys()].map((k) => [k, 1])),
309
+ orders: evidence.orders,
310
+ fills: evidence.fills,
311
+ funding: [],
312
+ flows: evidence.flows,
313
+ coverage_complete: evidence.complete,
314
+ };
315
+ }
316
+ async settlement(user, p, identity, block) {
317
+ const fills = [], flows = [], orders = [];
318
+ let complete = true;
319
+ const known = new Set();
320
+ const wallet = identity.wallet.toLowerCase();
321
+ const records = await Promise.all(p.executions
322
+ .filter((e) => e.key.startsWith("prediction:") && e.task_id)
323
+ .map(async (execution) => ({
324
+ execution,
325
+ result: await this.core("prediction_execution", {
326
+ user_id: user,
327
+ portfolio_id: p.id,
328
+ task_id: execution.task_id,
329
+ }),
330
+ })));
331
+ const owners = new Map();
332
+ for (const { execution, result } of records) {
333
+ let response;
334
+ try {
335
+ response = JSON.parse(result.response ?? "{}");
336
+ }
337
+ catch {
338
+ response = {};
339
+ }
340
+ for (const trade of response.agent_settlement?.trades ?? [])
341
+ if (trade.transaction_hash) {
342
+ const key = String(trade.transaction_hash).toLowerCase();
343
+ const group = owners.get(key) ?? new Set();
344
+ group.add(execution.id);
345
+ owners.set(key, group);
346
+ }
347
+ }
348
+ for (const { execution, result } of records) {
349
+ const ref = await this.outcome(execution.key);
350
+ let response;
351
+ try {
352
+ response = JSON.parse(result.response ?? "{}");
353
+ }
354
+ catch {
355
+ response = {};
356
+ }
357
+ if ([1, 2, 7, 8].includes(result.status)) {
358
+ orders.push({
359
+ id: execution.task_id,
360
+ key: execution.key,
361
+ increases_risk: result.side === 1,
362
+ });
363
+ }
364
+ const settled = response.agent_settlement;
365
+ if (!settled?.complete) {
366
+ if (D(result.filled_size).gt(0) || result.status === 3)
367
+ complete = false;
368
+ continue;
369
+ }
370
+ const byHash = new Map();
371
+ for (const trade of settled.trades ?? []) {
372
+ if (trade.status !== "CONFIRMED" || !trade.transaction_hash) {
373
+ complete = false;
374
+ continue;
375
+ }
376
+ const rows = byHash.get(trade.transaction_hash) ?? [];
377
+ rows.push(trade);
378
+ byHash.set(trade.transaction_hash, rows);
379
+ }
380
+ for (const [hash, trades] of byHash) {
381
+ if ((owners.get(hash.toLowerCase())?.size ?? 0) !== 1 ||
382
+ known.has(hash)) {
383
+ complete = false;
384
+ continue;
385
+ }
386
+ known.add(hash);
387
+ const receipt = await this.provider().getTransactionReceipt(hash);
388
+ if (!receipt ||
389
+ receipt.status !== 1 ||
390
+ block - receipt.blockNumber < 2) {
391
+ complete = false;
392
+ continue;
393
+ }
394
+ let cashDelta = 0n, quantity = 0n;
395
+ let otherInventory = false;
396
+ for (const log of receipt.logs) {
397
+ try {
398
+ const parsed = transfers.parseLog(log);
399
+ if (!parsed)
400
+ continue;
401
+ const from = String(parsed.args.from).toLowerCase(), to = String(parsed.args.to).toLowerCase(), direction = (to === wallet ? 1n : 0n) - (from === wallet ? 1n : 0n);
402
+ if (!direction)
403
+ continue;
404
+ if ([identity.collateral, identity.wrapped_collateral].some((t) => String(t).toLowerCase() === log.address.toLowerCase()) &&
405
+ parsed.name === "Transfer")
406
+ cashDelta += direction * BigInt(parsed.args.value);
407
+ if (log.address.toLowerCase() ===
408
+ identity.conditional_tokens.toLowerCase()) {
409
+ const ids = parsed.name === "TransferSingle"
410
+ ? [parsed.args.id]
411
+ : parsed.args.ids;
412
+ const values = parsed.name === "TransferSingle"
413
+ ? [parsed.args.value]
414
+ : parsed.args[4];
415
+ for (let i = 0; i < ids.length; i++) {
416
+ if (String(ids[i]) === ref.token_id)
417
+ quantity += direction * BigInt(values[i]);
418
+ else
419
+ otherInventory = true;
420
+ }
421
+ }
422
+ }
423
+ catch {
424
+ /* Unrelated event. */
425
+ }
426
+ }
427
+ if (otherInventory || quantity === 0n || cashDelta === 0n) {
428
+ complete = false;
429
+ continue;
430
+ }
431
+ const q = D(quantity.toString()).div(1e6), netCash = D(cashDelta.toString()).div(1e6), gross = trades.reduce((n, t) => n.plus(D(t.quantity).times(t.price)), D(0)), size = trades.reduce((n, t) => n.plus(t.quantity), D(0));
432
+ if (size.isZero()) {
433
+ complete = false;
434
+ continue;
435
+ }
436
+ const price = gross.div(size);
437
+ const fee = q.isPositive()
438
+ ? netCash.neg().minus(q.times(price))
439
+ : q.abs().times(price).minus(netCash);
440
+ if (fee.lt(0) || fee.gt(gross.times(".1"))) {
441
+ complete = false;
442
+ continue;
443
+ }
444
+ const mined = await this.provider().getBlock(receipt.blockNumber);
445
+ if (!mined) {
446
+ complete = false;
447
+ continue;
448
+ }
449
+ fills.push({
450
+ id: `poly_${fingerprint([execution.task_id, hash]).slice(0, 32)}`,
451
+ order_id: result.order_id,
452
+ task_id: execution.task_id,
453
+ key: execution.key,
454
+ quantity: q.toString(),
455
+ price: price.toString(),
456
+ fee_usdc: fee.toString(),
457
+ at: mined.timestamp * 1000,
458
+ });
459
+ }
460
+ }
461
+ // Full ERC20 cashflow coverage from the ownership checkpoint, including wrapping.
462
+ // Unknown token inventory events remain unresolved and block new risk.
463
+ if (p.ledger_chain_block != null) {
464
+ const sums = new Map();
465
+ const address = "0x" + wallet.slice(2).padStart(64, "0");
466
+ for (let from = p.ledger_chain_block; from <= block - 2; from += 500) {
467
+ const to = Math.min(block - 2, from + 499);
468
+ for (const token of [identity.collateral, identity.wrapped_collateral])
469
+ for (const topics of [
470
+ [transfers.getEvent("Transfer").topicHash, null, address],
471
+ [transfers.getEvent("Transfer").topicHash, address],
472
+ ]) {
473
+ const logs = await this.provider().getLogs({
474
+ address: token,
475
+ fromBlock: from,
476
+ toBlock: to,
477
+ topics,
478
+ });
479
+ for (const log of logs) {
480
+ if (known.has(log.transactionHash))
481
+ continue;
482
+ const receipt = await this.provider().getTransactionReceipt(log.transactionHash);
483
+ if (!receipt ||
484
+ receipt.logs.some((l) => {
485
+ if (l.address.toLowerCase() !==
486
+ identity.conditional_tokens.toLowerCase())
487
+ return false;
488
+ try {
489
+ const p = transfers.parseLog(l);
490
+ return (p &&
491
+ (String(p.args.from).toLowerCase() === wallet ||
492
+ String(p.args.to).toLowerCase() === wallet));
493
+ }
494
+ catch {
495
+ return false;
496
+ }
497
+ })) {
498
+ complete = false;
499
+ continue;
500
+ }
501
+ const parsed = transfers.parseLog(log);
502
+ const fromAddr = String(parsed.args.from).toLowerCase(), toAddr = String(parsed.args.to).toLowerCase();
503
+ if (fromAddr === wallet && toAddr === wallet)
504
+ continue;
505
+ const sign = toAddr === wallet ? 1n : -1n;
506
+ const old = sums.get(log.transactionHash) ?? {
507
+ amount: 0n,
508
+ at: log.blockNumber,
509
+ };
510
+ old.amount += sign * BigInt(parsed.args.value);
511
+ sums.set(log.transactionHash, old);
512
+ }
513
+ }
514
+ }
515
+ for (const [hash, flow] of sums) {
516
+ if (flow.amount === 0n)
517
+ continue;
518
+ const mined = await this.provider().getBlock(flow.at);
519
+ if (!mined) {
520
+ complete = false;
521
+ continue;
522
+ }
523
+ flows.push({
524
+ id: `poly_flow_${fingerprint(hash).slice(0, 32)}`,
525
+ amount_usdc: D(flow.amount.toString()).div(1e6).toString(),
526
+ at: mined.timestamp * 1000,
527
+ });
528
+ }
529
+ }
530
+ return { fills, flows, orders, complete };
531
+ }
532
+ async release(user, p) {
533
+ await this.core("prediction_release", {
534
+ user_id: user,
535
+ portfolio_id: p.id,
536
+ revision: p.revision,
537
+ });
538
+ }
539
+ async fence(user, p) {
540
+ await this.core(p.loss_latched ? "prediction_fence" : "prediction_activate", { user_id: user, portfolio_id: p.id, revision: p.revision });
541
+ }
542
+ async submitPayload(user, p, e) {
543
+ const snap = await this.account(user, p);
544
+ const current = D(snap.positions[e.key] ?? 0), target = D(e.target_quantity), delta = target.minus(current);
545
+ if (delta.isZero())
546
+ throw new AgentError("EXECUTION_REJECTED", "Prediction target already matches owned inventory.");
547
+ if (delta.gt(0) && (!snap.coverage_complete || p.loss_latched))
548
+ throw new AgentError("EXECUTION_REJECTED", "Unsettled accounting blocks new prediction risk.");
549
+ const ref = await this.outcome(e.key), book = await this.book(ref.token_id);
550
+ const price = delta.gt(0)
551
+ ? D.min(...book.asks.map((x) => D(x.price)))
552
+ : D.max(...book.bids.map((x) => D(x.price)));
553
+ let anchor = price;
554
+ if (delta.gt(0)) {
555
+ for (const s of p.sleeves.filter((s) => s.intents[e.key])) {
556
+ const intent = s.intents[e.key];
557
+ if (!fresh(intent.observed_at) ||
558
+ price.minus(intent.price).abs().div(intent.price).gt(".002"))
559
+ throw new AgentError("EXECUTION_REJECTED", "Prediction entry exceeds the frozen 0.2% price boundary.");
560
+ anchor = D.min(anchor, D(intent.price));
561
+ const source = await this.source(s.trader.id);
562
+ if (source.revision !== s.source_revision)
563
+ throw new AgentError("EXECUTION_REJECTED", "Prediction source changed before execution.");
564
+ }
565
+ }
566
+ if (delta.abs().lt(book.min_order_size))
567
+ throw new AgentError("EXECUTION_REJECTED", "Prediction target is below the venue minimum share size.");
568
+ return {
569
+ user_id: user,
570
+ portfolio_id: p.id,
571
+ task_id: e.id.slice("execution_".length),
572
+ revision: p.revision,
573
+ token_id: ref.token_id,
574
+ condition_id: ref.condition_id,
575
+ side: delta.gt(0) ? "BUY" : "SELL",
576
+ size: delta.abs().toString(),
577
+ price: anchor.toString(),
578
+ slippage_bps: delta.gt(0) ? 20 : 1000,
579
+ };
580
+ }
581
+ async execution(user, p, e, task) {
582
+ const r = await this.core("prediction_execution", {
583
+ user_id: user,
584
+ portfolio_id: p.id,
585
+ task_id: task,
586
+ });
587
+ const snap = await this.account(user, p);
588
+ let settled;
589
+ try {
590
+ settled = JSON.parse(r.response ?? "{}").agent_settlement;
591
+ }
592
+ catch { }
593
+ const done = settled?.complete && snap.coverage_complete;
594
+ return {
595
+ task_id: task,
596
+ status: done
597
+ ? "completed"
598
+ : r.status === 6
599
+ ? "failed"
600
+ : D(r.filled_size).gt(0)
601
+ ? "partially_filled"
602
+ : "submitted",
603
+ observed_quantity: snap.positions[e.key] ?? "0",
604
+ settled: !!done,
605
+ };
606
+ }
607
+ }