@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,1155 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import express from "express";
4
+ import { VERSION } from "../version.js";
5
+ import { firebaseAccount } from "./firebase.js";
6
+ import pg from "pg";
7
+ import { createHash, createSign, timingSafeEqual, } from "node:crypto";
8
+ import { DepositReads } from "./deposits.js";
9
+ import { PredictionVenue } from "./prediction.js";
10
+ import { AgentError, failure } from "./contracts.js";
11
+ import { perpsAccountMode, unifiedUsdc } from "./perps_account.js";
12
+ import { checkPerpsOrder, HYPERLIQUID_MIN_OPEN_NOTIONAL_USDC, } from "./order_rules.js";
13
+ import { D, cash, fingerprint, now, fresh, PERPS_COPY_MAX_LEVERAGE, } from "./domain.js";
14
+ const required = (key) => {
15
+ const v = process.env[key];
16
+ if (!v)
17
+ throw new Error(`${key} is required`);
18
+ return v;
19
+ };
20
+ export class CoreBridge {
21
+ coreUrl;
22
+ coreToken;
23
+ salt;
24
+ network;
25
+ pool;
26
+ prediction;
27
+ taxonomyCache = null;
28
+ candidatesCache = null;
29
+ tokens = null;
30
+ constructor(coreUrl, coreToken, database, salt, network) {
31
+ this.coreUrl = coreUrl;
32
+ this.coreToken = coreToken;
33
+ this.salt = salt;
34
+ this.network = network;
35
+ this.pool = new pg.Pool({ connectionString: database, max: 10 });
36
+ this.prediction = new PredictionVenue(this.pool, (op, input) => this.core(op, input), salt, process.env.RIDE_AGENT_POLYGON_RPC_URL);
37
+ if (salt.length < 32)
38
+ throw Error("Trader identity salt must contain 32 characters");
39
+ }
40
+ async migrate() {
41
+ await this.prediction.migrate();
42
+ await this.pool.query(`CREATE TABLE IF NOT EXISTS ride_agent_core_outbox (execution_id TEXT PRIMARY KEY,user_id TEXT NOT NULL,portfolio_id TEXT NOT NULL,fingerprint TEXT NOT NULL,payload JSONB NOT NULL,response JSONB,created_at TIMESTAMPTZ NOT NULL DEFAULT now());`);
43
+ }
44
+ async close() {
45
+ await this.pool.end();
46
+ }
47
+ async control(operation, payload) {
48
+ const id = `control:${payload.request_id}`;
49
+ await this.pool.query("INSERT INTO ride_agent_core_outbox(execution_id,user_id,portfolio_id,fingerprint,payload) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING", [
50
+ id,
51
+ payload.user_id,
52
+ payload.account_id,
53
+ operation,
54
+ { operation, input: payload },
55
+ ]);
56
+ const saved = await this.pool.query("SELECT user_id,portfolio_id,fingerprint,payload,response FROM ride_agent_core_outbox WHERE execution_id=$1", [id]);
57
+ const row = saved.rows[0];
58
+ if (row.user_id !== payload.user_id ||
59
+ row.portfolio_id !== payload.account_id ||
60
+ row.fingerprint !== operation)
61
+ throw new AgentError("IDEMPOTENCY_MISMATCH", "Control identifier belongs to a different account.");
62
+ if (row.response)
63
+ return row.response;
64
+ const response = await this.core(row.payload.operation, row.payload.input);
65
+ await this.pool.query("UPDATE ride_agent_core_outbox SET response=$2 WHERE execution_id=$1", [id, response]);
66
+ return response;
67
+ }
68
+ async core(operation, input) {
69
+ const r = await fetch(new URL(`/internal/agent-core/${operation}`, this.coreUrl), {
70
+ method: "POST",
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ Authorization: `Bearer ${this.coreToken}`,
74
+ },
75
+ body: JSON.stringify(input),
76
+ signal: AbortSignal.timeout(operation.startsWith("perps_funding_") ? 90000 : 20000),
77
+ redirect: "error",
78
+ });
79
+ const body = (await r.json());
80
+ if (!r.ok)
81
+ throw new AgentError(body.error?.code ?? "CORE_UNAVAILABLE", body.error?.message ?? "Core operation failed", r.status >= 500, r.status);
82
+ return body;
83
+ }
84
+ async info(payload, source = false) {
85
+ const url = source || this.network === "mainnet"
86
+ ? "https://api.hyperliquid.xyz/info"
87
+ : "https://api.hyperliquid-testnet.xyz/info";
88
+ const response = await fetch(url, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify(payload),
92
+ signal: AbortSignal.timeout(12000),
93
+ redirect: "error",
94
+ });
95
+ if (!response.ok)
96
+ throw new AgentError("VENUE_UNAVAILABLE", "Hyperliquid read failed.", true, 503);
97
+ return response.json();
98
+ }
99
+ traderId(address) {
100
+ return `trader_${createHash("sha256").update(`${this.salt}:perps:${address.toLowerCase()}`).digest("hex").slice(0, 32)}`;
101
+ }
102
+ async rows() {
103
+ if (this.candidatesCache && now() - this.candidatesCache.at < 30_000)
104
+ return this.candidatesCache.rows;
105
+ const rows = await this.core("perps_pool", {});
106
+ if (!Array.isArray(rows))
107
+ throw Error("Invalid trader pool");
108
+ this.candidatesCache = { at: now(), rows };
109
+ return rows;
110
+ }
111
+ async resolve(id) {
112
+ const row = (await this.rows()).find((r) => this.traderId(r.address) === id);
113
+ if (!row)
114
+ throw new AgentError("NOT_FOUND", "Fresh trader identity is unavailable.", false, 404);
115
+ return row;
116
+ }
117
+ async taxonomy() {
118
+ if (this.taxonomyCache && fresh(this.taxonomyCache.at, 3600_000))
119
+ return this.taxonomyCache.values;
120
+ const rows = await this.info({ type: "perpCategories" }, true);
121
+ if (!Array.isArray(rows) ||
122
+ !rows.length ||
123
+ rows.some((r) => !Array.isArray(r) ||
124
+ r.length !== 2 ||
125
+ typeof r[0] !== "string" ||
126
+ typeof r[1] !== "string"))
127
+ throw new AgentError("TAXONOMY_UNAVAILABLE", "Official asset classification is unavailable.", true, 503);
128
+ const values = new Map(rows.map((r) => [r[0], r[1].toLowerCase()]));
129
+ this.taxonomyCache = { at: now(), values };
130
+ return values;
131
+ }
132
+ async standardMode(address, source = false) {
133
+ const [mode, abstraction] = await Promise.all([
134
+ this.info({ type: "userAbstraction", user: address }, source),
135
+ this.info({ type: "userDexAbstraction", user: address }, source),
136
+ ]);
137
+ if (!["standard", "disabled"].includes(mode) ||
138
+ ![false, null].includes(abstraction))
139
+ throw new AgentError("ACCOUNT_MODE_UNSUPPORTED", "This dedicated RWA flow requires Standard mode; unified or portfolio-margin balances cannot be counted as separate DEX collateral.");
140
+ }
141
+ async source(id) {
142
+ if (process.env.RIDE_AGENT_PREDICTION_ENABLED === "true" &&
143
+ (await this.prediction.recognizes(id)))
144
+ return this.prediction.source(id);
145
+ const row = await this.resolve(id), state = await this.info({ type: "clearinghouseState", user: row.address }, true);
146
+ if (process.env.RIDE_AGENT_RWA_ENABLED === "true" &&
147
+ row.tags?.includes("category:stocks_rwa")) {
148
+ await this.standardMode(row.address, true);
149
+ const taxonomy = await this.taxonomy(), scopes = [
150
+ ...new Set([...taxonomy]
151
+ .filter(([_, c]) => ["stocks", "commodities", "indices", "fx", "preipo"].includes(c))
152
+ .map(([a]) => a.split(":")[0])),
153
+ ].filter((s) => s && [...taxonomy.keys()].some((a) => a.startsWith(s + ":")));
154
+ for (let offset = 0; offset < scopes.length; offset += 3) {
155
+ const observations = await Promise.all(scopes.slice(offset, offset + 3).map(async (dex) => ({
156
+ dex,
157
+ state: await this.info({ type: "clearinghouseState", user: row.address, dex }, true),
158
+ meta: await this.info({ type: "meta", dex }, true),
159
+ })));
160
+ for (const observation of observations) {
161
+ if (observation.meta.collateralToken !== 0) {
162
+ if (observation.state.assetPositions?.some((x) => !D(x.position?.szi ?? 0).isZero()))
163
+ throw new AgentError("UNSUPPORTED_COLLATERAL", "Source RWA positions use non-USDC collateral.");
164
+ continue;
165
+ }
166
+ if (!Array.isArray(observation.state.assetPositions) ||
167
+ observation.state.marginSummary?.accountValue == null)
168
+ throw new AgentError("SOURCE_STALE", "Source RWA scope is incomplete.", true);
169
+ state.marginSummary.accountValue = D(state.marginSummary.accountValue)
170
+ .plus(observation.state.marginSummary.accountValue)
171
+ .toString();
172
+ state.assetPositions.push(...observation.state.assetPositions.map((item) => ({
173
+ ...item,
174
+ position: {
175
+ ...item.position,
176
+ coin: item.position.coin.includes(":")
177
+ ? item.position.coin
178
+ : observation.dex + ":" + item.position.coin,
179
+ },
180
+ })));
181
+ }
182
+ }
183
+ }
184
+ if (!state.marginSummary?.accountValue ||
185
+ !Array.isArray(state.assetPositions))
186
+ throw new AgentError("SOURCE_STALE", "Trader account state is incomplete.", true);
187
+ const at = now();
188
+ const positions = state.assetPositions
189
+ .map((item) => item.position)
190
+ .filter((x) => x &&
191
+ x.coin &&
192
+ (process.env.RIDE_AGENT_RWA_ENABLED === "true" ||
193
+ !x.coin.includes(":")) &&
194
+ !D(x.szi).isZero())
195
+ .map((x) => ({
196
+ key: `perps:${x.coin}`,
197
+ asset: x.coin,
198
+ market: "perps",
199
+ quantity: String(x.szi),
200
+ price: D(x.positionValue).abs().div(D(x.szi).abs()).toString(),
201
+ source_entry_price: x.entryPx ?? null,
202
+ source_leverage: Number(x.leverage?.value ?? 1),
203
+ source_leverage_verified: x.leverage?.value != null &&
204
+ Number.isFinite(Number(x.leverage.value)) &&
205
+ Number(x.leverage.value) >= 1,
206
+ observed_at: at,
207
+ source_revision: "",
208
+ }));
209
+ const revision = fingerprint(positions.map((p) => [
210
+ p.key,
211
+ p.quantity,
212
+ p.source_leverage,
213
+ p.source_entry_price,
214
+ ]));
215
+ for (const p of positions)
216
+ p.source_revision = revision;
217
+ return {
218
+ observed_at: at,
219
+ revision,
220
+ account_value_usdc: String(state.marginSummary.accountValue),
221
+ positions,
222
+ complete: process.env.RIDE_AGENT_RWA_ENABLED === "true" ||
223
+ !state.assetPositions.some((x) => x.position?.coin?.includes(":")),
224
+ };
225
+ }
226
+ async profile(id) {
227
+ if (process.env.RIDE_AGENT_PREDICTION_ENABLED === "true" &&
228
+ (await this.prediction.recognizes(id)))
229
+ return this.prediction.profile(id);
230
+ const row = await this.resolve(id), source = await this.source(id);
231
+ let raw = {};
232
+ try {
233
+ raw = JSON.parse(row.raw ?? "{}");
234
+ }
235
+ catch { }
236
+ const components = {};
237
+ for (const name of [
238
+ "stability",
239
+ "pathSmoothness",
240
+ "drawdownRecovery",
241
+ "profitQuality",
242
+ "profitConcentration",
243
+ "positionLeverageStability",
244
+ "copyability",
245
+ "trackRecord",
246
+ ]) {
247
+ const v = raw.copyScoreV2_5?.components?.[name];
248
+ components[name] =
249
+ typeof v === "number"
250
+ ? v
251
+ : typeof v?.score === "number"
252
+ ? v.score
253
+ : null;
254
+ }
255
+ const assets = [...new Set(source.positions.map((x) => x.asset))];
256
+ const taxonomy = assets.some((a) => a.includes(":"))
257
+ ? await this.taxonomy()
258
+ : new Map();
259
+ const classifications = assets.map((a) => taxonomy.get(a) ?? (a.includes(":") ? "unknown" : "crypto"));
260
+ const assetCategory = row.tags?.includes("category:stocks_rwa")
261
+ ? "rwa"
262
+ : classifications.length && classifications.every((c) => c === "crypto")
263
+ ? "crypto"
264
+ : classifications.length &&
265
+ classifications.every((c) => ["stocks", "commodities", "indices", "fx", "preipo"].includes(c))
266
+ ? "rwa"
267
+ : classifications.includes("unknown")
268
+ ? "unknown"
269
+ : "mixed";
270
+ const directions = new Set(source.positions.map((x) => D(x.quantity).isPositive() ? "long" : "short"));
271
+ const handle = typeof row.handle === "string" && !/0x[a-fA-F0-9]{40}/.test(row.handle)
272
+ ? row.handle
273
+ : `Ride trader ${id.slice(-6)}`;
274
+ let atr = null;
275
+ try {
276
+ const returns = [];
277
+ for (const asset of assets.slice(0, 3)) {
278
+ const end = Math.floor(now() / 86400_000) * 86400_000;
279
+ const candles = await this.info({
280
+ type: "candleSnapshot",
281
+ req: {
282
+ coin: asset,
283
+ interval: "1d",
284
+ startTime: end - 16 * 86400_000,
285
+ endTime: end - 1,
286
+ },
287
+ }, true);
288
+ if (candles.length < 15)
289
+ continue;
290
+ const sorted = candles.sort((a, b) => a.t - b.t);
291
+ const tr = sorted.slice(-14).map((c, i) => {
292
+ const previous = sorted[sorted.length - 15 + i];
293
+ return D.max(D(c.h).minus(c.l), D(c.h).minus(previous.c).abs(), D(c.l).minus(previous.c).abs());
294
+ });
295
+ const last = sorted.at(-1);
296
+ returns.push(tr
297
+ .reduce((n, v) => n.plus(v), D(0))
298
+ .div(14)
299
+ .div(last.c)
300
+ .times(100)
301
+ .toNumber());
302
+ }
303
+ if (returns.length)
304
+ atr = Math.max(...returns);
305
+ }
306
+ catch {
307
+ /* Missing ATR stays unavailable; new Ride recommendations follow source leverage. */
308
+ }
309
+ return {
310
+ id,
311
+ handle,
312
+ market: "perps",
313
+ asset_category: assetCategory,
314
+ asset_categories: Object.fromEntries(assets.map((a, i) => [
315
+ a,
316
+ classifications[i] === "crypto"
317
+ ? "crypto"
318
+ : ["stocks", "commodities", "indices", "fx", "preipo"].includes(classifications[i])
319
+ ? "rwa"
320
+ : "unknown",
321
+ ])),
322
+ execution_supported: source.complete &&
323
+ (assetCategory !== "rwa" ||
324
+ process.env.RIDE_AGENT_RWA_ENABLED === "true"),
325
+ score: Number(row.score),
326
+ score_version: row.score_version,
327
+ components,
328
+ scored_at: row.computed_at,
329
+ expires_at: row.expires_at,
330
+ eligibility: row.eligibility === "PASS" &&
331
+ row.score_version === "strict_perp_v2_5_daily_live_v1" &&
332
+ Object.values(components).every((v) => v !== null && Number.isFinite(v))
333
+ ? "PASS"
334
+ : "REJECT",
335
+ assets: assets.length ? assets : ["BTC", "ETH"],
336
+ style: row.tags?.[0] ?? "unclassified",
337
+ direction: directions.size === 1
338
+ ? [...directions][0]
339
+ : "mixed",
340
+ median_hold_hours: row.median_hold_seconds == null ? null : row.median_hold_seconds / 3600,
341
+ source_roi_pct: row.roi_ratio == null ? null : D(row.roi_ratio).times(100).toNumber(),
342
+ source_drawdown_pct: row.drawdown_ratio == null
343
+ ? null
344
+ : D(row.drawdown_ratio).abs().times(100).toNumber(),
345
+ atr_pct: atr,
346
+ // Planning envelope matches the App setting. The execution snapshot
347
+ // separately verifies the current per-asset exchange limit.
348
+ venue_max_leverage: PERPS_COPY_MAX_LEVERAGE,
349
+ min_notional_usdc: HYPERLIQUID_MIN_OPEN_NOTIONAL_USDC,
350
+ last_trade_at: (() => {
351
+ const at = raw.tradeMetrics?.status === "available"
352
+ ? raw.tradeMetrics.newestFillTime
353
+ : raw.dailyRescore?.lastTradeAt;
354
+ return Number.isSafeInteger(at) && at > 0 && at <= now() + 1000
355
+ ? at
356
+ : null;
357
+ })(),
358
+ };
359
+ }
360
+ async candidates(market = "both") {
361
+ if (market === "prediction")
362
+ return process.env.RIDE_AGENT_PREDICTION_ENABLED === "true"
363
+ ? this.prediction.candidates()
364
+ : [];
365
+ const sorted = [...(await this.rows())]
366
+ .filter((r) => r.eligibility === "PASS" && r.expires_at > now())
367
+ .sort((a, b) => Number(b.score) - Number(a.score))
368
+ .slice(0, 30);
369
+ const out = [];
370
+ for (let offset = 0; offset < sorted.length; offset += 3) {
371
+ const batch = await Promise.allSettled(sorted
372
+ .slice(offset, offset + 3)
373
+ .map((r) => this.profile(this.traderId(r.address))));
374
+ for (const r of batch)
375
+ if (r.status === "fulfilled")
376
+ out.push(r.value);
377
+ }
378
+ if (market !== "perps" &&
379
+ process.env.RIDE_AGENT_PREDICTION_ENABLED === "true")
380
+ out.push(...(await this.prediction.candidates()));
381
+ return out;
382
+ }
383
+ async paged(type, address, start, end = now(), source = false) {
384
+ let cursor = start;
385
+ const result = new Map();
386
+ for (let page = 0; page < 10; page++) {
387
+ const rows = await this.info({
388
+ type,
389
+ user: address,
390
+ startTime: cursor,
391
+ endTime: end,
392
+ aggregateByTime: false,
393
+ }, source);
394
+ if (!Array.isArray(rows))
395
+ return { events: [...result.values()], complete: false };
396
+ for (const row of rows)
397
+ result.set(`${row.tid ?? row.hash ?? fingerprint(row)}:${row.time}`, row);
398
+ if (rows.length < 500)
399
+ return { events: [...result.values()], complete: true };
400
+ const next = Math.max(...rows.map((r) => r.time));
401
+ if (next <= cursor)
402
+ return { events: [...result.values()], complete: false };
403
+ cursor = next;
404
+ }
405
+ return { events: [...result.values()], complete: false };
406
+ }
407
+ async perpsAccount(user, p, bind = false) {
408
+ let identity, accountId;
409
+ const binding = p?.venue_accounts?.perps ??
410
+ (p?.sleeves.every((s) => s.trader.market === "perps")
411
+ ? p?.account_id
412
+ : null);
413
+ if (binding) {
414
+ identity = await this.core("perps_account", {
415
+ user_id: user,
416
+ account_id: binding,
417
+ });
418
+ accountId = binding;
419
+ }
420
+ else {
421
+ identity = await this.core("perps_wallet", { user_id: user });
422
+ accountId = `account_${fingerprint([user, this.network]).slice(0, 32)}`;
423
+ }
424
+ const [state, orders, mids, meta, abstraction] = await Promise.all([
425
+ this.info({ type: "clearinghouseState", user: identity.master }),
426
+ this.info({ type: "frontendOpenOrders", user: identity.master }),
427
+ this.info({ type: "allMids" }),
428
+ this.info({ type: "meta" }),
429
+ this.info({ type: "userAbstraction", user: identity.master }),
430
+ ]);
431
+ if (!state.marginSummary?.accountValue ||
432
+ !Array.isArray(state.assetPositions) ||
433
+ !Array.isArray(orders) ||
434
+ !Array.isArray(meta.universe))
435
+ throw new AgentError("ACCOUNT_STALE", "Account observations are incomplete.", true);
436
+ const mode = perpsAccountMode(abstraction);
437
+ if (mode === "unsupported" ||
438
+ (mode === "unified" && process.env.RIDE_AGENT_UNIFIED_ENABLED !== "true"))
439
+ throw new AgentError("ACCOUNT_NOT_READY", "This account mode is not supported by the independent Agent execution service.");
440
+ let accountValue = String(state.marginSummary.accountValue);
441
+ let available = String(state.withdrawable);
442
+ if (mode === "unified") {
443
+ const [spot, dexAbstraction, dexes] = await Promise.all([
444
+ this.info({ type: "spotClearinghouseState", user: identity.master }),
445
+ this.info({ type: "userDexAbstraction", user: identity.master }),
446
+ this.info({ type: "perpDexs" }),
447
+ ]);
448
+ if (![false, null].includes(dexAbstraction))
449
+ throw new AgentError("ACCOUNT_NOT_READY", "Agent execution requires disabled DEX abstraction.");
450
+ const balance = unifiedUsdc(spot);
451
+ accountValue = balance.equity;
452
+ available = balance.available;
453
+ // Core-only unified support cannot claim a shared balance while another
454
+ // DEX has exposure or pending orders. The worker repeats these checks.
455
+ if (!Array.isArray(dexes) ||
456
+ dexes[0] !== null ||
457
+ dexes
458
+ .slice(1)
459
+ .some((dex) => typeof dex?.name !== "string" || !dex.name.trim()))
460
+ throw new AgentError("ACCOUNT_STALE", "Complete Perps market scopes are unavailable.", true);
461
+ await Promise.all(dexes.slice(1).map(async (dex) => {
462
+ const [other, pending] = await Promise.all([
463
+ this.info({
464
+ type: "clearinghouseState",
465
+ user: identity.master,
466
+ dex: dex.name,
467
+ }),
468
+ this.info({
469
+ type: "frontendOpenOrders",
470
+ user: identity.master,
471
+ dex: dex.name,
472
+ }),
473
+ ]);
474
+ if (!Array.isArray(other?.assetPositions) || !Array.isArray(pending))
475
+ throw new AgentError("ACCOUNT_STALE", "Complete dedicated account observations are unavailable.", true);
476
+ if (pending.length ||
477
+ other.assetPositions.some((row) => !D(row.position?.szi).isZero()))
478
+ throw new AgentError("ACCOUNT_NOT_READY", "Unified Agent execution requires no exposure or open orders on other Perps DEXs.");
479
+ }));
480
+ if (perpsAccountMode(await this.info({ type: "userAbstraction", user: identity.master })) !== mode)
481
+ throw new AgentError("ACCOUNT_STALE", "Account mode changed during the balance observation.", true);
482
+ }
483
+ const scopedAvailable = { core: available };
484
+ const scopes = [
485
+ ...new Set((p?.sleeves ?? [])
486
+ .flatMap((s) => s.trader.assets)
487
+ .filter((a) => a.includes(":"))
488
+ .map((a) => a.split(":")[0])),
489
+ ];
490
+ if (scopes.length) {
491
+ if (process.env.RIDE_AGENT_RWA_ENABLED !== "true")
492
+ throw new AgentError("RWA_EXECUTION_UNAVAILABLE", "RWA execution is not enabled on this independent Agent service.");
493
+ await this.standardMode(identity.master);
494
+ for (const dex of scopes) {
495
+ const [extra, extraOrders, extraMids, extraMeta] = await Promise.all([
496
+ this.info({ type: "clearinghouseState", user: identity.master, dex }),
497
+ this.info({ type: "frontendOpenOrders", user: identity.master, dex }),
498
+ this.info({ type: "allMids", dex }),
499
+ this.info({ type: "meta", dex }),
500
+ ]);
501
+ if (extraMeta.collateralToken !== 0)
502
+ throw new AgentError("UNSUPPORTED_COLLATERAL", "Only verified USDC-collateral RWA DEXes can join this plan.");
503
+ if (!Array.isArray(extra.assetPositions) ||
504
+ !Array.isArray(extraOrders) ||
505
+ !Array.isArray(extraMeta.universe) ||
506
+ extra.marginSummary?.accountValue == null ||
507
+ extra.withdrawable == null)
508
+ throw new AgentError("ACCOUNT_STALE", "RWA account scope is incomplete.", true);
509
+ scopedAvailable[dex] = String(extra.withdrawable);
510
+ state.marginSummary.accountValue = D(state.marginSummary.accountValue)
511
+ .plus(extra.marginSummary.accountValue)
512
+ .toString();
513
+ state.withdrawable = D(state.withdrawable)
514
+ .plus(extra.withdrawable)
515
+ .toString();
516
+ state.assetPositions.push(...extra.assetPositions.map((item) => ({
517
+ ...item,
518
+ position: {
519
+ ...item.position,
520
+ coin: item.position.coin.includes(":")
521
+ ? item.position.coin
522
+ : dex + ":" + item.position.coin,
523
+ },
524
+ })));
525
+ orders.push(...extraOrders.map((order) => ({
526
+ ...order,
527
+ coin: order.coin.includes(":")
528
+ ? order.coin
529
+ : dex + ":" + order.coin,
530
+ })));
531
+ Object.assign(mids, Object.fromEntries(Object.entries(extraMids).map(([coin, value]) => [
532
+ coin.includes(":") ? coin : dex + ":" + coin,
533
+ value,
534
+ ])));
535
+ meta.universe.push(...extraMeta.universe.map((x) => ({
536
+ ...x,
537
+ name: x.name.includes(":") ? x.name : dex + ":" + x.name,
538
+ })));
539
+ }
540
+ }
541
+ if (bind) {
542
+ if (state.assetPositions.some((x) => !D(x.position?.szi ?? 0).isZero()) ||
543
+ orders.length)
544
+ throw new AgentError("OWNERSHIP_CONFLICT", "Existing positions or orders must remain with their current owner.");
545
+ const b = await this.core("perps_bind", {
546
+ user_id: user,
547
+ portfolio_id: p.id,
548
+ });
549
+ accountId = b.account_id;
550
+ p.venue_accounts = { ...p.venue_accounts, perps: accountId };
551
+ identity = await this.core("perps_account", {
552
+ user_id: user,
553
+ account_id: accountId,
554
+ });
555
+ }
556
+ const positions = Object.fromEntries(state.assetPositions.map((x) => [
557
+ `perps:${x.position.coin}`,
558
+ String(x.position.szi),
559
+ ])), prices = Object.fromEntries(Object.entries(mids).map(([coin, price]) => [
560
+ `perps:${coin}`,
561
+ String(price),
562
+ ]));
563
+ const maxLeverages = Object.fromEntries(meta.universe.map((x) => [`perps:${x.name}`, x.maxLeverage]));
564
+ let fills = [], funding = [], flows = [], coverage = true;
565
+ if (p?.ledger_started_at) {
566
+ const [fillRows, fundingRows, flowRows] = await Promise.all([
567
+ this.paged("userFillsByTime", identity.master, p.ledger_started_at),
568
+ this.paged("userFunding", identity.master, p.ledger_started_at),
569
+ this.paged("userNonFundingLedgerUpdates", identity.master, p.ledger_started_at),
570
+ ]);
571
+ coverage = fillRows.complete && fundingRows.complete && flowRows.complete;
572
+ const oidTasks = new Map();
573
+ for (const e of p.executions.filter((e) => e.task_id && e.key.startsWith("perps:"))) {
574
+ const status = await this.core("perps_execution", {
575
+ user_id: user,
576
+ account_id: accountId,
577
+ task_id: e.task_id,
578
+ });
579
+ for (const leg of status.legs ?? [])
580
+ if (leg.oid)
581
+ oidTasks.set(String(leg.oid), e.task_id);
582
+ }
583
+ for (const f of fillRows.events) {
584
+ const task = oidTasks.get(String(f.oid));
585
+ if (!task || f.feeToken !== "USDC") {
586
+ coverage = false;
587
+ continue;
588
+ }
589
+ fills.push({
590
+ id: `hl_${f.tid}_${f.oid}`,
591
+ order_id: String(f.oid),
592
+ task_id: task,
593
+ key: `perps:${f.coin}`,
594
+ quantity: D(f.sz)
595
+ .times(f.side === "B" ? 1 : -1)
596
+ .toString(),
597
+ price: String(f.px),
598
+ fee_usdc: String(f.fee),
599
+ at: f.time,
600
+ });
601
+ }
602
+ funding = fundingRows.events.map((f) => ({
603
+ id: `fund_${fingerprint(f).slice(0, 32)}`,
604
+ key: `perps:${f.delta.coin}`,
605
+ amount_usdc: String(f.delta.usdc),
606
+ at: f.time,
607
+ }));
608
+ for (const f of flowRows.events) {
609
+ const d = f.delta;
610
+ let amount = null;
611
+ if (d?.type === "deposit")
612
+ amount = String(d.usdc);
613
+ else if (d?.type === "withdraw")
614
+ amount = D(d.usdc)
615
+ .plus(d.fee ?? 0)
616
+ .neg()
617
+ .toString();
618
+ else if ([
619
+ "accountClassTransfer",
620
+ "internalTransfer",
621
+ "subAccountTransfer",
622
+ ].includes(d?.type)) {
623
+ if (d.usdc !== undefined && typeof d.toPerp === "boolean")
624
+ amount = D(d.usdc)
625
+ .times(d.toPerp ? 1 : -1)
626
+ .toString();
627
+ else
628
+ coverage = false;
629
+ }
630
+ else
631
+ coverage = false;
632
+ if (amount !== null)
633
+ flows.push({
634
+ id: `flow_${fingerprint(f).slice(0, 32)}`,
635
+ amount_usdc: amount,
636
+ at: f.time,
637
+ });
638
+ }
639
+ }
640
+ if (mode === "standard") {
641
+ accountValue = String(state.marginSummary.accountValue);
642
+ available = String(state.withdrawable);
643
+ }
644
+ const snap = {
645
+ account_id: accountId,
646
+ venue_accounts: { perps: accountId },
647
+ available_by_market: { perps: available },
648
+ available_by_scope: scopedAvailable,
649
+ observed_at: now(),
650
+ account_value_usdc: accountValue,
651
+ available_usdc: available,
652
+ dedicated: binding != null ||
653
+ bind ||
654
+ (!orders.length &&
655
+ !Object.values(positions).some((q) => !D(q).isZero())),
656
+ eligible: identity.status === 4 || identity.status === "active",
657
+ positions,
658
+ prices,
659
+ max_leverages: maxLeverages,
660
+ quantity_decimals: Object.fromEntries(meta.universe.map((x) => [`perps:${x.name}`, x.szDecimals])),
661
+ min_notionals: Object.fromEntries(Object.keys(prices).map((key) => [
662
+ key,
663
+ HYPERLIQUID_MIN_OPEN_NOTIONAL_USDC,
664
+ ])),
665
+ orders: orders.map((x) => ({
666
+ id: String(x.oid),
667
+ key: `perps:${x.coin}`,
668
+ increases_risk: !x.reduceOnly,
669
+ })),
670
+ fills,
671
+ funding,
672
+ flows,
673
+ coverage_complete: coverage,
674
+ };
675
+ if (bind) {
676
+ await this.configure(user, p, snap);
677
+ return { ...snap, account_id: accountId };
678
+ }
679
+ return snap;
680
+ }
681
+ async account(user, p, bind = false) {
682
+ if (!p)
683
+ return this.perpsAccount(user, p, bind);
684
+ const venues = new Set(p.sleeves.map((s) => s.trader.market));
685
+ const snapshots = [];
686
+ if (venues.has("perps"))
687
+ snapshots.push(await this.perpsAccount(user, p, bind));
688
+ if (venues.has("prediction"))
689
+ snapshots.push(await this.prediction.account(user, p, bind));
690
+ if (snapshots.length === 1)
691
+ return snapshots[0];
692
+ const merge = (field) => Object.assign({}, ...snapshots.map((s) => s[field] ?? {}));
693
+ return {
694
+ account_id: `account_${fingerprint([user, p.id, "both"]).slice(0, 32)}`,
695
+ venue_accounts: merge("venue_accounts"),
696
+ chain_block: snapshots.find((s) => s.chain_block != null)?.chain_block,
697
+ observed_at: Math.min(...snapshots.map((s) => s.observed_at)),
698
+ account_value_usdc: cash(snapshots.reduce((n, s) => n.plus(s.account_value_usdc), D(0))),
699
+ available_usdc: cash(snapshots.reduce((n, s) => n.plus(s.available_usdc), D(0))),
700
+ available_by_market: merge("available_by_market"),
701
+ dedicated: snapshots.every((s) => s.dedicated),
702
+ eligible: snapshots.every((s) => s.eligible),
703
+ positions: merge("positions"),
704
+ prices: merge("prices"),
705
+ max_leverages: merge("max_leverages"),
706
+ quantity_decimals: merge("quantity_decimals"),
707
+ min_notionals: merge("min_notionals"),
708
+ fills: snapshots.flatMap((s) => s.fills),
709
+ funding: snapshots.flatMap((s) => s.funding),
710
+ flows: snapshots.flatMap((s) => s.flows),
711
+ orders: snapshots.flatMap((s) => s.orders),
712
+ coverage_complete: snapshots.every((s) => s.coverage_complete),
713
+ };
714
+ }
715
+ perpsBinding(p) {
716
+ return p.venue_accounts?.perps ?? p.account_id;
717
+ }
718
+ async configure(user, p, snap) {
719
+ const base = { user_id: user, account_id: this.perpsBinding(p) };
720
+ const perps = p.sleeves.filter((s) => s.trader.market === "perps" && s.state !== "closed");
721
+ if (!perps.length)
722
+ return;
723
+ const policyLeverage = Math.max(...perps.map((s) => {
724
+ if (s.leverage_cap != null)
725
+ return s.leverage_cap;
726
+ const limits = s.trader.assets.map((a) => snap.max_leverages[`perps:${a}`]);
727
+ if (!limits.length ||
728
+ limits.some((limit) => !Number.isInteger(limit) || limit < 1))
729
+ throw new AgentError("VENUE_RULES_UNAVAILABLE", "Fresh per-asset leverage limits are required.", true);
730
+ return Math.max(...limits);
731
+ }));
732
+ const exposure = D.min(
733
+ // Preserve Core's existing NUMERIC(8,4) columns and prepared statements.
734
+ "9999.9999", policyLeverage * 100, p.sleeves
735
+ .filter((s) => s.trader.market === "perps" && s.state !== "closed")
736
+ .reduce((n, s) => n.plus(s.amount_usdc), D(0))
737
+ .div(snap.account_value_usdc)
738
+ .times(policyLeverage)
739
+ .times(100));
740
+ const assets = [
741
+ ...new Set(p.sleeves
742
+ .filter((s) => s.trader.market === "perps")
743
+ .flatMap((s) => s.trader.assets)),
744
+ ];
745
+ const policy = {
746
+ ...base,
747
+ assets,
748
+ max_total_position_pct: exposure.toFixed(4),
749
+ max_leverage: policyLeverage,
750
+ request_id: `${p.id}:policy:${p.control_id ?? p.revision}`,
751
+ fingerprint: fingerprint([p.id, p.revision, assets, exposure.toFixed(4)]),
752
+ };
753
+ await this.control("perps_policy", policy);
754
+ const state = {
755
+ ...base,
756
+ state: p.loss_latched ? "reduce_only" : "active",
757
+ request_id: `${p.id}:active:${p.control_id ?? p.revision}`,
758
+ fingerprint: fingerprint([p.id, p.revision, p.loss_latched]),
759
+ };
760
+ await this.control("perps_state", state);
761
+ }
762
+ async fence(user, p) {
763
+ if (!p.account_id)
764
+ return;
765
+ if (p.sleeves.some((s) => s.trader.market === "prediction"))
766
+ await this.prediction.fence(user, p);
767
+ if (!p.sleeves.some((s) => s.trader.market === "perps"))
768
+ return;
769
+ await this.control("perps_state", {
770
+ user_id: user,
771
+ account_id: this.perpsBinding(p),
772
+ state: "paused",
773
+ request_id: `${p.id}:fence:${p.control_id ?? p.revision}`,
774
+ fingerprint: fingerprint([p.id, p.revision, "fence"]),
775
+ });
776
+ await this.configure(user, p, await this.perpsAccount(user, p));
777
+ }
778
+ async release(user, p) {
779
+ const snap = await this.account(user, p);
780
+ if (!fresh(snap.observed_at) ||
781
+ !snap.coverage_complete ||
782
+ snap.orders.length ||
783
+ Object.values(snap.positions).some((q) => !D(q).isZero()))
784
+ throw new AgentError("SETTLEMENT_PENDING", "Owned inventory and pending orders must settle before account release.", true);
785
+ if (p.sleeves.some((s) => s.trader.market === "prediction"))
786
+ await this.prediction.release(user, p);
787
+ if (p.sleeves.some((s) => s.trader.market === "perps"))
788
+ await this.control("perps_state", {
789
+ user_id: user,
790
+ account_id: this.perpsBinding(p),
791
+ state: "disabled",
792
+ request_id: `${p.id}:release`,
793
+ fingerprint: fingerprint([p.id, "release"]),
794
+ });
795
+ }
796
+ async submit(user, p, e) {
797
+ const prediction = e.key.startsWith("prediction:");
798
+ const existing = await this.pool.query("SELECT fingerprint,payload,response FROM ride_agent_core_outbox WHERE execution_id=$1 AND user_id=$2 AND portfolio_id=$3", [e.id, user, p.id]);
799
+ let payload;
800
+ if (existing.rowCount) {
801
+ const row = existing.rows[0];
802
+ if (row.fingerprint !== e.fingerprint)
803
+ throw new AgentError("IDEMPOTENCY_MISMATCH", "Execution parameters changed.");
804
+ if (row.response)
805
+ return row.response;
806
+ payload = row.payload;
807
+ }
808
+ else if (prediction) {
809
+ payload = await this.prediction.submitPayload(user, p, e);
810
+ await this.pool.query("INSERT INTO ride_agent_core_outbox(execution_id,user_id,portfolio_id,fingerprint,payload) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING", [e.id, user, p.id, e.fingerprint, payload]);
811
+ const saved = await this.pool.query("SELECT fingerprint,payload FROM ride_agent_core_outbox WHERE execution_id=$1 AND user_id=$2 AND portfolio_id=$3", [e.id, user, p.id]);
812
+ if (!saved.rowCount || saved.rows[0].fingerprint !== e.fingerprint)
813
+ throw new AgentError("IDEMPOTENCY_MISMATCH", "Execution identifier has other parameters.");
814
+ payload = saved.rows[0].payload;
815
+ }
816
+ else {
817
+ const snap = await this.perpsAccount(user, p);
818
+ if (!fresh(snap.observed_at) ||
819
+ !snap.coverage_complete ||
820
+ !snap.dedicated ||
821
+ !snap.eligible)
822
+ throw new AgentError("EXECUTION_REJECTED", "Account evidence does not authorize this target.");
823
+ const price = snap.prices[e.key];
824
+ if (!price)
825
+ throw new AgentError("EXECUTION_REJECTED", "Price is unavailable.");
826
+ const current = D(snap.positions[e.key] ?? 0), target = D(e.target_quantity);
827
+ if (p.preferences.portfolio_kind) {
828
+ const check = checkPerpsOrder(snap, e.key, e.target_quantity);
829
+ if (!check.ok)
830
+ throw new AgentError("EXECUTION_REJECTED", `${check.code}: ${check.message}`);
831
+ }
832
+ if (p.loss_latched && !target.isZero())
833
+ throw new AgentError("EXECUTION_REJECTED", "Loss trigger blocks non-flat targets.");
834
+ const increases = target.abs().gt(current.abs()) ||
835
+ (!target.isZero() &&
836
+ !current.isZero() &&
837
+ target.isPositive() !== current.isPositive());
838
+ for (const s of p.sleeves.filter((s) => s.intents[e.key])) {
839
+ if (!increases)
840
+ continue;
841
+ const intent = s.intents[e.key];
842
+ if (s.leverage_mode != null &&
843
+ (intent.source_leverage_verified === false ||
844
+ !Number.isInteger(intent.source_leverage) ||
845
+ intent.source_leverage < 1 ||
846
+ !Number.isFinite(snap.max_leverages[e.key]) ||
847
+ intent.source_leverage > snap.max_leverages[e.key]))
848
+ throw new AgentError("EXECUTION_REJECTED", "Verified leverage rules changed; recalculate the target before increasing exposure.");
849
+ if (!fresh(intent.observed_at) ||
850
+ D(price).minus(intent.price).abs().div(intent.price).gt(".002"))
851
+ throw new AgentError("EXECUTION_REJECTED", "Execution price moved beyond the frozen 0.2% limit.");
852
+ const source = await this.source(s.trader.id);
853
+ if (s.state === "active" && source.revision !== s.source_revision)
854
+ throw new AgentError("EXECUTION_REJECTED", "Source position changed before execution.");
855
+ }
856
+ const identity = await this.core("perps_account", {
857
+ user_id: user,
858
+ account_id: this.perpsBinding(p),
859
+ });
860
+ payload = {
861
+ user_id: user,
862
+ account_id: this.perpsBinding(p),
863
+ execution_id: e.id,
864
+ fingerprint: e.fingerprint,
865
+ asset: e.key.slice(6),
866
+ direction: target.isZero()
867
+ ? "flat"
868
+ : target.isPositive()
869
+ ? "long"
870
+ : "short",
871
+ target_quantity: e.target_quantity,
872
+ entry_reference_price: increases ? price : null,
873
+ max_entry_deviation_bps: increases ? 20 : null,
874
+ target_position_pct: target
875
+ .abs()
876
+ .times(price)
877
+ .div(snap.account_value_usdc)
878
+ .times(100)
879
+ .toFixed(4),
880
+ leverage: Math.max(1, Math.min(snap.max_leverages[e.key] ?? 1, ...p.sleeves
881
+ .filter((s) => s.intents[e.key])
882
+ .map((s) => Math.min(s.leverage_cap ?? Number.POSITIVE_INFINITY, s.intents[e.key].source_leverage)))),
883
+ created_at: e.created_at,
884
+ expires_at: e.expires_at,
885
+ policy_revision: identity.policy_revision,
886
+ };
887
+ await this.pool.query("INSERT INTO ride_agent_core_outbox(execution_id,user_id,portfolio_id,fingerprint,payload) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING", [e.id, user, p.id, e.fingerprint, payload]);
888
+ const saved = await this.pool.query("SELECT fingerprint,payload FROM ride_agent_core_outbox WHERE execution_id=$1", [e.id]);
889
+ if (saved.rows[0].fingerprint !== e.fingerprint)
890
+ throw new AgentError("IDEMPOTENCY_MISMATCH", "Execution identifier already belongs to other parameters.");
891
+ payload = saved.rows[0].payload;
892
+ }
893
+ const response = await this.core(prediction ? "prediction_submit" : "perps_submit", payload);
894
+ await this.pool.query("UPDATE ride_agent_core_outbox SET response=$2 WHERE execution_id=$1", [e.id, response]);
895
+ return response;
896
+ }
897
+ async execution(user, p, e) {
898
+ let task = e.task_id;
899
+ if (!task) {
900
+ const saved = await this.pool.query("SELECT response,payload FROM ride_agent_core_outbox WHERE execution_id=$1 AND user_id=$2 AND portfolio_id=$3", [e.id, user, p.id]);
901
+ if (!saved.rowCount)
902
+ return { status: "failed", observed_quantity: null, settled: false };
903
+ if (saved.rows[0].response)
904
+ task = saved.rows[0].response.task_id;
905
+ else {
906
+ const response = await this.core(e.key.startsWith("prediction:")
907
+ ? "prediction_submit"
908
+ : "perps_submit", saved.rows[0].payload);
909
+ await this.pool.query("UPDATE ride_agent_core_outbox SET response=$2 WHERE execution_id=$1", [e.id, response]);
910
+ task = response.task_id;
911
+ }
912
+ }
913
+ if (e.key.startsWith("prediction:"))
914
+ return this.prediction.execution(user, p, e, task);
915
+ const response = await this.core("perps_execution", {
916
+ user_id: user,
917
+ account_id: this.perpsBinding(p),
918
+ task_id: task,
919
+ });
920
+ let snapshot = {};
921
+ try {
922
+ snapshot =
923
+ typeof response.snapshot === "string"
924
+ ? JSON.parse(response.snapshot)
925
+ : (response.snapshot ?? {});
926
+ }
927
+ catch { }
928
+ const snap = await this.account(user, p), position = snap.positions[e.key] ?? "0";
929
+ const done = response.status === 3 &&
930
+ !snapshot.fill_settlement_pending &&
931
+ !snapshot.settlement_observation_required;
932
+ const failed = [4, 5].includes(response.status);
933
+ return {
934
+ task_id: task,
935
+ status: done
936
+ ? "completed"
937
+ : failed
938
+ ? "failed"
939
+ : response.legs?.some((l) => D(l.quantity ?? 0).gt(0))
940
+ ? "partially_filled"
941
+ : "submitted",
942
+ observed_quantity: position,
943
+ settled: done,
944
+ };
945
+ }
946
+ async notify(user, event) {
947
+ const tokens = await this.core("push_tokens", { user_id: user });
948
+ if (!tokens.length)
949
+ throw new AgentError("PUSH_NOT_REGISTERED", "No Ride App device is registered for push delivery.", true, 409);
950
+ const service = firebaseAccount();
951
+ if (!service)
952
+ throw new AgentError("PUSH_NOT_CONFIGURED", "Ride App push is not configured.", true, 503);
953
+ if (!this.tokens || this.tokens.expiry < now() + 60000) {
954
+ const encode = (v) => Buffer.from(JSON.stringify(v)).toString("base64url");
955
+ const at = Math.floor(now() / 1000), claim = `${encode({ alg: "RS256", typ: "JWT" })}.${encode({ iss: service.client_email, scope: "https://www.googleapis.com/auth/firebase.messaging", aud: "https://oauth2.googleapis.com/token", iat: at, exp: at + 3600 })}`;
956
+ const signer = createSign("RSA-SHA256");
957
+ signer.update(claim);
958
+ const assertion = `${claim}.${signer.sign(service.private_key).toString("base64url")}`;
959
+ const response = await fetch("https://oauth2.googleapis.com/token", {
960
+ method: "POST",
961
+ body: new URLSearchParams({
962
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
963
+ assertion,
964
+ }),
965
+ signal: AbortSignal.timeout(10000),
966
+ });
967
+ const payload = (await response.json());
968
+ if (!response.ok || !payload.access_token)
969
+ throw Error("Push authentication failed");
970
+ this.tokens = {
971
+ value: payload.access_token,
972
+ expiry: now() + (payload.expires_in ?? 3600) * 1000,
973
+ };
974
+ }
975
+ for (const token of tokens) {
976
+ const response = await fetch(`https://fcm.googleapis.com/v1/projects/${encodeURIComponent(service.project_id)}/messages:send`, {
977
+ method: "POST",
978
+ headers: {
979
+ Authorization: `Bearer ${this.tokens.value}`,
980
+ "Content-Type": "application/json",
981
+ },
982
+ body: JSON.stringify({
983
+ message: {
984
+ token,
985
+ notification: { title: "Ride Agent", body: event.message },
986
+ data: {
987
+ type: "ride_agent",
988
+ event_id: event.id,
989
+ event_kind: event.kind,
990
+ ...(event.portfolio_id
991
+ ? { portfolio_id: event.portfolio_id }
992
+ : {}),
993
+ },
994
+ android: { priority: "HIGH", collapse_key: event.id },
995
+ apns: { headers: { "apns-collapse-id": event.id } },
996
+ },
997
+ }),
998
+ signal: AbortSignal.timeout(10000),
999
+ });
1000
+ if (!response.ok)
1001
+ throw Error("Push delivery failed");
1002
+ }
1003
+ }
1004
+ async operation(op, input) {
1005
+ switch (op) {
1006
+ case "deposit_options":
1007
+ return new DepositReads((op, input) => this.core(op, input), this.network).options();
1008
+ case "deposit_address":
1009
+ return new DepositReads((op, input) => this.core(op, input), this.network).address(input.user_id, input.input, input.portfolio_id);
1010
+ case "deposit_status": {
1011
+ const status = await new DepositReads((op, input) => this.core(op, input), this.network).status(input.deposit, input.user_id);
1012
+ if (input.deposit.provider === "polymarket_bridge") {
1013
+ const balance = await this.prediction.fundingBalance(input.user_id);
1014
+ return {
1015
+ ...status,
1016
+ ...balance,
1017
+ sufficient_for_amount: D(balance.trading_balance_usdc).gte(input.deposit.amount_usdc ?? "5"),
1018
+ next_action: balance.available_to_copy
1019
+ ? "Trading collateral is verified. Refresh the saved plan before confirming copying."
1020
+ : "Bridge settlement is separate from trading collateral. Wait for verified credit and account readiness.",
1021
+ };
1022
+ }
1023
+ return status;
1024
+ }
1025
+ case "funding_quote":
1026
+ case "funding_submit":
1027
+ case "funding_activate_quote":
1028
+ case "funding_activate_submit":
1029
+ return this.core(`perps_${op}`, input);
1030
+ case "candidates": {
1031
+ const market = input?.market ?? "both";
1032
+ if (!["perps", "prediction", "both"].includes(market))
1033
+ throw new AgentError("INVALID_PREFERENCES", "Unknown candidate market.", false, 400);
1034
+ return this.candidates(market);
1035
+ }
1036
+ case "profile":
1037
+ return this.profile(input.trader_id);
1038
+ case "source":
1039
+ return this.source(input.trader_id);
1040
+ case "source_fills": {
1041
+ if (!Number.isSafeInteger(input.start_at) ||
1042
+ !Number.isSafeInteger(input.end_at) ||
1043
+ input.end_at <= input.start_at ||
1044
+ input.end_at - input.start_at > 31 * 86400_000 ||
1045
+ input.end_at > now())
1046
+ throw new AgentError("INVALID_WINDOW", "Invalid source history window.", false, 400);
1047
+ if (input.market !== "perps")
1048
+ return {
1049
+ fills: [],
1050
+ complete: false,
1051
+ reason: "Authenticated prediction source fill history is unavailable.",
1052
+ };
1053
+ const row = await this.resolve(input.trader_id);
1054
+ const history = await this.paged("userFillsByTime", row.address, input.start_at, input.end_at, true);
1055
+ const valid = history.events.filter((f) => f.time >= input.start_at && f.time < input.end_at);
1056
+ return {
1057
+ complete: history.complete,
1058
+ fills: valid.map((f) => ({
1059
+ id: `source_${fingerprint([input.trader_id, f.tid, f.oid]).slice(0, 32)}`,
1060
+ key: `perps:${f.coin}`,
1061
+ start_quantity: f.startPosition == null ? undefined : String(f.startPosition),
1062
+ quantity: D(f.sz)
1063
+ .times(f.side === "B" ? 1 : -1)
1064
+ .toString(),
1065
+ price: String(f.px),
1066
+ fee_usdc: f.feeToken === "USDC" ? String(f.fee) : null,
1067
+ realized_usdc: f.closedPnl == null ? null : String(f.closedPnl),
1068
+ at: f.time,
1069
+ })),
1070
+ };
1071
+ }
1072
+ case "notification_status": {
1073
+ const tokens = await this.core("push_tokens", {
1074
+ user_id: input.user_id,
1075
+ });
1076
+ return {
1077
+ configured: firebaseAccount() !== null,
1078
+ device_registered: Array.isArray(tokens) && tokens.length > 0,
1079
+ };
1080
+ }
1081
+ case "preflight":
1082
+ return this.account(input.user_id, input.portfolio);
1083
+ case "bind":
1084
+ return this.account(input.user_id, input.portfolio, true);
1085
+ case "account":
1086
+ return this.account(input.user_id, input.portfolio);
1087
+ case "submit":
1088
+ return this.submit(input.user_id, input.portfolio, input.execution);
1089
+ case "execution":
1090
+ return this.execution(input.user_id, input.portfolio, input.execution);
1091
+ case "fence":
1092
+ return this.fence(input.user_id, input.portfolio).then(() => ({
1093
+ ok: true,
1094
+ }));
1095
+ case "release":
1096
+ return this.release(input.user_id, input.portfolio).then(() => ({
1097
+ ok: true,
1098
+ }));
1099
+ case "cancel_increasing": {
1100
+ const snap = await this.account(input.user_id, input.portfolio);
1101
+ for (const order of snap.orders.filter((x) => x.increases_risk && x.key.startsWith("prediction:")))
1102
+ await this.core("prediction_cancel", {
1103
+ user_id: input.user_id,
1104
+ portfolio_id: input.portfolio.id,
1105
+ task_id: order.id,
1106
+ });
1107
+ if (snap.orders.some((x) => x.increases_risk))
1108
+ throw new AgentError("UNRESOLVED_OPEN_ORDER", "Increasing open orders require Core reconciliation before exit.", true);
1109
+ return { ok: true };
1110
+ }
1111
+ case "notify":
1112
+ return this.notify(input.user_id, input.event).then(() => ({
1113
+ ok: true,
1114
+ }));
1115
+ default:
1116
+ throw new AgentError("NOT_FOUND", "Unknown private adapter operation.", false, 404);
1117
+ }
1118
+ }
1119
+ }
1120
+ export async function startCoreBridge() {
1121
+ const bridge = new CoreBridge(required("RIDE_AGENT_CORE_URL"), required("RIDE_AGENT_CORE_SERVICE_TOKEN"), required("RIDE_AGENT_DATABASE_URL"), required("RIDE_AGENT_TRADER_ID_SALT"), process.env.RIDE_AGENT_NETWORK === "mainnet" ? "mainnet" : "testnet");
1122
+ await bridge.migrate();
1123
+ const token = required("RIDE_AGENT_EXECUTOR_TOKEN");
1124
+ if (token.length < 32)
1125
+ throw Error("Executor token must contain 32 characters");
1126
+ const app = express();
1127
+ app.disable("x-powered-by");
1128
+ app.get("/health", (_req, res) => res.json({ service: "ride-agent-bridge", version: "3", release: VERSION }));
1129
+ app.use(express.json({ limit: "1mb" }));
1130
+ app.post("/internal/agent/:operation", async (req, res) => {
1131
+ const actual = Buffer.from(req.get("authorization")?.replace(/^Bearer /, "") ?? ""), expected = Buffer.from(token);
1132
+ if (actual.length !== expected.length ||
1133
+ !timingSafeEqual(actual, expected)) {
1134
+ res
1135
+ .status(401)
1136
+ .json(failure(new AgentError("AUTH_REQUIRED", "Private executor authentication required.", false, 401)));
1137
+ return;
1138
+ }
1139
+ try {
1140
+ res.json(await bridge.operation(req.params.operation, req.body));
1141
+ }
1142
+ catch (e) {
1143
+ res
1144
+ .status(e instanceof AgentError ? e.httpStatus : 503)
1145
+ .json({ error: failure(e).error });
1146
+ }
1147
+ });
1148
+ const listener = app.listen(Number(process.env.PORT ?? 3355), process.env.HOST ?? "127.0.0.1");
1149
+ const stop = () => {
1150
+ listener.close();
1151
+ void bridge.close();
1152
+ };
1153
+ process.once("SIGTERM", stop);
1154
+ process.once("SIGINT", stop);
1155
+ }