@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,2373 @@
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ride Agent · Confirmation</title><style>/*INLINE_CSS*/:root {
2
+ font-family:
3
+ Inter,
4
+ -apple-system,
5
+ BlinkMacSystemFont,
6
+ "Segoe UI",
7
+ sans-serif;
8
+ color: #171717;
9
+ background: #fcfbf8;
10
+ font-size: 14px;
11
+ }
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+ body {
16
+ margin: 0;
17
+ padding: 20px;
18
+ }
19
+ main {
20
+ max-width: 900px;
21
+ margin: auto;
22
+ }
23
+ .brand {
24
+ font-size: 24px;
25
+ font-weight: 850;
26
+ letter-spacing: -1px;
27
+ }
28
+ .brand span {
29
+ background: #ffd94a;
30
+ padding: 2px 8px;
31
+ border-radius: 6px;
32
+ }
33
+ header {
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+ margin-bottom: 22px;
38
+ }
39
+ h1 {
40
+ font-size: 25px;
41
+ letter-spacing: -0.7px;
42
+ margin: 8px 0 6px;
43
+ }
44
+ h2 {
45
+ font-size: 16px;
46
+ margin: 0 0 12px;
47
+ }
48
+ .muted {
49
+ color: #666;
50
+ line-height: 1.6;
51
+ }
52
+ .card {
53
+ background: white;
54
+ border: 1px solid #e2e2da;
55
+ border-radius: 16px;
56
+ padding: 18px;
57
+ margin-bottom: 12px;
58
+ }
59
+ .grid {
60
+ margin-bottom: 12px;
61
+ display: grid;
62
+ grid-template-columns: repeat(2, minmax(0, 1fr));
63
+ gap: 12px;
64
+ }
65
+ .grid .card {
66
+ margin: 0;
67
+ }
68
+ .row {
69
+ display: flex;
70
+ justify-content: space-between;
71
+ gap: 16px;
72
+ padding: 8px 0;
73
+ border-bottom: 1px solid #f0f0eb;
74
+ }
75
+ .row:last-child {
76
+ border: 0;
77
+ }
78
+ .row strong {
79
+ text-align: right;
80
+ }
81
+ .metric {
82
+ font-size: 24px;
83
+ font-weight: 750;
84
+ }
85
+ .tag {
86
+ border-radius: 99px;
87
+ background: #f1f1e9;
88
+ padding: 5px 9px;
89
+ font-size: 12px;
90
+ }
91
+ .warning {
92
+ border-left: 3px solid #f6d600;
93
+ background: #fffce2;
94
+ padding: 12px;
95
+ line-height: 1.6;
96
+ border-radius: 8px;
97
+ margin: 14px 0;
98
+ }
99
+ button,
100
+ .button {
101
+ background: #ffd94a;
102
+ color: #111;
103
+ border: 0;
104
+ border-radius: 10px;
105
+ padding: 13px 18px;
106
+ font-weight: 750;
107
+ cursor: pointer;
108
+ display: inline-block;
109
+ text-decoration: none;
110
+ }
111
+ button.secondary {
112
+ background: #eee;
113
+ }
114
+ button:disabled {
115
+ opacity: 0.5;
116
+ cursor: wait;
117
+ }
118
+ .actions {
119
+ display: flex;
120
+ gap: 10px;
121
+ flex-wrap: wrap;
122
+ margin-top: 16px;
123
+ }
124
+ .error {
125
+ color: #a32626;
126
+ }
127
+ .good {
128
+ color: #147a4b;
129
+ }
130
+ table {
131
+ border-collapse: collapse;
132
+ width: 100%;
133
+ font-size: 13px;
134
+ }
135
+ th,
136
+ td {
137
+ padding: 10px 6px;
138
+ text-align: left;
139
+ border-bottom: 1px solid #eee;
140
+ }
141
+ .table-wrap {
142
+ overflow-x: auto;
143
+ }
144
+ input,
145
+ select {
146
+ padding: 10px;
147
+ border: 1px solid #ddd;
148
+ border-radius: 8px;
149
+ font: inherit;
150
+ width: 100%;
151
+ margin: 6px 0 14px;
152
+ }
153
+ details {
154
+ margin-top: 14px;
155
+ }
156
+ pre {
157
+ overflow: auto;
158
+ white-space: pre-wrap;
159
+ word-break: break-word;
160
+ font-size: 12px;
161
+ }
162
+ footer {
163
+ margin: 18px 0;
164
+ font-size: 12px;
165
+ color: #666;
166
+ }
167
+ .status {
168
+ font-size: 12px;
169
+ letter-spacing: 0.2px;
170
+ text-transform: uppercase;
171
+ }
172
+ @media (max-width: 520px) {
173
+ body {
174
+ padding: 14px;
175
+ }
176
+ .grid {
177
+ grid-template-columns: 1fr;
178
+ }
179
+ h1 {
180
+ font-size: 22px;
181
+ }
182
+ .card {
183
+ padding: 15px;
184
+ }
185
+ .row {
186
+ gap: 8px;
187
+ }
188
+ .metric {
189
+ font-size: 21px;
190
+ }
191
+ }
192
+ [hidden] {
193
+ display: none !important;
194
+ }
195
+ .field {
196
+ display: block;
197
+ color: #666;
198
+ font-size: 12px;
199
+ margin-top: 10px;
200
+ }
201
+ .field input,
202
+ .field select {
203
+ display: block;
204
+ color: #171717;
205
+ margin-top: 6px;
206
+ }
207
+ td,
208
+ th {
209
+ overflow-wrap: anywhere;
210
+ }
211
+ .card .grid {
212
+ margin-top: 12px;
213
+ }
214
+ .card h2:not(:first-child) {
215
+ margin-top: 20px;
216
+ }
217
+ summary {
218
+ cursor: pointer;
219
+ font-weight: 700;
220
+ padding: 12px 0;
221
+ }
222
+ .grid .card button {
223
+ margin-top: 10px;
224
+ padding: 8px 12px;
225
+ font-size: 12px;
226
+ }
227
+ @media (min-width: 820px) {
228
+ .grid {
229
+ grid-template-columns: repeat(3, minmax(0, 1fr));
230
+ }
231
+ }
232
+
233
+ .performance-chart svg {
234
+ width: 100%;
235
+ height: auto;
236
+ display: block;
237
+ }
238
+ .deposit-qr {
239
+ display: block;
240
+ max-width: 100%;
241
+ height: auto;
242
+ margin: 16px auto;
243
+ }
244
+ .deposit-address {
245
+ display: block;
246
+ overflow-wrap: anywhere;
247
+ padding: 12px;
248
+ background: #f5f3ec;
249
+ border-radius: 8px;
250
+ }
251
+ .field input[type="checkbox"] {
252
+ width: auto;
253
+ }
254
+ .confirmation-frame {
255
+ width: 100%;
256
+ height: 680px;
257
+ border: 1px solid #e2e2da;
258
+ border-radius: 16px;
259
+ background: #fcfbf8;
260
+ }
261
+ </style></head><body><main><header><div class="brand"><span>Ride Agent</span> / confirmation</div><span class="tag">Human approval</span></header><div id="login" class="card"><h1>Sign in to review your proposal</h1><p class="muted">Ride verifies your account before displaying or confirming changes.</p><label>Access code, if required<input id="access-code" autocomplete="off"></label><button id="sign-in">Continue with Google</button></div><div id="status" aria-live="polite"></div><div id="content"></div><div id="actions" class="actions" hidden><button id="approve">Confirm these parameters</button><button id="reject" class="secondary">Reject proposal</button></div></main><script type="module">/*INLINE_VIEW*/const el = (tag, text, cls) => {
262
+ const node = document.createElement(tag);
263
+ if (text !== undefined && text !== null) node.textContent = String(text);
264
+ if (cls) node.className = cls;
265
+ return node;
266
+ };
267
+ const val = (v) => (v === null || v === undefined ? "Unavailable" : String(v));
268
+ const row = (label, value) => {
269
+ const r = el("div", null, "row");
270
+ r.append(el("span", label, "muted"), el("strong", val(value)));
271
+ return r;
272
+ };
273
+ const card = (title) => {
274
+ const c = el("section", null, "card");
275
+ c.append(el("h2", title));
276
+ return c;
277
+ };
278
+ const usdc = (v) => (v == null ? "Unavailable" : `${v} USDC`);
279
+ const sumCash = (amounts) => {
280
+ const n = amounts.reduce((total, v) => {
281
+ const [whole, fraction = ""] = String(v).split(".");
282
+ return total + BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
283
+ }, 0n);
284
+ return `${n / 1000000n}.${String(n % 1000000n).padStart(6, "0")}`;
285
+ };
286
+ function sleeveCards(
287
+ sleeves,
288
+ callTool,
289
+ addTrader,
290
+ actionLabel = "Add to basket",
291
+ ) {
292
+ const grid = el("div", null, "grid");
293
+ for (const s of sleeves) {
294
+ const c = card(s.trader?.handle ?? s.copy_id);
295
+ c.append(
296
+ el("span", `${s.trader?.market ?? ""} · ${s.trader?.style ?? ""}`, "tag"),
297
+ row("Copy Score", s.trader?.score),
298
+ row("Allocation", usdc(s.amount_usdc)),
299
+ ...(s.leverage_mode != null
300
+ ? [row("Leverage mode", "Follow source leverage")]
301
+ : []),
302
+ row(
303
+ s.leverage_mode != null ? "Your leverage cap" : "Leverage cap",
304
+ s.leverage_cap == null && s.leverage_mode === "follow_source"
305
+ ? "No extra cap"
306
+ : s.leverage_cap == null
307
+ ? null
308
+ : `${s.leverage_cap}x`,
309
+ ),
310
+ row(
311
+ s.loss_policy === "pause_then_exit"
312
+ ? "Pause new exposure"
313
+ : "Sleeve exit trigger",
314
+ s.stop_loss_pct == null ? null : `${s.stop_loss_pct}%`,
315
+ ),
316
+ row("Copy mode", s.copy_mode ?? "Ride Agent recommended"),
317
+ ...(s.copy_mode === "fixed"
318
+ ? [row("Fixed margin per source entry", usdc(s.fixed_margin_usdc))]
319
+ : []),
320
+ ...(s.loss_policy === "pause_then_exit"
321
+ ? [row("Emergency exit", `${s.emergency_exit_pct ?? 70}%`)]
322
+ : []),
323
+ );
324
+ if (s.state)
325
+ c.append(
326
+ row("State", s.state),
327
+ row("Net contribution", usdc(s.net_profit_usdc)),
328
+ );
329
+ if (callTool) {
330
+ const b = el("button", "Trader details", "secondary");
331
+ b.addEventListener("click", () =>
332
+ callTool("get_trader_profile", { trader_id: s.trader.id }),
333
+ );
334
+ c.append(b);
335
+ }
336
+ if (addTrader && s.amount_usdc == null) {
337
+ const add = el("button", actionLabel, "secondary");
338
+ add.addEventListener("click", () => addTrader(s.trader));
339
+ c.append(add);
340
+ }
341
+ grid.append(c);
342
+ }
343
+ return grid;
344
+ }
345
+ function render(
346
+ root,
347
+ value,
348
+ openLink = () => {},
349
+ callTool = () => {},
350
+ confirmationCard,
351
+ ) {
352
+ root.replaceChildren();
353
+ if (!value) {
354
+ welcome(root, callTool);
355
+ return;
356
+ }
357
+ if (value.data?.preference_saved)
358
+ root.append(el("p", "Preferences saved", "status"));
359
+ else if (
360
+ ["queued", "executing", "uncertain", "settling"].includes(value.status)
361
+ )
362
+ root.append(
363
+ el(
364
+ "p",
365
+ {
366
+ queued: "Confirmed changes queued",
367
+ executing: "Execution in progress",
368
+ uncertain: "Execution status needs verification",
369
+ settling: "Settlement pending",
370
+ }[value.status],
371
+ "status",
372
+ ),
373
+ );
374
+ const d = value.data ?? {};
375
+ if (
376
+ confirmationCard &&
377
+ d.proposal &&
378
+ (d.proposal.status === "requires_confirmation" ||
379
+ d.confirmation_demo ||
380
+ d.confirmation_card)
381
+ ) {
382
+ root.append(
383
+ el(
384
+ "h1",
385
+ d.confirmation_demo
386
+ ? "确认卡片测试"
387
+ : d.proposal.status === "requires_confirmation"
388
+ ? "确认本次操作"
389
+ : "操作状态记录",
390
+ ),
391
+ );
392
+ confirmationCard(root, d.proposal, d.confirmation_demo === true);
393
+ return;
394
+ }
395
+ if (d.account?.authenticated) {
396
+ root.append(
397
+ el(
398
+ "p",
399
+ `Signed in${d.account.display_name ? `: ${d.account.display_name}` : ""}`,
400
+ "muted",
401
+ ),
402
+ );
403
+ }
404
+ const appCopies =
405
+ d.app_copies ?? (d.query_scope === "ride_app_copies" ? d : null);
406
+ if (appCopies) {
407
+ const account = card("Your Ride App copies");
408
+ for (const [name, market] of [
409
+ ["Perps", appCopies.perps],
410
+ ["Prediction", appCopies.prediction],
411
+ ]) {
412
+ account.append(
413
+ row(`${name} copy configurations`, market?.configs?.length),
414
+ row(
415
+ `${name} enabled copies`,
416
+ market?.configs?.filter((c) => c.isEnabled).length,
417
+ ),
418
+ row(`${name} current copied positions`, market?.positions?.length),
419
+ );
420
+ }
421
+ account.append(
422
+ el(
423
+ "p",
424
+ appCopies.coverage_complete
425
+ ? "Account read complete. Manage these existing copies in Ride."
426
+ : "Account read incomplete. Missing copies or positions are unavailable.",
427
+ "muted",
428
+ ),
429
+ );
430
+ root.append(account);
431
+ }
432
+ if (d.account_balances)
433
+ renderAccountBalances(root, d.account_balances, callTool);
434
+ if (value.error) root.append(el("p", value.error.message, "warning error"));
435
+ if (d.recommendation_diagnostics && (value.error || !d.plan))
436
+ renderRecommendationDiagnostics(root, d, callTool);
437
+ if (Array.isArray(d.candidates)) renderCandidates(root, d, callTool);
438
+ if (d.market_coverage)
439
+ root.append(
440
+ el(
441
+ "p",
442
+ `Allowed markets: ${d.market_coverage.requested}. Selected: ${d.market_coverage.selected.join(", ")}. Either market may be selected; both are not required.`,
443
+ "muted",
444
+ ),
445
+ );
446
+ if (d.deposit_options)
447
+ renderDepositOptions(root, d.deposit_options, callTool);
448
+ if (d.deposit) renderDeposit(root, d.deposit, callTool);
449
+ if (d.deposit_status) {
450
+ const c = card("Deposit status");
451
+ c.append(
452
+ row("Status", d.deposit_status.status),
453
+ row("Trading available", usdc(d.deposit_status.trading_balance_usdc)),
454
+ row(
455
+ "Saved plan shortfall",
456
+ usdc(d.deposit_status.saved_plan_shortfall_usdc),
457
+ ),
458
+ row("Available to copy", d.deposit_status.available_to_copy),
459
+ el("p", d.deposit_status.next_action, "muted"),
460
+ );
461
+ const check = el("button", "Check again", "secondary");
462
+ check.addEventListener("click", () =>
463
+ callTool("check_deposit_status", {
464
+ deposit_id: d.deposit_status.deposit_id,
465
+ }),
466
+ );
467
+ c.append(check);
468
+ if (d.saved_plan_id) {
469
+ const resume = el("button", "Refresh saved plan and review");
470
+ resume.addEventListener("click", () =>
471
+ callTool("recalculate_plan", { plan_id: d.saved_plan_id }),
472
+ );
473
+ c.append(resume);
474
+ }
475
+ root.append(c);
476
+ }
477
+ if (d.daily_checkin) {
478
+ const c = card(
479
+ d.daily_checkin.offer_review
480
+ ? "Welcome back · daily portfolio review"
481
+ : "Today’s check-in",
482
+ );
483
+ c.append(
484
+ el(
485
+ "p",
486
+ d.daily_checkin.offer_review
487
+ ? "Would you like to review the last 24 hours?"
488
+ : "Your daily check-in has already been offered, or no Agent portfolio is active.",
489
+ "muted",
490
+ ),
491
+ );
492
+ if (d.daily_checkin.offer_review) {
493
+ const b = el("button", "Review performance");
494
+ b.addEventListener("click", () =>
495
+ callTool("get_performance", {
496
+ portfolio_id: d.daily_checkin.portfolio_id,
497
+ period: "daily",
498
+ }),
499
+ );
500
+ c.append(b);
501
+ }
502
+ root.append(c);
503
+ }
504
+ if (d.preference_collection && !d.preference_collection.ready) {
505
+ welcome(
506
+ root,
507
+ callTool,
508
+ d.preference_collection.answers,
509
+ d.preference_collection,
510
+ );
511
+ } else if (d.preferences) {
512
+ root.append(
513
+ el(
514
+ "p",
515
+ `Assets: ${d.preferences.asset_category ?? "any"} · ${d.preferences.assets?.join(", ") || "no symbol restriction"}. Change preferences in the AI conversation.`,
516
+ "muted",
517
+ ),
518
+ );
519
+ if (d.preference_collection?.suggestions)
520
+ root.append(
521
+ el(
522
+ "p",
523
+ `Suggested portfolio loss trigger: ${d.preference_collection.suggestions.loss_trigger_pct}%. Review or change before confirming in Ride; this is not a guaranteed loss limit.`,
524
+ "warning",
525
+ ),
526
+ );
527
+ }
528
+ if (d.funding) {
529
+ const c = card("Fund your trading account"),
530
+ f = d.funding;
531
+ c.append(
532
+ row("Amount", usdc(f.amount_usdc)),
533
+ row("Status", f.status),
534
+ row("Destination", f.destination),
535
+ );
536
+ const b = el("button", "Review and confirm funding in Ride");
537
+ b.addEventListener("click", () => openLink(f.confirmation_url));
538
+ c.append(
539
+ b,
540
+ el(
541
+ "p",
542
+ "No funds move until you confirm in Ride. Submitted or uncertain requests must be checked before retrying.",
543
+ "muted",
544
+ ),
545
+ );
546
+ root.append(c);
547
+ }
548
+ if (d.plan) {
549
+ const plan = d.plan;
550
+ root.append(
551
+ el(
552
+ "h1",
553
+ d.query_scope === "agent_portfolio_and_ride_app"
554
+ ? "Saved basket preview"
555
+ : "Your trader basket",
556
+ ),
557
+ el(
558
+ "p",
559
+ `${usdc(plan.preferences.budget_usdc)} · ${plan.preferences.market} · ${plan.allocations.length} traders`,
560
+ "muted",
561
+ ),
562
+ sleeveCards(plan.allocations, callTool),
563
+ );
564
+ const risk = card("Portfolio parameters");
565
+ risk.append(
566
+ row(
567
+ "Selected allocation",
568
+ usdc(sumCash(plan.allocations.map((a) => a.amount_usdc))),
569
+ ),
570
+ row("Unallocated budget", usdc(d.unallocated_usdc ?? "0.000000")),
571
+ row("Loss trigger", `${plan.preferences.loss_trigger_pct}%`),
572
+ row(
573
+ "Historical portfolio drawdown",
574
+ d.portfolio_historical_drawdown_pct == null
575
+ ? "Unavailable"
576
+ : `${d.portfolio_historical_drawdown_pct}%`,
577
+ ),
578
+ row("Existing entry deviation", "≤ 0.2%"),
579
+ row("Plan expires", new Date(plan.expires_at).toLocaleString()),
580
+ );
581
+ root.append(risk);
582
+ const start = el("button", "Review start proposal");
583
+ start.disabled = plan.expires_at <= Date.now();
584
+ start.addEventListener("click", () =>
585
+ callTool("start_copy", {
586
+ plan_id: plan.id,
587
+ ...(start.dataset.compatibility
588
+ ? { compatibility_ack: start.dataset.compatibility }
589
+ : {}),
590
+ }),
591
+ );
592
+ const refresh = el("button", "Refresh saved plan", "secondary");
593
+ refresh.addEventListener("click", () =>
594
+ callTool("recalculate_plan", {
595
+ plan_id: plan.id,
596
+ }),
597
+ );
598
+
599
+ renderCompatibility(root, plan, d, callTool, start);
600
+ root.append(planEditor(plan, d, callTool, start));
601
+ if (d.query_scope !== "agent_portfolio_and_ride_app")
602
+ root.append(
603
+ el(
604
+ "div",
605
+ "Preview works before funding. Use the Ride funding confirmation page to fund and activate the trading account, then refresh this saved plan and review a new proposal.",
606
+ "warning",
607
+ ),
608
+ );
609
+ const actions = el("div", null, "actions");
610
+ const deposit = el("button", "Deposit into Ride", "secondary");
611
+ deposit.addEventListener("click", () =>
612
+ callTool("get_deposit_options", {}),
613
+ );
614
+ actions.append(start, refresh, deposit);
615
+ root.append(actions);
616
+ root.append(
617
+ el(
618
+ "p",
619
+ "Perps follows source leverage with no extra user cap by default. You can set or clear your cap; exchange limits, budget and risk checks still apply. A single-trader plan is concentrated. Review each trader’s pause/exit rules before confirming.",
620
+ "muted",
621
+ ),
622
+ );
623
+ }
624
+ if (d.portfolio) {
625
+ const p = d.portfolio;
626
+ root.append(
627
+ el("h1", "Your portfolio"),
628
+ el("p", `${p.state} · ${p.id}`, "muted"),
629
+ );
630
+ const overview = card("Agent trading account snapshot");
631
+ overview.append(
632
+ row("Account value", usdc(p.account_value_usdc)),
633
+ row("Reconciled net profit", usdc(p.net_profit_usdc)),
634
+ row("Available funds", usdc(p.available_usdc)),
635
+ row("Pending reserve", usdc(p.reserved_usdc)),
636
+ row("Allocated funds", usdc(p.allocated_usdc)),
637
+ row("Accounting", p.reconciliation),
638
+ );
639
+ const portfolioStop = field(
640
+ overview,
641
+ "Portfolio loss trigger · %",
642
+ "number",
643
+ p.preferences.loss_trigger_pct,
644
+ );
645
+ const changeStop = el(
646
+ "button",
647
+ "Review portfolio stop change",
648
+ "secondary",
649
+ );
650
+ changeStop.addEventListener("click", () =>
651
+ callTool("update_portfolio", {
652
+ portfolio_id: p.id,
653
+ loss_trigger_pct: Number(portfolioStop.value),
654
+ }),
655
+ );
656
+ overview.append(changeStop);
657
+ root.append(overview, sleeveCards(p.sleeves, callTool));
658
+ const actions = el("div", null, "actions");
659
+ for (const [label, name, args] of [
660
+ ["Refresh portfolio", "get_portfolio", { portfolio_id: p.id }],
661
+ ["Review portfolio", "review_portfolio", { portfolio_id: p.id }],
662
+ [
663
+ "Performance history",
664
+ "get_performance",
665
+ { portfolio_id: p.id, period: "weekly" },
666
+ ],
667
+ ["Daily report settings", "get_notification_preferences", {}],
668
+ ]) {
669
+ const b = el("button", label, "secondary");
670
+ b.addEventListener("click", () => callTool(name, args));
671
+ actions.append(b);
672
+ }
673
+ root.append(actions);
674
+ for (const s of p.sleeves) {
675
+ const controls = card(`${s.trader.handle} · manage copy`);
676
+ const diagnostic = el("button", "Explain copy performance", "secondary");
677
+ diagnostic.addEventListener("click", () => {
678
+ const end = Date.now();
679
+ callTool("diagnose_copy", {
680
+ copy_id: s.copy_id,
681
+ start_at: Math.max(
682
+ p.ledger_started_at ?? p.created_at,
683
+ end - 7 * 86400000,
684
+ ),
685
+ end_at: end,
686
+ });
687
+ });
688
+ controls.append(diagnostic);
689
+ if (s.state === "closed") {
690
+ const replacement = field(
691
+ controls,
692
+ "Replacement public trader ID",
693
+ "text",
694
+ "",
695
+ );
696
+ const b = el("button", "Review replacement proposal", "secondary");
697
+ b.addEventListener("click", () =>
698
+ callTool("update_copy", {
699
+ copy_id: s.copy_id,
700
+ replacement_trader_id: replacement.value.trim(),
701
+ }),
702
+ );
703
+ controls.append(
704
+ b,
705
+ el(
706
+ "p",
707
+ "Requires verified released funds; one replacement per rolling seven days. The previous copy remains in history.",
708
+ "muted",
709
+ ),
710
+ );
711
+ } else if (s.state === "active") {
712
+ const close = el("button", "Review immediate exit", "secondary");
713
+ close.addEventListener("click", () =>
714
+ callTool("stop_copy", { copy_id: s.copy_id, mode: "close_now" }),
715
+ );
716
+ controls.append(close);
717
+ }
718
+ const margin = field(
719
+ controls,
720
+ "Allocation · USDC",
721
+ "text",
722
+ s.amount_usdc,
723
+ ),
724
+ leverage = field(
725
+ controls,
726
+ "Leverage cap · optional",
727
+ "number",
728
+ s.leverage_cap ?? "",
729
+ ),
730
+ pause = field(
731
+ controls,
732
+ "Trader pause/exit threshold · %",
733
+ "number",
734
+ s.stop_loss_pct,
735
+ );
736
+ leverage.min = "1";
737
+ leverage.max = s.trader?.market === "prediction" ? "1" : "40";
738
+ leverage.placeholder = "No extra cap · follow source";
739
+ const mode = select(
740
+ controls,
741
+ "Copy mode",
742
+ [
743
+ ["ride_recommends", "Ride Agent recommended"],
744
+ ["fixed", "Fixed margin"],
745
+ ["proportional", "Proportional"],
746
+ ],
747
+ s.copy_mode ?? "ride_recommends",
748
+ );
749
+ const fixed = field(
750
+ controls,
751
+ "Fixed margin per source entry · USDC",
752
+ "text",
753
+ s.fixed_margin_usdc ?? "10",
754
+ ),
755
+ multiplier = field(
756
+ controls,
757
+ "Proportional multiplier",
758
+ "number",
759
+ s.proportional_multiplier ?? 1,
760
+ );
761
+ const lossPolicy = select(
762
+ controls,
763
+ "Trader loss policy",
764
+ [
765
+ ["exit", "Exit at threshold"],
766
+ ["pause_then_exit", "Pause, then emergency exit"],
767
+ ],
768
+ s.loss_policy ?? "exit",
769
+ ),
770
+ emergency = field(
771
+ controls,
772
+ "Emergency exit threshold · %",
773
+ "number",
774
+ s.emergency_exit_pct ?? 70,
775
+ );
776
+ const edit = el("button", "Review parameter change", "secondary");
777
+ edit.disabled = s.state === "closed";
778
+ edit.addEventListener("click", () =>
779
+ callTool("update_copy", {
780
+ copy_id: s.copy_id,
781
+ amount_usdc: margin.value.trim(),
782
+ leverage_cap:
783
+ leverage.value.trim() === "" ? null : Number(leverage.value),
784
+ stop_loss_pct: Number(pause.value),
785
+ copy_mode: mode.value,
786
+ ...(mode.value === "fixed"
787
+ ? { fixed_margin_usdc: fixed.value.trim() }
788
+ : { proportional_multiplier: Number(multiplier.value) }),
789
+ loss_policy: lossPolicy.value,
790
+ ...(lossPolicy.value === "pause_then_exit"
791
+ ? { emergency_exit_pct: Number(emergency.value) }
792
+ : {}),
793
+ }),
794
+ );
795
+ controls.append(edit);
796
+ if (s.state === "risk_off") {
797
+ const resume = el("button", "Review resuming new trades", "secondary");
798
+ resume.addEventListener("click", () =>
799
+ callTool("update_copy", {
800
+ copy_id: s.copy_id,
801
+ resume_new_trades: true,
802
+ }),
803
+ );
804
+ controls.append(resume);
805
+ }
806
+ root.append(controls);
807
+ }
808
+ for (const s of p.sleeves.filter((s) => s.state === "active")) {
809
+ const stop = el("button", `Wind down ${s.trader.handle}`, "secondary");
810
+ stop.addEventListener("click", () =>
811
+ callTool("stop_copy", { copy_id: s.copy_id, mode: "wind_down" }),
812
+ );
813
+ root.append(stop);
814
+ }
815
+ const positions = card("Owned positions · gross and net");
816
+ for (const a of p.positions ?? [])
817
+ positions.append(
818
+ row(
819
+ `${a.asset} · ${a.market}`,
820
+ `Gross ${usdc(a.gross_notional_usdc)} / Net ${usdc(a.net_notional_usdc)}`,
821
+ ),
822
+ );
823
+ root.append(positions);
824
+ if (p.executions?.length) {
825
+ const executions = card("Execution status");
826
+ for (const e of p.executions) executions.append(row(e.key, e.status));
827
+ root.append(executions);
828
+ }
829
+ }
830
+ if (d.trader) {
831
+ const t = d.trader;
832
+ root.append(
833
+ el("h1", t.handle),
834
+ el("p", `${t.market} · ${t.style} · ${t.direction}`, "muted"),
835
+ );
836
+ const profile = card(`Copy Score ${t.score}`);
837
+ profile.append(row("Scoring version", t.score_version));
838
+ for (const [key, value] of Object.entries(t.components ?? {}))
839
+ profile.append(row(key, value));
840
+ profile.append(
841
+ row(
842
+ "Source ROI",
843
+ t.source_roi_pct == null ? "Unavailable" : `${t.source_roi_pct}%`,
844
+ ),
845
+ row(
846
+ "Source drawdown",
847
+ t.source_drawdown_pct == null
848
+ ? "Unavailable"
849
+ : `${t.source_drawdown_pct}%`,
850
+ ),
851
+ );
852
+ const single = el("button", "Copy this trader only", "secondary");
853
+ single.addEventListener("click", () => {
854
+ root.replaceChildren();
855
+ welcome(
856
+ root,
857
+ (name, args) =>
858
+ callTool(
859
+ name,
860
+ name === "recommend_traders"
861
+ ? { ...args, trader_ids: [t.id], batch_action: undefined }
862
+ : args,
863
+ ),
864
+ {
865
+ ...(d.preferences ?? {}),
866
+ portfolio_kind: "single",
867
+ basket_size: 1,
868
+ market: d.preferences?.market ?? t.market,
869
+ },
870
+ );
871
+ });
872
+ profile.append(single);
873
+ if (d.saved_plan && d.saved_plan.preferences.portfolio_kind !== "single") {
874
+ const add = el("button", "Add to saved basket", "secondary");
875
+ add.addEventListener("click", () =>
876
+ addToPlan(d.saved_plan, t, {}, callTool),
877
+ );
878
+ profile.append(add);
879
+ }
880
+ root.append(
881
+ profile,
882
+ el(
883
+ "p",
884
+ "Source performance describes the trader, not your follower account.",
885
+ "muted",
886
+ ),
887
+ );
888
+ }
889
+ if (d.proposal) {
890
+ const q = d.proposal;
891
+ root.append(
892
+ el("h1", "Review before confirming"),
893
+ el("p", `${q.action} · ${q.status}`, "muted"),
894
+ );
895
+ if (q.preview?.portfolio?.portfolio)
896
+ renderNestedPortfolio(root, q.preview.portfolio.portfolio);
897
+ const params = card("Frozen parameters");
898
+ params.append(
899
+ row("Loss trigger amount", usdc(q.preview?.loss_trigger_amount_usdc)),
900
+ row("Existing entry deviation", "≤ 0.2%"),
901
+ row(
902
+ "Fee estimate",
903
+ q.preview?.fees?.estimate_usdc == null
904
+ ? "Unavailable · exact fee at settlement"
905
+ : usdc(q.preview.fees.estimate_usdc),
906
+ ),
907
+ row("Expires", new Date(q.expires_at).toLocaleString()),
908
+ row(
909
+ "Execution state",
910
+ q.status === "requires_confirmation"
911
+ ? "Not submitted"
912
+ : q.execution_tasks?.map((e) => e.status).join(", ") || q.status,
913
+ ),
914
+ );
915
+ const details = el("details");
916
+ details.append(
917
+ el("summary", "View exact requested changes"),
918
+ el("pre", JSON.stringify(q.arguments, null, 2)),
919
+ );
920
+ params.append(details);
921
+ root.append(params);
922
+ if (
923
+ confirmationCard &&
924
+ (q.status === "requires_confirmation" || d.confirmation_demo)
925
+ ) {
926
+ confirmationCard(root, q, d.confirmation_demo === true);
927
+ } else if (q.status === "requires_confirmation" && q.confirmation_url) {
928
+ const b = el("button", "Open Ride confirmation");
929
+ b.addEventListener("click", () => openLink(q.confirmation_url));
930
+ root.append(b);
931
+ }
932
+ }
933
+ if (d.observations) {
934
+ root.append(el("h1", "Portfolio review"));
935
+ for (const o of d.observations)
936
+ root.append(
937
+ el(
938
+ "p",
939
+ Object.entries(o)
940
+ .map(([k, v]) => `${k}: ${val(v)}`)
941
+ .join(" · "),
942
+ "card",
943
+ ),
944
+ );
945
+ if (!d.observations.length)
946
+ root.append(
947
+ el("p", "No actionable observations from current coverage.", "muted"),
948
+ );
949
+ }
950
+ if (d.updates) {
951
+ root.append(el("h1", "Ride Agent updates"));
952
+ for (const u of d.updates) {
953
+ const c = card(u.kind);
954
+ c.append(
955
+ el("p", u.message),
956
+ el("time", new Date(u.at).toLocaleString(), "muted"),
957
+ );
958
+ root.append(c);
959
+ }
960
+ if (!d.updates.length) root.append(el("p", "No new updates.", "muted"));
961
+ }
962
+ if (d.performance) renderPerformance(root, d.performance, callTool);
963
+ if (d.diagnostic) renderDiagnostic(root, d.diagnostic);
964
+ if (d.notification_preferences) renderNotifications(root, d, callTool);
965
+ for (const warning of value.warnings ?? [])
966
+ root.append(el("p", warning, "warning"));
967
+ root.append(
968
+ el(
969
+ "footer",
970
+ `Data as of ${value.as_of ?? "Unavailable"}. Configuration, submission, fill and settlement are separate states.`,
971
+ ),
972
+ );
973
+ }
974
+ const candidateSelections = new WeakMap();
975
+ function renderCandidates(root, data, callTool) {
976
+ const candidates = data.candidates,
977
+ pref = data.preferences ?? data.plan?.preferences;
978
+ const c = card("Eligible trader candidates"),
979
+ selected = new Set(
980
+ candidateSelections.get(data) ??
981
+ data.plan?.allocations.map((a) => a.trader.id) ??
982
+ [],
983
+ );
984
+ const meta = data.candidates_meta ?? { total: candidates.length };
985
+ c.append(
986
+ el(
987
+ "p",
988
+ `${candidates.length} returned · ${meta.total} eligible in total. Candidates and plan generation are separate.`,
989
+ "muted",
990
+ ),
991
+ );
992
+ if (!candidates.length)
993
+ c.append(
994
+ el(
995
+ "p",
996
+ "No eligible candidates under the current filters. Review the diagnostic rules below.",
997
+ ),
998
+ );
999
+ const rows = el("div", null, "grid"),
1000
+ selection = el("p", "Select 1–8 traders to review a plan.", "muted");
1001
+ const generate = el("button", "Generate plan from selected traders");
1002
+ const update = () => {
1003
+ candidateSelections.set(data, [...selected]);
1004
+ generate.disabled = !pref || selected.size < 1 || selected.size > 8;
1005
+ selection.textContent = `${selected.size} traders selected · ${pref?.budget_usdc ?? "Unavailable"} USDC budget · risk and loss settings unchanged`;
1006
+ };
1007
+ let shown = 0;
1008
+ const more = el("button", "Load more candidates", "secondary");
1009
+ const appendPage = () => {
1010
+ for (const t of candidates.slice(shown, shown + 8)) {
1011
+ const r = card(t.handle);
1012
+ r.append(
1013
+ row(
1014
+ "Market / category",
1015
+ `${t.market} / ${t.asset_category ?? "unavailable"}`,
1016
+ ),
1017
+ row("Copy Score", t.score),
1018
+ row("Trading assets", t.assets?.join(", ") || "Unavailable"),
1019
+ row("Qualification", t.eligibility),
1020
+ );
1021
+ const info = (
1022
+ data.recommendation_diagnostics?.candidate_evaluations ?? []
1023
+ ).find((x) => x.trader_id === t.id);
1024
+ if (info)
1025
+ r.append(
1026
+ row(
1027
+ "Fits requested per-trader budget",
1028
+ info.budget_eligible ? "Yes" : "No · review the allocation minimum",
1029
+ ),
1030
+ );
1031
+ const label = el("label", "Select trader", "field"),
1032
+ check = el("input");
1033
+ check.type = "checkbox";
1034
+ check.checked = selected.has(t.id);
1035
+ check.addEventListener("change", () => {
1036
+ if (check.checked) selected.add(t.id);
1037
+ else selected.delete(t.id);
1038
+ update();
1039
+ });
1040
+ label.append(check);
1041
+ const details = el("button", "Trader details", "secondary");
1042
+ details.addEventListener("click", () =>
1043
+ callTool("get_trader_profile", { trader_id: t.id }),
1044
+ );
1045
+ r.append(label, details);
1046
+ rows.append(r);
1047
+ }
1048
+ shown = Math.min(candidates.length, shown + 8);
1049
+ more.hidden = shown >= candidates.length;
1050
+ more.textContent = `Load remaining candidates (${candidates.length - shown})`;
1051
+ };
1052
+ more.addEventListener("click", appendPage);
1053
+ appendPage();
1054
+ update();
1055
+ generate.addEventListener("click", () =>
1056
+ callTool("recommend_traders", {
1057
+ preferences: {
1058
+ ...pref,
1059
+ portfolio_kind: selected.size === 1 ? "single" : "basket",
1060
+ basket_size: selected.size,
1061
+ },
1062
+ trader_ids: [...selected],
1063
+ }),
1064
+ );
1065
+ c.append(rows, more, selection, generate);
1066
+ root.append(c);
1067
+ }
1068
+ function renderRecommendationDiagnostics(root, data, callTool) {
1069
+ const d = data.recommendation_diagnostics,
1070
+ c = card("Why this plan is unavailable");
1071
+ c.append(
1072
+ row("Requested trader count", d.requested_basket_size),
1073
+ row("Failure stage", d.stage),
1074
+ row("Diagnostic ID", d.diagnostic_id),
1075
+ el("p", d.count_semantics ?? "", "muted"),
1076
+ );
1077
+ for (const [label, key] of [
1078
+ ["Source", "source_candidates"],
1079
+ ["Matching market", "market_candidates"],
1080
+ ["Fresh", "fresh_candidates"],
1081
+ ["Qualified", "qualified_candidates"],
1082
+ ["Matching assets", "asset_matching_candidates"],
1083
+ ["Executable risk / mode", "eligible_candidates"],
1084
+ ["Affordable at the requested count", "budget_eligible_candidates"],
1085
+ ])
1086
+ c.append(row(label, d[key]));
1087
+ for (const a of d.adjustments ?? []) {
1088
+ const b = el("button", a.label, "secondary");
1089
+ b.addEventListener("click", () =>
1090
+ callTool("recommend_traders", { preferences: a.preferences }),
1091
+ );
1092
+ c.append(b);
1093
+ }
1094
+ const details = el("details");
1095
+ details.append(el("summary", "View exclusions and affected fields"));
1096
+ for (const r of d.rules ?? [])
1097
+ details.append(
1098
+ el(
1099
+ "p",
1100
+ `${r.trader_id}: ${r.message} Field: ${r.field}. Current: ${JSON.stringify(r.current)}. Required: ${JSON.stringify(r.required)}.`,
1101
+ ),
1102
+ );
1103
+ c.append(details);
1104
+ root.append(c);
1105
+ }
1106
+ function renderAccountBalances(root, balances, callTool) {
1107
+ const c = card("Your account balances"),
1108
+ summary = balances.summary ?? {};
1109
+ c.append(
1110
+ row(
1111
+ "Complete account total · USD",
1112
+ summary.account_value_usd ?? "Unavailable · incomplete coverage",
1113
+ ),
1114
+ row("Verified subtotal · USD", summary.known_subtotal_usd),
1115
+ el(
1116
+ "p",
1117
+ summary.basis ?? "Missing account data is unavailable, never zero.",
1118
+ "muted",
1119
+ ),
1120
+ );
1121
+ for (const [name, market] of Object.entries(balances.markets ?? {})) {
1122
+ const section = card(
1123
+ name === "perps"
1124
+ ? "Perps · Stocks & Crypto"
1125
+ : name === "prediction"
1126
+ ? "Prediction"
1127
+ : "Meme · independent wallet module",
1128
+ );
1129
+ section.append(
1130
+ row("Availability", market.status === "ok" ? "Available" : market.status),
1131
+ row("Account value · USD", market.account_value_usd),
1132
+ row("Available trading collateral", usdc(market.available_trading_usdc)),
1133
+ row("Occupied / reserved", usdc(market.occupied_or_reserved_usdc)),
1134
+ row(
1135
+ market.positions_value_basis?.includes("notional")
1136
+ ? "Positions · gross notional, not account equity"
1137
+ : "Position value · USD",
1138
+ market.positions_value_usd,
1139
+ ),
1140
+ row(
1141
+ "Source time",
1142
+ market.observed_at ?? "Source does not supply a timestamp",
1143
+ ),
1144
+ row("Fetched at", market.fetched_at ?? balances.fetched_at),
1145
+ );
1146
+ if (market.cash_observed_at)
1147
+ section.append(row("Cash source time", market.cash_observed_at));
1148
+ if (market.known_subtotal_usd != null && market.account_value_usd == null)
1149
+ section.append(
1150
+ row("Verified partial value · USD", market.known_subtotal_usd),
1151
+ );
1152
+ if (
1153
+ market.retained_wallet_cash_usdc != null &&
1154
+ Number(market.retained_wallet_cash_usdc) > 0
1155
+ )
1156
+ section.append(
1157
+ row("Retained-wallet cash", usdc(market.retained_wallet_cash_usdc)),
1158
+ row("App display basis · USD", market.app_display_value_usd),
1159
+ el("p", market.app_difference_reason, "muted"),
1160
+ );
1161
+ if (market.reason) section.append(el("p", market.reason, "muted"));
1162
+ c.append(section);
1163
+ }
1164
+ const refresh = el("button", "Refresh all account balances", "secondary");
1165
+ refresh.addEventListener("click", () => callTool("get_portfolio", {}));
1166
+ c.append(refresh);
1167
+ root.append(c);
1168
+ }
1169
+ function field(parent, label, type, value) {
1170
+ const wrap = el("label", label, "field");
1171
+ const input = el("input");
1172
+ input.type = type;
1173
+ input.value = String(value);
1174
+ wrap.append(input);
1175
+ parent.append(wrap);
1176
+ return input;
1177
+ }
1178
+ function select(parent, label, options, value) {
1179
+ const wrap = el("label", label, "field"),
1180
+ input = el("select");
1181
+ for (const [id, text] of options) {
1182
+ const option = el("option", text);
1183
+ option.value = id;
1184
+ input.append(option);
1185
+ }
1186
+ input.value = value;
1187
+ wrap.append(input);
1188
+ parent.append(wrap);
1189
+ return input;
1190
+ }
1191
+ function welcome(root, callTool, pref = {}, collection) {
1192
+ root.append(
1193
+ el("h1", "Plan your copy trading in conversation"),
1194
+ el(
1195
+ "p",
1196
+ collection?.questions?.map((q) => q.question).join(" ") ||
1197
+ "Tell your AI your USDC budget and risk preference: conservative, balanced or aggressive.",
1198
+ "muted",
1199
+ ),
1200
+ el(
1201
+ "p",
1202
+ "Assets default to all Crypto and RWA; Perps and Prediction are both allowed unless you choose otherwise. Ride suggests the trader count and allocation. Review all parameters before confirming in Ride.",
1203
+ "muted",
1204
+ ),
1205
+ );
1206
+ const details = el("details");
1207
+ details.append(el("summary", "Optional: answer here"));
1208
+ const budget =
1209
+ pref.budget_usdc == null
1210
+ ? field(details, "Budget · USDC", "text", "")
1211
+ : null;
1212
+ if (budget) budget.inputMode = "decimal";
1213
+ const tolerance =
1214
+ !pref.risk_tolerance && pref.loss_trigger_pct == null
1215
+ ? select(
1216
+ details,
1217
+ "Risk preference",
1218
+ [
1219
+ ["", "Choose"],
1220
+ ["low", "Conservative"],
1221
+ ["medium", "Balanced"],
1222
+ ["high", "Aggressive"],
1223
+ ],
1224
+ "",
1225
+ )
1226
+ : null;
1227
+ const fixed = collection?.missing_fields?.includes("fixed_margin_usdc")
1228
+ ? field(details, "Fixed margin per source entry · USDC", "text", "")
1229
+ : null;
1230
+ const button = el("button", "Continue in conversation");
1231
+ const validate = () => {
1232
+ button.disabled = !!(
1233
+ (budget && !(Number(budget.value) > 0)) ||
1234
+ (tolerance && !tolerance.value) ||
1235
+ (fixed && !(Number(fixed.value) > 0))
1236
+ );
1237
+ };
1238
+ for (const input of [budget, tolerance, fixed].filter(Boolean))
1239
+ input.addEventListener("input", validate);
1240
+ validate();
1241
+ button.addEventListener("click", () =>
1242
+ callTool("recommend_traders", {
1243
+ preferences: {
1244
+ ...(budget ? { budget_usdc: budget.value.trim() } : {}),
1245
+ ...(tolerance ? { risk_tolerance: tolerance.value } : {}),
1246
+ ...(fixed ? { fixed_margin_usdc: fixed.value.trim() } : {}),
1247
+ },
1248
+ }),
1249
+ );
1250
+ details.append(button);
1251
+ root.append(details);
1252
+ }
1253
+ function planEditor(plan, data, callTool, start) {
1254
+ const c = card(
1255
+ plan.preferences.portfolio_kind === "single"
1256
+ ? "Adjust your trader"
1257
+ : "Adjust your basket",
1258
+ ),
1259
+ rows = el("div", null, "grid"),
1260
+ editors = [];
1261
+ const candidates = data.candidates ?? plan.allocations.map((a) => a.trader);
1262
+ const maximumLimits = new Map(
1263
+ (data.candidate_limits ?? []).map((x) => [
1264
+ x.trader_id,
1265
+ x.maximum_leverage_cap,
1266
+ ]),
1267
+ );
1268
+ const dirty = () => {
1269
+ start.dataset.dirty = "true";
1270
+ start.disabled = true;
1271
+ note.textContent = "Changes need recalculation before you can start.";
1272
+ };
1273
+ const note = el(
1274
+ "p",
1275
+ "Perps defaults to following source leverage with no extra user cap. Leave the cap empty for this default, or set 1–40x like the App. Live exchange limits, budget and risk checks still apply. Recalculate changes before confirming.",
1276
+ "muted",
1277
+ );
1278
+ function add(a) {
1279
+ const r = card("Trader allocation");
1280
+ const trader = select(
1281
+ r,
1282
+ "Trader",
1283
+ candidates.map((t) => [
1284
+ t.id,
1285
+ `${t.handle} · ${t.market} · Score ${t.score}`,
1286
+ ]),
1287
+ a.trader.id,
1288
+ );
1289
+ const amount = field(r, "Allocation · USDC", "text", a.amount_usdc);
1290
+ amount.inputMode = "decimal";
1291
+ const leverage = field(
1292
+ r,
1293
+ "Leverage cap · optional",
1294
+ "number",
1295
+ a.leverage_cap ?? "",
1296
+ );
1297
+ leverage.placeholder = "No extra cap · follow source";
1298
+ leverage.min = "1";
1299
+ leverage.max = String(
1300
+ maximumLimits.get(a.trader.id) ??
1301
+ (a.trader.market === "prediction" ? 1 : 40),
1302
+ );
1303
+ const loss = field(r, "Sleeve loss trigger · %", "number", a.stop_loss_pct);
1304
+ loss.min = "1";
1305
+ loss.max = "100";
1306
+ const mode = select(
1307
+ r,
1308
+ "Copy mode",
1309
+ [
1310
+ ["ride_recommends", "Ride Agent recommended"],
1311
+ ["fixed", "Fixed margin"],
1312
+ ["proportional", "Proportional"],
1313
+ ],
1314
+ a.copy_mode ?? "ride_recommends",
1315
+ ),
1316
+ fixed = field(
1317
+ r,
1318
+ "Fixed margin · USDC",
1319
+ "text",
1320
+ a.fixed_margin_usdc ?? "10",
1321
+ ),
1322
+ multiplier = field(
1323
+ r,
1324
+ "Proportional multiplier",
1325
+ "number",
1326
+ a.proportional_multiplier ?? 1,
1327
+ ),
1328
+ policy = select(
1329
+ r,
1330
+ "Loss policy",
1331
+ [
1332
+ ["exit", "Exit at threshold"],
1333
+ ["pause_then_exit", "Pause then emergency exit"],
1334
+ ],
1335
+ a.loss_policy ?? "exit",
1336
+ ),
1337
+ emergency = field(
1338
+ r,
1339
+ "Emergency exit · %",
1340
+ "number",
1341
+ a.emergency_exit_pct ?? 70,
1342
+ );
1343
+ const editor = {
1344
+ r,
1345
+ trader,
1346
+ amount,
1347
+ leverage,
1348
+ loss,
1349
+ mode,
1350
+ fixed,
1351
+ multiplier,
1352
+ policy,
1353
+ emergency,
1354
+ };
1355
+ editors.push(editor);
1356
+ r.addEventListener("input", dirty);
1357
+ trader.addEventListener("change", () => {
1358
+ const selected = candidates.find((t) => t.id === trader.value);
1359
+ const maximum =
1360
+ maximumLimits.get(trader.value) ??
1361
+ (selected?.market === "prediction" ? 1 : 40);
1362
+ leverage.max = String(maximum);
1363
+ if (selected?.market === "prediction" || leverage.value.trim() !== "")
1364
+ leverage.value = String(Math.min(Number(leverage.value), maximum));
1365
+ dirty();
1366
+ });
1367
+ const remove = el("button", "Remove", "secondary");
1368
+ remove.addEventListener("click", () => {
1369
+ if (
1370
+ editors.length <=
1371
+ (plan.preferences.portfolio_kind === "single"
1372
+ ? 1
1373
+ : plan.preferences.portfolio_kind === "basket"
1374
+ ? 2
1375
+ : 3)
1376
+ ) {
1377
+ note.textContent = "Keep the minimum number of traders for this plan.";
1378
+ return;
1379
+ }
1380
+ editors.splice(editors.indexOf(editor), 1);
1381
+ r.remove();
1382
+ dirty();
1383
+ });
1384
+ r.append(remove);
1385
+ rows.append(r);
1386
+ }
1387
+ plan.allocations.forEach(add);
1388
+ c.append(rows);
1389
+ const plus = el("button", "Add another trader", "secondary");
1390
+ plus.addEventListener("click", () => {
1391
+ if (
1392
+ editors.length >=
1393
+ (plan.preferences.portfolio_kind === "single"
1394
+ ? 1
1395
+ : plan.preferences.portfolio_kind === "basket"
1396
+ ? 8
1397
+ : 6)
1398
+ ) {
1399
+ note.textContent = "This plan has reached its trader limit.";
1400
+ return;
1401
+ }
1402
+ const trader = candidates.find(
1403
+ (t) => !editors.some((e) => e.trader.value === t.id),
1404
+ );
1405
+ if (!trader) {
1406
+ note.textContent = "No additional eligible trader is available.";
1407
+ return;
1408
+ }
1409
+ add({
1410
+ trader,
1411
+ amount_usdc: trader.market === "perps" ? "" : "50",
1412
+ leverage_cap: trader.market === "prediction" ? 1 : null,
1413
+ stop_loss_pct: 30,
1414
+ });
1415
+ dirty();
1416
+ });
1417
+ const recalc = el("button", "Recalculate and review");
1418
+ recalc.addEventListener("click", () =>
1419
+ callTool("recalculate_plan", {
1420
+ plan_id: plan.id,
1421
+ preferences: plan.preferences,
1422
+ allocations: editors.map((e) => ({
1423
+ trader_id: e.trader.value,
1424
+ amount_usdc: e.amount.value.trim(),
1425
+ leverage_cap:
1426
+ e.leverage.value.trim() === "" ? null : Number(e.leverage.value),
1427
+ stop_loss_pct: Number(e.loss.value),
1428
+ copy_mode: e.mode.value,
1429
+ ...(e.mode.value === "fixed"
1430
+ ? { fixed_margin_usdc: e.fixed.value.trim() }
1431
+ : { proportional_multiplier: Number(e.multiplier.value) }),
1432
+ loss_policy: e.policy.value,
1433
+ ...(e.policy.value === "pause_then_exit"
1434
+ ? { emergency_exit_pct: Number(e.emergency.value) }
1435
+ : {}),
1436
+ })),
1437
+ }),
1438
+ );
1439
+ const actions = el("div", null, "actions");
1440
+ actions.append(plus, recalc);
1441
+ c.append(actions, note);
1442
+ const details = el("details");
1443
+ details.append(el("summary", "Edit allocations and traders"), c);
1444
+ return details;
1445
+ }
1446
+ function table(parent, headers, rows) {
1447
+ const wrap = el("div", null, "table-wrap"),
1448
+ t = el("table"),
1449
+ head = el("tr");
1450
+ headers.forEach((v) => head.append(el("th", v)));
1451
+ t.append(head);
1452
+ for (const values of rows) {
1453
+ const r = el("tr");
1454
+ values.forEach((v) => r.append(el("td", val(v))));
1455
+ t.append(r);
1456
+ }
1457
+ wrap.append(t);
1458
+ parent.append(wrap);
1459
+ }
1460
+ function renderPerformance(root, p, callTool) {
1461
+ root.append(el("h1", "Performance history"));
1462
+ const c = card(`${p.period} · ${p.coverage}`);
1463
+ c.append(
1464
+ row("Observed-period net profit", usdc(p.net_profit_usdc)),
1465
+ row("Inception net profit", usdc(p.inception_net_profit_usdc)),
1466
+ );
1467
+ const windowText = (w) =>
1468
+ w
1469
+ ? `${new Date(w.start_at).toLocaleString()} — ${new Date(w.end_at).toLocaleString()}`
1470
+ : null;
1471
+ c.append(
1472
+ row("Requested window", windowText(p.requested_window)),
1473
+ row("Observed window", windowText(p.observed_window)),
1474
+ );
1475
+ if (p.series_truncated)
1476
+ c.append(
1477
+ el(
1478
+ "p",
1479
+ "Displaying the latest 366 daily marks; older snapshots remain stored.",
1480
+ "muted",
1481
+ ),
1482
+ );
1483
+ if (p.missing_reason) c.append(el("p", p.missing_reason, "warning"));
1484
+ const actions = el("div", null, "actions");
1485
+ for (const period of ["daily", "weekly", "inception"]) {
1486
+ const b = el("button", period, "secondary");
1487
+ b.addEventListener("click", () =>
1488
+ callTool("get_performance", { portfolio_id: p.portfolio_id, period }),
1489
+ );
1490
+ actions.append(b);
1491
+ }
1492
+ c.append(actions);
1493
+ const chart = el("div", null, "performance-chart");
1494
+ const chartActions = el("div", null, "actions");
1495
+ for (const [metric, label] of [
1496
+ ["net_profit_usdc", "Cumulative net profit"],
1497
+ ["account_value_usdc", "Account value"],
1498
+ ]) {
1499
+ const button = el("button", label, "secondary");
1500
+ button.addEventListener("click", () =>
1501
+ drawPerformance(chart, p.points, metric, label),
1502
+ );
1503
+ chartActions.append(button);
1504
+ }
1505
+ c.append(chartActions, chart);
1506
+ drawPerformance(chart, p.points, "net_profit_usdc", "Cumulative net profit");
1507
+ table(
1508
+ c,
1509
+ ["Observed at", "Net profit · USDC", "Accounting"],
1510
+ p.points.map((x) => [
1511
+ new Date(x.at).toLocaleString(),
1512
+ x.net_profit_usdc,
1513
+ x.reconciliation,
1514
+ ]),
1515
+ );
1516
+ const latest = p.points.at(-1);
1517
+ if (latest)
1518
+ table(
1519
+ c,
1520
+ ["Historical copy", "State", "Net contribution · USDC"],
1521
+ latest.sleeves.map((s) => [s.handle, s.state, s.net_profit_usdc]),
1522
+ );
1523
+ if (p.recent_fills?.length)
1524
+ table(
1525
+ c,
1526
+ [
1527
+ "Time",
1528
+ "Trader",
1529
+ "Asset",
1530
+ "Quantity",
1531
+ "Fill price",
1532
+ "Realized · USDC",
1533
+ "Fee · USDC",
1534
+ ],
1535
+ p.recent_fills.map((f) => [
1536
+ new Date(f.at).toLocaleString(),
1537
+ f.trader,
1538
+ f.asset,
1539
+ f.quantity,
1540
+ f.price,
1541
+ f.realized_usdc,
1542
+ f.fee_usdc,
1543
+ ]),
1544
+ );
1545
+ c.append(
1546
+ el(
1547
+ "p",
1548
+ "Historical points keep their original trader identities. External deposits and withdrawals are excluded from net profit; unobserved periods stay unavailable.",
1549
+ "muted",
1550
+ ),
1551
+ );
1552
+ root.append(c);
1553
+ }
1554
+
1555
+ function drawPerformance(root, points, metric, label) {
1556
+ root.replaceChildren(el("h3", label));
1557
+ const valid = points.filter(
1558
+ (p) => p[metric] != null && Number.isFinite(Number(p[metric])),
1559
+ );
1560
+ if (!valid.length) {
1561
+ root.append(el("p", "Verified history is unavailable.", "muted"));
1562
+ return;
1563
+ }
1564
+ const ns = "http://www.w3.org/2000/svg",
1565
+ svg = document.createElementNS(ns, "svg");
1566
+ svg.setAttribute("viewBox", "0 0 640 220");
1567
+ svg.setAttribute("role", "img");
1568
+ svg.setAttribute("aria-label", `${label} in USDC`);
1569
+ const min = Math.min(...valid.map((p) => Number(p[metric]))),
1570
+ max = Math.max(...valid.map((p) => Number(p[metric]))),
1571
+ range = Math.max(1, max - min);
1572
+ const first = points[0].at,
1573
+ last = points.at(-1).at,
1574
+ span = Math.max(1, last - first),
1575
+ segments = [];
1576
+ let segment = [];
1577
+ let previous = null;
1578
+ for (const p of points) {
1579
+ if (p[metric] == null || !Number.isFinite(Number(p[metric]))) {
1580
+ if (segment.length) segments.push(segment);
1581
+ segment = [];
1582
+ previous = null;
1583
+ continue;
1584
+ }
1585
+ if (previous && p.at - previous.at > 2 * 86400000) {
1586
+ if (segment.length) segments.push(segment);
1587
+ segment = [];
1588
+ }
1589
+ const x = 62 + ((p.at - first) / span) * 558,
1590
+ y = 178 - ((Number(p[metric]) - min) / range) * 140;
1591
+ segment.push([x, y]);
1592
+ previous = p;
1593
+ const dot = document.createElementNS(ns, "circle");
1594
+ dot.setAttribute("cx", x);
1595
+ dot.setAttribute("cy", y);
1596
+ dot.setAttribute("r", "3");
1597
+ dot.setAttribute("fill", "#8c740c");
1598
+ const title = document.createElementNS(ns, "title");
1599
+ title.textContent = `${new Date(p.at).toLocaleDateString()}: ${p[metric]} USDC`;
1600
+ dot.append(title);
1601
+ svg.append(dot);
1602
+ }
1603
+ if (segment.length) segments.push(segment);
1604
+ for (const values of segments) {
1605
+ const path = document.createElementNS(ns, "polyline");
1606
+ path.setAttribute("points", values.map((v) => v.join(",")).join(" "));
1607
+ path.setAttribute("fill", "none");
1608
+ path.setAttribute("stroke", "#c3a017");
1609
+ path.setAttribute("stroke-width", "2");
1610
+ svg.append(path);
1611
+ }
1612
+ for (const [value, y] of [
1613
+ [max, 38],
1614
+ [min, 178],
1615
+ ]) {
1616
+ const text = document.createElementNS(ns, "text");
1617
+ text.setAttribute("x", "4");
1618
+ text.setAttribute("y", y);
1619
+ text.setAttribute("font-size", "11");
1620
+ text.textContent = value.toFixed(2);
1621
+ svg.append(text);
1622
+ }
1623
+ root.append(
1624
+ svg,
1625
+ el(
1626
+ "p",
1627
+ metric === "account_value_usdc"
1628
+ ? "Account value includes external funding. Use net profit to assess trading performance."
1629
+ : "Net profit excludes external deposits and withdrawals. Missing periods remain gaps.",
1630
+ "muted",
1631
+ ),
1632
+ );
1633
+ }
1634
+
1635
+ function renderCompatibility(root, plan, data, callTool, start) {
1636
+ const report = data.compatibility;
1637
+ if (!report) return;
1638
+ const c = card("Portfolio compatibility check");
1639
+ c.append(
1640
+ row("Overlap flags", report.warnings),
1641
+ el("p", report.missing_evidence, "muted"),
1642
+ );
1643
+ table(
1644
+ c,
1645
+ ["Traders", "Shared assets", "Same direction", "Similar holding period"],
1646
+ report.pairs.map((p) => [
1647
+ p.traders
1648
+ .map(
1649
+ (id) =>
1650
+ plan.allocations.find((a) => a.trader.id === id)?.trader.handle ??
1651
+ id,
1652
+ )
1653
+ .join(" / "),
1654
+ p.shared_assets.join(" / "),
1655
+ p.same_direction,
1656
+ p.similar_holding_period,
1657
+ ]),
1658
+ );
1659
+ if (report.warnings && plan.preferences.portfolio_kind) {
1660
+ const ack = field(
1661
+ c,
1662
+ "Keep this selection after reviewing its overlap warnings",
1663
+ "checkbox",
1664
+ "",
1665
+ );
1666
+ start.disabled = true;
1667
+ ack.addEventListener("change", () => {
1668
+ start.disabled =
1669
+ !ack.checked ||
1670
+ start.dataset.dirty === "true" ||
1671
+ plan.expires_at <= Date.now();
1672
+ });
1673
+ start.dataset.compatibility = report.fingerprint;
1674
+ }
1675
+ for (const suggestion of data.alternatives ?? []) {
1676
+ const b = el(
1677
+ "button",
1678
+ `Replace ${plan.allocations.find((a) => a.trader.id === suggestion.outgoing_trader_id)?.trader.handle} with ${suggestion.incoming_trader.handle} · ${suggestion.fewer_flags} fewer flags`,
1679
+ "secondary",
1680
+ );
1681
+ b.addEventListener("click", () =>
1682
+ callTool("recalculate_plan", {
1683
+ plan_id: plan.id,
1684
+ allocations: plan.allocations.map((a) => ({
1685
+ trader_id:
1686
+ a.trader.id === suggestion.outgoing_trader_id
1687
+ ? suggestion.incoming_trader.id
1688
+ : a.trader.id,
1689
+ amount_usdc: a.amount_usdc,
1690
+ leverage_cap: a.leverage_cap,
1691
+ stop_loss_pct: a.stop_loss_pct,
1692
+ ...copyParameters(a),
1693
+ })),
1694
+ }),
1695
+ );
1696
+ c.append(b);
1697
+ }
1698
+ root.append(c);
1699
+ }
1700
+ const copyParameters = (a) =>
1701
+ Object.fromEntries(
1702
+ [
1703
+ "copy_mode",
1704
+ "fixed_margin_usdc",
1705
+ "proportional_multiplier",
1706
+ "loss_policy",
1707
+ "emergency_exit_pct",
1708
+ ]
1709
+ .filter((k) => a[k] != null)
1710
+ .map((k) => [k, a[k]]),
1711
+ );
1712
+
1713
+ function addToPlan(plan, trader, data, callTool) {
1714
+ if (plan.allocations.some((a) => a.trader.id === trader.id)) return;
1715
+ if (plan.preferences.portfolio_kind === "single") {
1716
+ const allocation = plan.allocations[0];
1717
+ const limit =
1718
+ data.candidate_limits?.find((x) => x.trader_id === trader.id)
1719
+ ?.maximum_leverage_cap ?? (trader.market === "prediction" ? 1 : 40);
1720
+ callTool("recalculate_plan", {
1721
+ plan_id: plan.id,
1722
+ preferences: plan.preferences,
1723
+ allocations: [
1724
+ {
1725
+ trader_id: trader.id,
1726
+ amount_usdc: allocation.amount_usdc,
1727
+ leverage_cap:
1728
+ trader.market === "prediction"
1729
+ ? 1
1730
+ : allocation.leverage_cap == null
1731
+ ? null
1732
+ : Math.min(allocation.leverage_cap, limit),
1733
+ stop_loss_pct: allocation.stop_loss_pct,
1734
+ ...copyParameters(allocation),
1735
+ },
1736
+ ],
1737
+ });
1738
+ return;
1739
+ }
1740
+ const max =
1741
+ plan.preferences.portfolio_kind === "single"
1742
+ ? 1
1743
+ : plan.preferences.portfolio_kind === "basket"
1744
+ ? 8
1745
+ : 6;
1746
+ if (plan.allocations.length >= max) return;
1747
+ const values = [
1748
+ ...plan.allocations,
1749
+ {
1750
+ trader,
1751
+ leverage_cap: trader.market === "prediction" ? 1 : null,
1752
+ stop_loss_pct: 30,
1753
+ ...copyParameters(plan.preferences),
1754
+ },
1755
+ ];
1756
+ const [whole, fraction = ""] = String(plan.preferences.budget_usdc).split(
1757
+ ".",
1758
+ ),
1759
+ total = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0")),
1760
+ count = BigInt(values.length),
1761
+ base = total / count,
1762
+ remainder = total % count;
1763
+ callTool("recalculate_plan", {
1764
+ plan_id: plan.id,
1765
+ preferences: plan.preferences,
1766
+ allocations: values.map((a, i) => {
1767
+ const amount = base + (BigInt(i) < remainder ? 1n : 0n);
1768
+ return {
1769
+ trader_id: a.trader.id,
1770
+ amount_usdc: `${amount / 1000000n}.${String(amount % 1000000n).padStart(6, "0")}`,
1771
+ leverage_cap: a.leverage_cap,
1772
+ stop_loss_pct: a.stop_loss_pct,
1773
+ ...copyParameters(a),
1774
+ };
1775
+ }),
1776
+ });
1777
+ }
1778
+
1779
+ function renderDepositOptions(root, options, callTool) {
1780
+ const c = card("Deposit into your Ride account");
1781
+ const market = select(
1782
+ c,
1783
+ "Destination",
1784
+ [
1785
+ ["perps", "Trading account"],
1786
+ ["prediction", "Prediction account"],
1787
+ ],
1788
+ "perps",
1789
+ );
1790
+ const asset = select(
1791
+ c,
1792
+ "Asset",
1793
+ [
1794
+ ["USDC", "USDC"],
1795
+ ["USDT", "USDT"],
1796
+ ],
1797
+ "USDC",
1798
+ );
1799
+ const network = select(c, "Network", [], "");
1800
+ const update = () => {
1801
+ network.replaceChildren();
1802
+ for (const route of options.routes.filter(
1803
+ (r) => r.market === market.value && r.asset === asset.value,
1804
+ )) {
1805
+ const o = el("option", route.network);
1806
+ o.value = route.network;
1807
+ network.append(o);
1808
+ }
1809
+ b.disabled = !network.options.length;
1810
+ note.textContent = network.options.length
1811
+ ? "Only currently supported routes are shown."
1812
+ : "No verified route for this account and asset. Choose another option.";
1813
+ };
1814
+ const amount = field(
1815
+ c,
1816
+ "Amount · optional, used for minimum checks",
1817
+ "text",
1818
+ "",
1819
+ );
1820
+ const b = el("button", "Get my deposit address and QR"),
1821
+ note = el("p", null, "muted");
1822
+ b.addEventListener("click", () =>
1823
+ callTool("get_deposit_address", {
1824
+ market: market.value,
1825
+ asset: asset.value,
1826
+ network: network.value,
1827
+ ...(amount.value.trim() ? { amount_usdc: amount.value.trim() } : {}),
1828
+ }),
1829
+ );
1830
+ market.addEventListener("change", update);
1831
+ asset.addEventListener("change", update);
1832
+ update();
1833
+ c.append(note, b);
1834
+ for (const n of options.notes ?? []) c.append(el("p", n, "muted"));
1835
+ root.append(c);
1836
+ }
1837
+ function renderDeposit(root, deposit, callTool) {
1838
+ const c = card(`Deposit ${deposit.asset} · ${deposit.network}`);
1839
+ c.append(
1840
+ row("Destination", deposit.destination),
1841
+ row("Network", deposit.network),
1842
+ row("Asset", deposit.asset),
1843
+ );
1844
+ const image = el("img");
1845
+ image.alt = `Receiving address QR for ${deposit.asset} on ${deposit.network}`;
1846
+ image.width = 240;
1847
+ image.height = 240;
1848
+ image.src = deposit.qr_data_url;
1849
+ image.className = "deposit-qr";
1850
+ c.append(
1851
+ image,
1852
+ el("code", deposit.address, "deposit-address"),
1853
+ el("p", deposit.instructions, "warning"),
1854
+ );
1855
+ const b = el("button", "Check deposit status");
1856
+ b.addEventListener("click", () =>
1857
+ callTool("check_deposit_status", { deposit_id: deposit.id }),
1858
+ );
1859
+ c.append(b);
1860
+ if (deposit.provider === "wallet") {
1861
+ const amount = field(
1862
+ c,
1863
+ "Amount to move into trading · USDC",
1864
+ "text",
1865
+ deposit.amount_usdc ?? "",
1866
+ );
1867
+ amount.inputMode = "decimal";
1868
+ const prepare = el("button", "Review trading-account funding", "secondary");
1869
+ prepare.addEventListener("click", () =>
1870
+ callTool("prepare_deposit", {
1871
+ deposit_id: deposit.id,
1872
+ amount_usdc: amount.value.trim(),
1873
+ }),
1874
+ );
1875
+ const validate = () => {
1876
+ prepare.disabled =
1877
+ !/^\d+(\.\d{1,6})?$/.test(amount.value.trim()) ||
1878
+ Number(amount.value) < 5;
1879
+ };
1880
+ amount.addEventListener("input", validate);
1881
+ validate();
1882
+ c.append(prepare);
1883
+ }
1884
+ root.append(c);
1885
+ }
1886
+ function renderDiagnostic(root, d) {
1887
+ root.append(el("h1", `Copy diagnosis · ${d.trader}`));
1888
+ const c = card("Evidence in the same time window");
1889
+ c.append(
1890
+ row(
1891
+ "Window",
1892
+ `${new Date(d.window.start_at).toLocaleString()} — ${new Date(d.window.end_at).toLocaleString()}`,
1893
+ ),
1894
+ row("Source coverage", d.source_coverage),
1895
+ row("Follower coverage", d.follower_coverage),
1896
+ row("Attributed fees", usdc(d.fees_usdc)),
1897
+ );
1898
+ c.append(
1899
+ row(
1900
+ "Follower verified through",
1901
+ d.follower_verified_through
1902
+ ? new Date(d.follower_verified_through).toLocaleString()
1903
+ : null,
1904
+ ),
1905
+ );
1906
+ c.append(el("h2", "Source fills"));
1907
+ table(
1908
+ c,
1909
+ ["Time", "Asset", "Quantity", "Price", "Fee · USDC"],
1910
+ d.source_fills.map((f) => [
1911
+ new Date(f.at).toLocaleString(),
1912
+ f.key,
1913
+ f.quantity,
1914
+ f.price,
1915
+ f.fee_usdc,
1916
+ ]),
1917
+ );
1918
+ c.append(el("h2", "Your attributed fills"));
1919
+ table(
1920
+ c,
1921
+ [
1922
+ "Time",
1923
+ "Asset",
1924
+ "Quantity",
1925
+ "Price",
1926
+ "Fee · USDC",
1927
+ "Leverage cap at fill",
1928
+ ],
1929
+ d.follower_fills.map((r) => [
1930
+ new Date(r.fill.at).toLocaleString(),
1931
+ r.fill.key,
1932
+ r.quantity,
1933
+ r.fill.price,
1934
+ r.fee_usdc,
1935
+ r.config?.leverage_cap ??
1936
+ (r.config?.leverage_mode === "follow_source" ? "No extra cap" : null),
1937
+ ]),
1938
+ );
1939
+ c.append(el("h2", "Entry decisions"));
1940
+ table(
1941
+ c,
1942
+ ["Time", "Asset", "Reason", "Target quantity", "Leverage cap"],
1943
+ d.decisions.map((x) => [
1944
+ new Date(x.at).toLocaleString(),
1945
+ x.key,
1946
+ x.reason,
1947
+ x.target_quantity,
1948
+ x.config.leverage_cap ??
1949
+ (x.config.leverage_mode === "follow_source" ? "No extra cap" : null),
1950
+ ]),
1951
+ );
1952
+ c.append(el("h2", "Execution and funding evidence"));
1953
+ table(
1954
+ c,
1955
+ ["Asset", "Execution state", "Error"],
1956
+ (d.executions ?? []).map((e) => [e.key, e.status, e.error]),
1957
+ );
1958
+ table(
1959
+ c,
1960
+ ["Funding time", "Asset", "Amount · USDC"],
1961
+ (d.funding ?? []).map((f) => [
1962
+ new Date(f.at).toLocaleString(),
1963
+ f.key,
1964
+ f.amount_usdc,
1965
+ ]),
1966
+ );
1967
+ for (const event of d.events ?? [])
1968
+ c.append(el("p", `${event.kind} · ${event.message}`, "muted"));
1969
+ c.append(el("p", d.missing_reason, "warning"));
1970
+ if (d.source_missing_reason)
1971
+ c.append(el("p", d.source_missing_reason, "muted"));
1972
+ root.append(c);
1973
+ }
1974
+ function renderNotifications(root, d, callTool) {
1975
+ root.append(el("h1", "Daily Ride Agent report"));
1976
+ const p = d.notification_preferences,
1977
+ c = card("Notification settings");
1978
+ const enabled = select(
1979
+ c,
1980
+ "Daily report",
1981
+ [
1982
+ ["false", "Off"],
1983
+ ["true", "On · Ride App push"],
1984
+ ],
1985
+ String(p.daily_digest),
1986
+ );
1987
+ const time = field(c, "Local delivery time", "time", p.local_time);
1988
+ const zone = field(c, "Timezone", "text", p.timezone);
1989
+ c.append(
1990
+ row("Push delivery", d.delivery.status),
1991
+ row("Device registered", d.delivery.device_registered ? "Yes" : "No"),
1992
+ );
1993
+ const b = el("button", "Save notification settings");
1994
+ b.addEventListener("click", () =>
1995
+ callTool("set_notification_preferences", {
1996
+ daily_digest: enabled.value === "true",
1997
+ local_time: time.value,
1998
+ timezone: zone.value.trim(),
1999
+ }),
2000
+ );
2001
+ c.append(
2002
+ b,
2003
+ el(
2004
+ "p",
2005
+ "Reports run on the Ride server. Critical risk alerts remain enabled. A registered Ride App device and configured push service are required for delivery.",
2006
+ "muted",
2007
+ ),
2008
+ );
2009
+ root.append(c);
2010
+ }
2011
+ function renderNestedPortfolio(root, p) {
2012
+ root.append(sleeveCards(p.sleeves ?? []));
2013
+ }
2014
+
2015
+ /*INLINE_CONFIRM*/const config = /*CONFIG*/{};
2016
+ const status = document.getElementById("status");
2017
+ const content = document.getElementById("content");
2018
+ const loginView = document.getElementById("login");
2019
+ const actions = document.getElementById("actions");
2020
+ const signIn = document.getElementById("sign-in");
2021
+ const embedded = config.embedded === true;
2022
+ let client,
2023
+ initialization,
2024
+ csrf,
2025
+ proposal,
2026
+ popup,
2027
+ challenge,
2028
+ submitting = false;
2029
+ loginView.hidden = true;
2030
+ document.getElementById("approve").textContent = config.demo
2031
+ ? "确认模拟"
2032
+ : "确认本次操作";
2033
+ document.getElementById("reject").textContent = config.demo
2034
+ ? "取消模拟"
2035
+ : "取消方案";
2036
+ const showError = (e) => {
2037
+ status.textContent = e?.message ?? String(e);
2038
+ status.className = "warning error";
2039
+ };
2040
+ async function init() {
2041
+ if (client) return client;
2042
+ if (initialization) return initialization;
2043
+ if (!config.privy_app_id) throw Error("Ride Agent sign-in is unavailable.");
2044
+ const { default: Privy, LocalStorage } = await import(config.privy_sdk);
2045
+ initialization = (async () => {
2046
+ const next = new Privy({
2047
+ appId: config.privy_app_id,
2048
+ ...(config.privy_client_id ? { clientId: config.privy_client_id } : {}),
2049
+ storage: new LocalStorage(),
2050
+ });
2051
+ await next.initialize();
2052
+ client = next;
2053
+ return next;
2054
+ })();
2055
+ try {
2056
+ return await initialization;
2057
+ } finally {
2058
+ initialization = undefined;
2059
+ }
2060
+ }
2061
+ async function call(operation, body = {}) {
2062
+ const response = await fetch(`/agent/human/${operation}`, {
2063
+ method: "POST",
2064
+ credentials: "same-origin",
2065
+ headers: {
2066
+ "Content-Type": "application/json",
2067
+ ...(embedded ? { "X-Ride-Embedded": "1" } : {}),
2068
+ ...(csrf ? { "X-Ride-CSRF": csrf } : {}),
2069
+ },
2070
+ body: JSON.stringify(body),
2071
+ signal: AbortSignal.timeout(30000),
2072
+ });
2073
+ const payload = await response.json();
2074
+ if (!response.ok) {
2075
+ const error = new Error(payload.error?.message ?? "Ride Agent request failed.");
2076
+ error.code = payload.error?.code;
2077
+ throw error;
2078
+ }
2079
+ return payload;
2080
+ }
2081
+ const authError = (e) =>
2082
+ e?.code === "AUTH_REQUIRED" || e?.code === "CSRF_REQUIRED";
2083
+ function announce(state) {
2084
+ // No token, CSRF, handoff ticket or signing challenge crosses into the host.
2085
+ if (embedded)
2086
+ window.parent.postMessage(
2087
+ {
2088
+ type: "ride:confirmation-state",
2089
+ proposal_id: config.proposal_id,
2090
+ status: state,
2091
+ height: Math.min(1400, Math.max(420, document.body.scrollHeight + 24)),
2092
+ },
2093
+ "*",
2094
+ );
2095
+ }
2096
+ async function loadProposal() {
2097
+ const value = await call("proposal", { proposal_id: config.proposal_id });
2098
+ proposal = value.data.proposal;
2099
+ render(content, value);
2100
+ content.querySelectorAll("button").forEach((b) => b.remove());
2101
+ const expired = proposal.expires_at <= Date.now();
2102
+ actions.hidden = proposal.status !== "requires_confirmation" || expired;
2103
+ actions.querySelectorAll("button").forEach((b) => (b.disabled = submitting));
2104
+ status.className = "";
2105
+ status.textContent =
2106
+ expired && proposal.status === "requires_confirmation"
2107
+ ? "方案已过期,请在聊天中刷新方案。"
2108
+ : "";
2109
+ announce(proposal.status);
2110
+ }
2111
+ async function ready() {
2112
+ loginView.hidden = true;
2113
+ const url = new URL(location.href);
2114
+ const nonce = new URLSearchParams(url.hash.slice(1)).get("challenge");
2115
+ if (!embedded && url.searchParams.get("card_login") === "1") {
2116
+ if (!window.opener || !/^[a-f0-9]{64}$/.test(nonce ?? ""))
2117
+ throw Error("连接窗口已失效,请回到卡片重新连接。无需确认任何交易。");
2118
+ const { ticket } = await call("embed-ticket", {
2119
+ proposal_id: config.proposal_id,
2120
+ challenge: nonce,
2121
+ });
2122
+ window.opener.postMessage(
2123
+ {
2124
+ type: "ride:human-session",
2125
+ ticket,
2126
+ challenge: nonce,
2127
+ proposal_id: config.proposal_id,
2128
+ },
2129
+ location.origin,
2130
+ );
2131
+ status.textContent =
2132
+ "Ride 已连接,请返回聊天卡片确认操作。此窗口没有提交交易。";
2133
+ return;
2134
+ }
2135
+ await loadProposal();
2136
+ }
2137
+ async function finish(session) {
2138
+ const c = await init();
2139
+ const user = session?.user ?? (await c.user.get()).user;
2140
+ if (!user?.id) return false;
2141
+ const accounts = user.linkedAccounts ?? user.linked_accounts ?? [];
2142
+ const wallet = accounts.find(
2143
+ (x) =>
2144
+ x.type === "wallet" &&
2145
+ (x.chainType ?? x.chain_type) === "ethereum" &&
2146
+ (x.walletClientType ?? x.wallet_client_type) === "privy",
2147
+ );
2148
+ if (!wallet)
2149
+ throw Error("Set up your Ride wallet in the App before confirming.");
2150
+ const token = await c.getAccessToken();
2151
+ if (!token) return false;
2152
+ const login = await call("login", {
2153
+ privyId: user.id,
2154
+ privyAccessToken: token,
2155
+ walletAddress: wallet.address,
2156
+ privyWalletId: wallet.id,
2157
+ authProvider: "GOOGLE",
2158
+ accessCode: sessionStorage.getItem("ride:agent:access-code") ?? undefined,
2159
+ });
2160
+ csrf = login.csrf;
2161
+ sessionStorage.removeItem("ride:agent:access-code");
2162
+ await ready();
2163
+ return true;
2164
+ }
2165
+ function showLogin() {
2166
+ loginView.hidden = false;
2167
+ actions.hidden = true;
2168
+ if (embedded) {
2169
+ loginView.querySelector("h1").textContent = "连接 Ride 后在此确认";
2170
+ loginView.querySelector("p").textContent =
2171
+ "首次连接会打开 Ride 登录窗口;连接有效时,后续卡片直接显示方案。";
2172
+ signIn.textContent = "连接 Ride";
2173
+ document.getElementById("access-code").parentElement.hidden = true;
2174
+ }
2175
+ announce("sign_in_required");
2176
+ }
2177
+ signIn.onclick = async () => {
2178
+ if (embedded) {
2179
+ challenge = [...crypto.getRandomValues(new Uint8Array(32))]
2180
+ .map((n) => n.toString(16).padStart(2, "0"))
2181
+ .join("");
2182
+ const url = new URL(
2183
+ `/agent/confirm/${config.proposal_id}`,
2184
+ location.origin,
2185
+ );
2186
+ url.searchParams.set("card_login", "1");
2187
+ url.hash = new URLSearchParams({ challenge }).toString();
2188
+ popup = window.open(
2189
+ url.href,
2190
+ "ride-human-sign-in",
2191
+ "popup,width=540,height=720",
2192
+ );
2193
+ if (!popup)
2194
+ showError(
2195
+ Error("此客户端未允许登录窗口,请使用卡片下方的 Ride 页面入口。"),
2196
+ );
2197
+ return;
2198
+ }
2199
+ signIn.disabled = true;
2200
+ try {
2201
+ const c = await init();
2202
+ sessionStorage.setItem("ride:mcp:oauth_authorize_url", location.href);
2203
+ sessionStorage.setItem(
2204
+ "ride:agent:access-code",
2205
+ document.getElementById("access-code").value,
2206
+ );
2207
+ const out = await c.auth.oauth.generateURL(
2208
+ "google",
2209
+ `${location.origin}/auth/privy/oauth/callback`,
2210
+ );
2211
+ location.assign(out.url);
2212
+ } catch (e) {
2213
+ showError(e);
2214
+ signIn.disabled = false;
2215
+ }
2216
+ };
2217
+ window.addEventListener("message", async (event) => {
2218
+ if (
2219
+ !embedded ||
2220
+ event.origin !== location.origin ||
2221
+ !popup ||
2222
+ event.source !== popup
2223
+ )
2224
+ return;
2225
+ const msg = event.data;
2226
+ if (
2227
+ msg?.type !== "ride:human-session" ||
2228
+ msg.challenge !== challenge ||
2229
+ msg.proposal_id !== config.proposal_id
2230
+ )
2231
+ return;
2232
+ try {
2233
+ const session = await call("embed-session", {
2234
+ ticket: msg.ticket,
2235
+ challenge,
2236
+ proposal_id: config.proposal_id,
2237
+ });
2238
+ csrf = session.csrf;
2239
+ popup.close();
2240
+ popup = undefined;
2241
+ challenge = undefined;
2242
+ // Prove the partitioned cookie was retained before offering approval.
2243
+ await call("session");
2244
+ await ready();
2245
+ } catch (e) {
2246
+ showError(
2247
+ authError(e)
2248
+ ? Error("此客户端没有保留卡片登录状态,请使用 Ride 页面入口。")
2249
+ : e,
2250
+ );
2251
+ showLogin();
2252
+ }
2253
+ });
2254
+ for (const [id, operation] of [
2255
+ ["approve", "confirm"],
2256
+ ["reject", "reject"],
2257
+ ]) {
2258
+ document.getElementById(id).onclick = async () => {
2259
+ if (submitting || !proposal || proposal.status !== "requires_confirmation")
2260
+ return;
2261
+ if (proposal.expires_at <= Date.now()) {
2262
+ actions.hidden = true;
2263
+ showError(Error("方案已过期,请在聊天中刷新方案。"));
2264
+ announce("expired");
2265
+ return;
2266
+ }
2267
+ submitting = true;
2268
+ actions.querySelectorAll("button").forEach((b) => (b.disabled = true));
2269
+ try {
2270
+ const value = await call(operation, {
2271
+ proposal_id: config.proposal_id,
2272
+ ...(operation === "confirm"
2273
+ ? { preview_hash: proposal.preview_hash }
2274
+ : {}),
2275
+ });
2276
+ render(content, value);
2277
+ actions.hidden = true;
2278
+ status.textContent =
2279
+ operation === "confirm"
2280
+ ? "已确认并进入处理队列;成交和结算结果仍需核对。"
2281
+ : "已取消此方案。";
2282
+ announce(
2283
+ value.data?.proposal?.status ??
2284
+ (operation === "confirm" ? "queued" : "rejected"),
2285
+ );
2286
+ } catch (e) {
2287
+ showError(
2288
+ Error(`${e.message} 提交结果待核实,请刷新本方案状态;不要重复提交。`),
2289
+ );
2290
+ const refresh = el("button", "刷新本方案状态", "secondary");
2291
+ refresh.onclick = async () => {
2292
+ try {
2293
+ await loadProposal();
2294
+ } catch (error) {
2295
+ showError(error);
2296
+ }
2297
+ };
2298
+ content.append(refresh);
2299
+ // An uncertain submission stays disabled. Reading the same proposal ID
2300
+ // cannot create another proposal or resubmit the financial action.
2301
+ }
2302
+ };
2303
+ }
2304
+ function demo() {
2305
+ loginView.hidden = true;
2306
+ content.append(card("模拟确认卡片 · 不会交易"));
2307
+ content.firstElementChild.append(
2308
+ row("操作", "界面确认测试"),
2309
+ row("金额", "0 USDC"),
2310
+ el("p", "本卡片不连接账户、交易所或执行服务。", "muted"),
2311
+ );
2312
+ actions.hidden = false;
2313
+ for (const id of ["approve", "reject"])
2314
+ document.getElementById(id).onclick = () => {
2315
+ actions.hidden = true;
2316
+ status.textContent =
2317
+ id === "approve"
2318
+ ? "模拟确认成功 · 未提交交易"
2319
+ : "模拟已取消 · 未提交交易";
2320
+ announce(id === "approve" ? "demo_confirmed" : "demo_rejected");
2321
+ };
2322
+ announce("demo_ready");
2323
+ }
2324
+ async function start() {
2325
+ if (config.demo) {
2326
+ demo();
2327
+ return;
2328
+ }
2329
+ status.textContent = "正在恢复 Ride 登录…";
2330
+ const url = new URL(location.href),
2331
+ query = url.searchParams;
2332
+ if (query.has("error") && !query.has("privy_oauth_code")) {
2333
+ url.searchParams.delete("error");
2334
+ url.searchParams.delete("error_description");
2335
+ history.replaceState({}, "", url.href);
2336
+ showLogin();
2337
+ showError(Error("Ride 登录已取消。"));
2338
+ return;
2339
+ }
2340
+ try {
2341
+ if (query.get("privy_oauth_code") && query.get("privy_oauth_state")) {
2342
+ const c = await init();
2343
+ const session = await c.auth.oauth.loginWithCode(
2344
+ query.get("privy_oauth_code"),
2345
+ query.get("privy_oauth_state"),
2346
+ "google",
2347
+ );
2348
+ for (const key of ["privy_oauth_code", "privy_oauth_state"])
2349
+ url.searchParams.delete(key);
2350
+ history.replaceState({}, "", url.href);
2351
+ if (!(await finish(session))) showLogin();
2352
+ return;
2353
+ }
2354
+ try {
2355
+ csrf = (await call("session")).csrf;
2356
+ await ready();
2357
+ return;
2358
+ } catch (e) {
2359
+ if (!authError(e)) throw e;
2360
+ csrf = undefined;
2361
+ }
2362
+ // Existing Privy storage can silently renew an expired human cookie.
2363
+ // Never launch Google OAuth automatically, and never sign in inside a card.
2364
+ if (!embedded && (await finish())) return;
2365
+ status.textContent = "";
2366
+ showLogin();
2367
+ } catch (e) {
2368
+ showLogin();
2369
+ showError(e);
2370
+ }
2371
+ }
2372
+ start();
2373
+ </script></body></html>