@exagent/agent 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -37,12 +37,21 @@ __export(index_exports, {
37
37
  DeepSeekAdapter: () => DeepSeekAdapter,
38
38
  GoogleAdapter: () => GoogleAdapter,
39
39
  GroqAdapter: () => GroqAdapter,
40
+ HYPERLIQUID_DOMAIN: () => HYPERLIQUID_DOMAIN,
41
+ HyperliquidClient: () => HyperliquidClient,
42
+ HyperliquidSigner: () => HyperliquidSigner,
43
+ HyperliquidWebSocket: () => HyperliquidWebSocket,
40
44
  LLMConfigSchema: () => LLMConfigSchema,
41
45
  LLMProviderSchema: () => LLMProviderSchema,
42
46
  MarketDataService: () => MarketDataService,
43
47
  MistralAdapter: () => MistralAdapter,
44
48
  OllamaAdapter: () => OllamaAdapter,
45
49
  OpenAIAdapter: () => OpenAIAdapter,
50
+ OrderManager: () => OrderManager,
51
+ PerpConfigSchema: () => PerpConfigSchema,
52
+ PerpOnboarding: () => PerpOnboarding,
53
+ PerpTradeRecorder: () => PerpTradeRecorder,
54
+ PositionManager: () => PositionManager,
46
55
  RelayClient: () => RelayClient,
47
56
  RelayConfigSchema: () => RelayConfigSchema,
48
57
  RiskManager: () => RiskManager,
@@ -58,7 +67,10 @@ __export(index_exports, {
58
67
  createSampleConfig: () => createSampleConfig,
59
68
  decryptEnvFile: () => decryptEnvFile,
60
69
  encryptEnvFile: () => encryptEnvFile,
70
+ fillHashToBytes32: () => fillHashToBytes32,
71
+ fillOidToBytes32: () => fillOidToBytes32,
61
72
  getAllStrategyTemplates: () => getAllStrategyTemplates,
73
+ getNextNonce: () => getNextNonce,
62
74
  getStrategyTemplate: () => getStrategyTemplate,
63
75
  loadConfig: () => loadConfig,
64
76
  loadSecureEnv: () => loadSecureEnv,
@@ -70,8 +82,9 @@ module.exports = __toCommonJS(index_exports);
70
82
 
71
83
  // src/runtime.ts
72
84
  var import_sdk2 = require("@exagent/sdk");
73
- var import_viem3 = require("viem");
74
- var import_chains2 = require("viem/chains");
85
+ var import_viem6 = require("viem");
86
+ var import_chains4 = require("viem/chains");
87
+ var import_accounts5 = require("viem/accounts");
75
88
 
76
89
  // src/llm/openai.ts
77
90
  var import_openai = __toESM(require("openai"));
@@ -937,6 +950,26 @@ var RelayConfigSchema = import_zod.z.object({
937
950
  apiUrl: import_zod.z.string().url(),
938
951
  heartbeatIntervalMs: import_zod.z.number().min(5e3).default(3e4)
939
952
  }).optional();
953
+ var PerpConfigSchema = import_zod.z.object({
954
+ /** Enable perp trading */
955
+ enabled: import_zod.z.boolean().default(false),
956
+ /** Hyperliquid REST API URL */
957
+ apiUrl: import_zod.z.string().url().default("https://api.hyperliquid.xyz"),
958
+ /** Hyperliquid WebSocket URL */
959
+ wsUrl: import_zod.z.string().default("wss://api.hyperliquid.xyz/ws"),
960
+ /** Builder address for fee collection (must have >= 100 USDC on HL) */
961
+ builderAddress: import_zod.z.string(),
962
+ /** Builder fee in tenths of basis points (100 = 10 bps = 0.10%) */
963
+ builderFeeTenthsBps: import_zod.z.number().min(0).max(500).default(100),
964
+ /** Private key for the perp relayer (calls recordPerpTrade on Base). Falls back to agent wallet. */
965
+ perpRelayerKey: import_zod.z.string().optional(),
966
+ /** Maximum leverage per position (default: 10) */
967
+ maxLeverage: import_zod.z.number().min(1).max(50).default(10),
968
+ /** Maximum notional position size in USD (default: 50000) */
969
+ maxNotionalUSD: import_zod.z.number().min(100).default(5e4),
970
+ /** Allowed perp instruments (e.g. ["ETH", "BTC", "SOL"]). If empty, all instruments allowed. */
971
+ allowedInstruments: import_zod.z.array(import_zod.z.string()).optional()
972
+ }).optional();
940
973
  var AgentConfigSchema = import_zod.z.object({
941
974
  // Identity (from on-chain registration)
942
975
  agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
@@ -954,6 +987,8 @@ var AgentConfigSchema = import_zod.z.object({
954
987
  vault: VaultConfigSchema.default({}),
955
988
  // Relay configuration (command center)
956
989
  relay: RelayConfigSchema,
990
+ // Perp trading configuration (Hyperliquid)
991
+ perp: PerpConfigSchema,
957
992
  // Allowed tokens (addresses)
958
993
  allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
959
994
  });
@@ -1538,6 +1573,101 @@ var RiskManager = class {
1538
1573
  isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
1539
1574
  };
1540
1575
  }
1576
+ // ============================================================
1577
+ // PERP RISK FILTERING
1578
+ // ============================================================
1579
+ /**
1580
+ * Filter perp trade signals through risk checks.
1581
+ * Reduce-only signals (closes) always pass.
1582
+ *
1583
+ * @param signals - Raw perp signals from strategy
1584
+ * @param positions - Current open positions on Hyperliquid
1585
+ * @param account - Current account equity and margin
1586
+ * @param maxLeverage - Maximum allowed leverage
1587
+ * @param maxNotionalUSD - Maximum notional per position
1588
+ * @returns Signals that pass risk checks
1589
+ */
1590
+ filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD) {
1591
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1592
+ if (today !== this.lastResetDate) {
1593
+ this.dailyPnL = 0;
1594
+ this.dailyFees = 0;
1595
+ this.lastResetDate = today;
1596
+ }
1597
+ if (this.isDailyLossLimitHit(account.totalEquity)) {
1598
+ console.warn("Daily loss limit reached \u2014 blocking new perp trades");
1599
+ return signals.filter((s) => s.reduceOnly);
1600
+ }
1601
+ return signals.filter((signal) => this.validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD));
1602
+ }
1603
+ /**
1604
+ * Validate an individual perp signal.
1605
+ */
1606
+ validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD) {
1607
+ if (signal.action === "hold") {
1608
+ return true;
1609
+ }
1610
+ if (signal.reduceOnly || signal.action === "close_long" || signal.action === "close_short") {
1611
+ return true;
1612
+ }
1613
+ if (signal.confidence < 0.5) {
1614
+ console.warn(`Perp signal confidence too low: ${signal.confidence} for ${signal.instrument}`);
1615
+ return false;
1616
+ }
1617
+ if (signal.leverage > maxLeverage) {
1618
+ console.warn(`Perp signal leverage ${signal.leverage}x exceeds max ${maxLeverage}x for ${signal.instrument}`);
1619
+ return false;
1620
+ }
1621
+ const signalNotional = signal.size * signal.price;
1622
+ if (signalNotional > maxNotionalUSD) {
1623
+ console.warn(`Perp signal notional $${signalNotional.toFixed(0)} exceeds max $${maxNotionalUSD} for ${signal.instrument}`);
1624
+ return false;
1625
+ }
1626
+ const currentNotional = account.totalNotional;
1627
+ const projectedNotional = currentNotional + signalNotional;
1628
+ const projectedLeverage = account.totalEquity > 0 ? projectedNotional / account.totalEquity : 0;
1629
+ if (projectedLeverage > maxLeverage) {
1630
+ console.warn(
1631
+ `Perp signal would push aggregate leverage to ${projectedLeverage.toFixed(1)}x (max: ${maxLeverage}x) \u2014 blocked`
1632
+ );
1633
+ return false;
1634
+ }
1635
+ const existingPos = positions.find((p) => p.instrument === signal.instrument);
1636
+ if (existingPos) {
1637
+ const liqProximity = this.calculateLiquidationProximity(existingPos);
1638
+ if (liqProximity > 0.7) {
1639
+ console.warn(
1640
+ `Position ${signal.instrument} liquidation proximity ${(liqProximity * 100).toFixed(0)}% \u2014 blocking new entry`
1641
+ );
1642
+ return false;
1643
+ }
1644
+ }
1645
+ const requiredMargin = signalNotional / signal.leverage;
1646
+ if (requiredMargin > account.availableMargin) {
1647
+ console.warn(
1648
+ `Insufficient margin for ${signal.instrument}: need $${requiredMargin.toFixed(0)}, have $${account.availableMargin.toFixed(0)}`
1649
+ );
1650
+ return false;
1651
+ }
1652
+ return true;
1653
+ }
1654
+ /**
1655
+ * Calculate liquidation proximity for a position (0.0 = safe, 1.0 = liquidated).
1656
+ */
1657
+ calculateLiquidationProximity(pos) {
1658
+ if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) return 0;
1659
+ if (pos.size > 0) {
1660
+ if (pos.markPrice <= pos.liquidationPrice) return 1;
1661
+ const distance = pos.markPrice - pos.liquidationPrice;
1662
+ const total = pos.entryPrice - pos.liquidationPrice;
1663
+ return total > 0 ? 1 - distance / total : 0;
1664
+ } else {
1665
+ if (pos.markPrice >= pos.liquidationPrice) return 1;
1666
+ const distance = pos.liquidationPrice - pos.markPrice;
1667
+ const total = pos.liquidationPrice - pos.entryPrice;
1668
+ return total > 0 ? 1 - distance / total : 0;
1669
+ }
1670
+ }
1541
1671
  };
1542
1672
 
1543
1673
  // src/vault/manager.ts
@@ -2105,130 +2235,1488 @@ function openBrowser(url) {
2105
2235
  }
2106
2236
  }
2107
2237
 
2108
- // src/runtime.ts
2109
- var FUNDS_LOW_THRESHOLD = 5e-3;
2110
- var FUNDS_CRITICAL_THRESHOLD = 1e-3;
2111
- var AgentRuntime = class {
2112
- config;
2113
- client;
2114
- llm;
2115
- strategy;
2116
- executor;
2117
- riskManager;
2118
- marketData;
2119
- vaultManager;
2120
- relay = null;
2121
- isRunning = false;
2122
- mode = "idle";
2123
- configHash;
2124
- cycleCount = 0;
2125
- lastCycleAt = 0;
2126
- lastPortfolioValue = 0;
2127
- lastEthBalance = "0";
2128
- processAlive = true;
2129
- riskUniverse = 0;
2130
- allowedTokens = /* @__PURE__ */ new Set();
2238
+ // src/perp/client.ts
2239
+ var HyperliquidClient = class {
2240
+ apiUrl;
2241
+ meta = null;
2242
+ assetIndexCache = /* @__PURE__ */ new Map();
2131
2243
  constructor(config) {
2132
- this.config = config;
2244
+ this.apiUrl = config.apiUrl;
2245
+ }
2246
+ // ============================================================
2247
+ // INFO API (read-only)
2248
+ // ============================================================
2249
+ /** Fetch perpetuals metadata (asset specs, names, indices) */
2250
+ async getMeta() {
2251
+ if (this.meta) return this.meta;
2252
+ const resp = await this.infoRequest({ type: "meta" });
2253
+ this.meta = resp.universe;
2254
+ this.meta.forEach((asset, idx) => {
2255
+ this.assetIndexCache.set(asset.name, idx);
2256
+ });
2257
+ return this.meta;
2258
+ }
2259
+ /** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
2260
+ async getAssetIndex(coin) {
2261
+ if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
2262
+ await this.getMeta();
2263
+ const idx = this.assetIndexCache.get(coin);
2264
+ if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
2265
+ return idx;
2266
+ }
2267
+ /** Get mid-market prices for all perpetuals */
2268
+ async getAllMids() {
2269
+ return this.infoRequest({ type: "allMids" });
2270
+ }
2271
+ /** Get clearinghouse state (positions, margin, equity) for a user */
2272
+ async getClearinghouseState(user) {
2273
+ return this.infoRequest({ type: "clearinghouseState", user });
2274
+ }
2275
+ /** Get user's recent fills */
2276
+ async getUserFills(user, startTime) {
2277
+ return this.infoRequest({
2278
+ type: "userFills",
2279
+ user,
2280
+ ...startTime !== void 0 && { startTime }
2281
+ });
2282
+ }
2283
+ /** Get user's fills in a time range */
2284
+ async getUserFillsByTime(user, startTime, endTime) {
2285
+ return this.infoRequest({
2286
+ type: "userFillsByTime",
2287
+ user,
2288
+ startTime,
2289
+ ...endTime !== void 0 && { endTime }
2290
+ });
2291
+ }
2292
+ /** Get user's open orders */
2293
+ async getOpenOrders(user) {
2294
+ return this.infoRequest({ type: "openOrders", user });
2295
+ }
2296
+ /** Get user's funding history */
2297
+ async getUserFunding(user, startTime) {
2298
+ return this.infoRequest({ type: "userFunding", user, startTime });
2299
+ }
2300
+ /** Check max approved builder fee for a user */
2301
+ async getMaxBuilderFee(user, builder) {
2302
+ const resp = await this.infoRequest({ type: "maxBuilderFee", user, builder });
2303
+ return resp;
2304
+ }
2305
+ /** Get L2 order book for a coin */
2306
+ async getL2Book(coin, depth) {
2307
+ return this.infoRequest({
2308
+ type: "l2Book",
2309
+ coin,
2310
+ ...depth !== void 0 && { nSigFigs: depth }
2311
+ });
2312
+ }
2313
+ // ============================================================
2314
+ // HIGH-LEVEL HELPERS
2315
+ // ============================================================
2316
+ /** Get parsed positions for a user address */
2317
+ async getPositions(user) {
2318
+ const state = await this.getClearinghouseState(user);
2319
+ return state.assetPositions.filter((p) => parseFloat(p.position.szi) !== 0).map((p) => this.parsePosition(p));
2320
+ }
2321
+ /** Get account summary (equity, margin, leverage) */
2322
+ async getAccountSummary(user) {
2323
+ const state = await this.getClearinghouseState(user);
2324
+ const crossMarginSummary = state.crossMarginSummary;
2325
+ const totalEquity = parseFloat(crossMarginSummary.accountValue);
2326
+ const totalNotional = parseFloat(crossMarginSummary.totalNtlPos);
2327
+ const totalMarginUsed = parseFloat(crossMarginSummary.totalMarginUsed);
2328
+ return {
2329
+ totalEquity,
2330
+ availableMargin: totalEquity - totalMarginUsed,
2331
+ totalMarginUsed,
2332
+ totalUnrealizedPnl: parseFloat(crossMarginSummary.totalRawUsd) - totalEquity,
2333
+ totalNotional,
2334
+ maintenanceMargin: totalMarginUsed * 0.5,
2335
+ // Approximate
2336
+ effectiveLeverage: totalEquity > 0 ? totalNotional / totalEquity : 0,
2337
+ cashBalance: parseFloat(state.crossMarginSummary.accountValue) - state.assetPositions.reduce(
2338
+ (sum, p) => sum + parseFloat(p.position.unrealizedPnl),
2339
+ 0
2340
+ )
2341
+ };
2342
+ }
2343
+ /** Get market data for a list of instruments */
2344
+ async getMarketData(instruments) {
2345
+ const mids = await this.getAllMids();
2346
+ const meta = await this.getMeta();
2347
+ return instruments.filter((inst) => mids[inst] !== void 0).map((inst) => {
2348
+ const midPrice = parseFloat(mids[inst]);
2349
+ const assetMeta = meta.find((m) => m.name === inst);
2350
+ return {
2351
+ instrument: inst,
2352
+ midPrice,
2353
+ bestBid: midPrice,
2354
+ // Approximate — use L2 book for exact
2355
+ bestAsk: midPrice,
2356
+ funding8h: 0,
2357
+ // Would need separate funding API call
2358
+ openInterest: 0,
2359
+ // Would need separate meta call
2360
+ volume24h: 0,
2361
+ priceChange24h: 0
2362
+ };
2363
+ });
2364
+ }
2365
+ /** Convert fills to our PerpFill type */
2366
+ parseFill(fill) {
2367
+ return {
2368
+ oid: fill.oid,
2369
+ coin: fill.coin,
2370
+ side: fill.side,
2371
+ px: fill.px,
2372
+ sz: fill.sz,
2373
+ fee: fill.fee,
2374
+ time: fill.time,
2375
+ hash: fill.hash,
2376
+ isMaker: fill.startPosition !== fill.px,
2377
+ // Approximate maker detection
2378
+ builderFee: fill.builderFee,
2379
+ liquidation: fill.liquidation
2380
+ };
2381
+ }
2382
+ // ============================================================
2383
+ // PRIVATE HELPERS
2384
+ // ============================================================
2385
+ parsePosition(ap) {
2386
+ const pos = ap.position;
2387
+ const size = parseFloat(pos.szi);
2388
+ const entryPrice = parseFloat(pos.entryPx || "0");
2389
+ const markPrice = parseFloat(pos.positionValue || "0") / Math.abs(size || 1);
2390
+ return {
2391
+ instrument: pos.coin,
2392
+ assetIndex: this.assetIndexCache.get(pos.coin) ?? -1,
2393
+ size,
2394
+ entryPrice,
2395
+ markPrice,
2396
+ unrealizedPnl: parseFloat(pos.unrealizedPnl),
2397
+ leverage: parseFloat(pos.leverage?.value || "1"),
2398
+ marginType: pos.leverage?.type === "isolated" ? "isolated" : "cross",
2399
+ liquidationPrice: parseFloat(pos.liquidationPx || "0"),
2400
+ notionalUSD: Math.abs(size) * markPrice,
2401
+ marginUsed: parseFloat(pos.marginUsed)
2402
+ };
2403
+ }
2404
+ async infoRequest(body) {
2405
+ const resp = await fetch(`${this.apiUrl}/info`, {
2406
+ method: "POST",
2407
+ headers: { "Content-Type": "application/json" },
2408
+ body: JSON.stringify(body)
2409
+ });
2410
+ if (!resp.ok) {
2411
+ throw new Error(`Hyperliquid Info API error: ${resp.status} ${await resp.text()}`);
2412
+ }
2413
+ return resp.json();
2414
+ }
2415
+ };
2416
+
2417
+ // src/perp/signer.ts
2418
+ var import_viem3 = require("viem");
2419
+ var HYPERLIQUID_DOMAIN = {
2420
+ name: "HyperliquidSignTransaction",
2421
+ version: "1",
2422
+ chainId: 42161n,
2423
+ // Always Arbitrum chain ID
2424
+ verifyingContract: "0x0000000000000000000000000000000000000000"
2425
+ };
2426
+ var HYPERLIQUID_TYPES = {
2427
+ HyperliquidTransaction: [
2428
+ { name: "hyperliquidChain", type: "string" },
2429
+ { name: "action", type: "string" },
2430
+ { name: "nonce", type: "uint64" }
2431
+ ]
2432
+ };
2433
+ var lastNonce = 0n;
2434
+ function getNextNonce() {
2435
+ const now = BigInt(Date.now());
2436
+ if (now <= lastNonce) {
2437
+ lastNonce = lastNonce + 1n;
2438
+ } else {
2439
+ lastNonce = now;
2440
+ }
2441
+ return lastNonce;
2442
+ }
2443
+ var HyperliquidSigner = class {
2444
+ constructor(walletClient) {
2445
+ this.walletClient = walletClient;
2133
2446
  }
2134
2447
  /**
2135
- * Initialize the agent runtime
2448
+ * Sign an exchange action (order, cancel, etc.)
2449
+ *
2450
+ * @param action - The action object (will be JSON-serialized)
2451
+ * @param nonce - Nonce (defaults to current timestamp)
2452
+ * @returns Signature hex string
2136
2453
  */
2137
- async initialize() {
2138
- console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
2139
- this.client = new import_sdk2.ExagentClient({
2140
- privateKey: this.config.privateKey,
2141
- network: this.config.network
2454
+ async signAction(action, nonce) {
2455
+ const actionNonce = nonce ?? getNextNonce();
2456
+ const actionStr = JSON.stringify(action);
2457
+ const account = this.walletClient.account;
2458
+ if (!account) throw new Error("Wallet client has no account");
2459
+ const signature = await this.walletClient.signTypedData({
2460
+ account,
2461
+ domain: HYPERLIQUID_DOMAIN,
2462
+ types: HYPERLIQUID_TYPES,
2463
+ primaryType: "HyperliquidTransaction",
2464
+ message: {
2465
+ hyperliquidChain: "Mainnet",
2466
+ action: actionStr,
2467
+ nonce: actionNonce
2468
+ }
2142
2469
  });
2143
- console.log(`Wallet: ${this.client.address}`);
2144
- const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
2145
- if (!agent) {
2146
- throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
2147
- }
2148
- console.log(`Agent verified: ${agent.name}`);
2149
- await this.ensureWalletLinked();
2150
- await this.loadTradingRestrictions();
2151
- console.log(`Initializing LLM: ${this.config.llm.provider}`);
2152
- this.llm = await createLLMAdapter(this.config.llm);
2153
- const llmMeta = this.llm.getMetadata();
2154
- console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
2155
- await this.syncConfigHash();
2156
- this.strategy = await loadStrategy();
2157
- this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
2158
- this.riskManager = new RiskManager(this.config.trading);
2159
- this.marketData = new MarketDataService(this.getRpcUrl());
2160
- await this.initializeVaultManager();
2161
- await this.initializeRelay();
2162
- console.log("Agent initialized successfully");
2470
+ return { signature, nonce: actionNonce };
2163
2471
  }
2164
2472
  /**
2165
- * Initialize the relay client for command center connectivity
2473
+ * Sign a user-level approval action (approve builder fee, approve agent).
2474
+ * These use the same EIP-712 structure but with different action payloads.
2166
2475
  */
2167
- async initializeRelay() {
2168
- const relayConfig = this.config.relay;
2169
- const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
2170
- if (!relayConfig?.enabled || !relayEnabled) {
2171
- console.log("Relay: Disabled");
2172
- return;
2476
+ async signApproval(action, nonce) {
2477
+ return this.signAction(action, nonce);
2478
+ }
2479
+ /**
2480
+ * Get the signer's address
2481
+ */
2482
+ getAddress() {
2483
+ const account = this.walletClient.account;
2484
+ if (!account) throw new Error("Wallet client has no account");
2485
+ return account.address;
2486
+ }
2487
+ };
2488
+ function fillHashToBytes32(fillHash) {
2489
+ if (fillHash.startsWith("0x") && fillHash.length === 66) {
2490
+ return fillHash;
2491
+ }
2492
+ return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["string"], [fillHash]));
2493
+ }
2494
+ function fillOidToBytes32(oid) {
2495
+ return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["uint256"], [BigInt(oid)]));
2496
+ }
2497
+
2498
+ // src/perp/orders.ts
2499
+ var OrderManager = class {
2500
+ client;
2501
+ signer;
2502
+ config;
2503
+ constructor(client, signer, config) {
2504
+ this.client = client;
2505
+ this.signer = signer;
2506
+ this.config = config;
2507
+ }
2508
+ /**
2509
+ * Place an order on Hyperliquid from a trade signal.
2510
+ * Attaches builder fee for revenue collection.
2511
+ */
2512
+ async placeOrder(signal) {
2513
+ try {
2514
+ const assetIndex = await this.client.getAssetIndex(signal.instrument);
2515
+ const isBuy = signal.action === "open_long" || signal.action === "close_short";
2516
+ const side = isBuy ? "B" : "A";
2517
+ const orderWire = {
2518
+ a: assetIndex,
2519
+ b: isBuy,
2520
+ p: signal.orderType === "market" ? this.getMarketPrice(signal) : signal.price.toString(),
2521
+ s: signal.size.toString(),
2522
+ r: signal.reduceOnly,
2523
+ t: signal.orderType === "market" ? { limit: { tif: "Ioc" } } : { limit: { tif: "Gtc" } }
2524
+ };
2525
+ const action = {
2526
+ type: "order",
2527
+ orders: [orderWire],
2528
+ grouping: "na",
2529
+ builder: {
2530
+ b: this.config.builderAddress,
2531
+ f: this.config.builderFeeTenthsBps
2532
+ }
2533
+ };
2534
+ const nonce = getNextNonce();
2535
+ const { signature } = await this.signer.signAction(action, nonce);
2536
+ const address = this.signer.getAddress();
2537
+ const resp = await this.exchangeRequest({
2538
+ action,
2539
+ nonce: Number(nonce),
2540
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2541
+ vaultAddress: null
2542
+ });
2543
+ return this.parseOrderResponse(resp);
2544
+ } catch (error) {
2545
+ const message = error instanceof Error ? error.message : String(error);
2546
+ console.error(`Order placement failed for ${signal.instrument}:`, message);
2547
+ return {
2548
+ success: false,
2549
+ status: "error",
2550
+ error: message
2551
+ };
2173
2552
  }
2174
- const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
2175
- if (!apiUrl) {
2176
- console.log("Relay: No API URL configured, skipping");
2177
- return;
2553
+ }
2554
+ /**
2555
+ * Cancel an open order by ID.
2556
+ */
2557
+ async cancelOrder(instrument, orderId) {
2558
+ try {
2559
+ const assetIndex = await this.client.getAssetIndex(instrument);
2560
+ const action = {
2561
+ type: "cancel",
2562
+ cancels: [{ a: assetIndex, o: orderId }]
2563
+ };
2564
+ const nonce = getNextNonce();
2565
+ const { signature } = await this.signer.signAction(action, nonce);
2566
+ await this.exchangeRequest({
2567
+ action,
2568
+ nonce: Number(nonce),
2569
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2570
+ vaultAddress: null
2571
+ });
2572
+ console.log(`Cancelled order ${orderId} for ${instrument}`);
2573
+ return true;
2574
+ } catch (error) {
2575
+ const message = error instanceof Error ? error.message : String(error);
2576
+ console.error(`Cancel failed for order ${orderId}:`, message);
2577
+ return false;
2178
2578
  }
2179
- this.relay = new RelayClient({
2180
- agentId: String(this.config.agentId),
2181
- privateKey: this.config.privateKey,
2182
- relay: {
2183
- ...relayConfig,
2184
- apiUrl
2185
- },
2186
- onCommand: (cmd) => this.handleCommand(cmd)
2187
- });
2579
+ }
2580
+ /**
2581
+ * Close an entire position for an instrument.
2582
+ * Uses a market order with reduceOnly flag.
2583
+ */
2584
+ async closePosition(instrument, positionSize) {
2585
+ const isLong = positionSize > 0;
2586
+ const signal = {
2587
+ action: isLong ? "close_long" : "close_short",
2588
+ instrument,
2589
+ size: Math.abs(positionSize),
2590
+ price: 0,
2591
+ leverage: 1,
2592
+ orderType: "market",
2593
+ reduceOnly: true,
2594
+ confidence: 1,
2595
+ reasoning: "Position close"
2596
+ };
2597
+ return this.placeOrder(signal);
2598
+ }
2599
+ /**
2600
+ * Update leverage for an instrument.
2601
+ */
2602
+ async updateLeverage(instrument, leverage, isCross = true) {
2188
2603
  try {
2189
- await this.relay.connect();
2190
- console.log("Relay: Connected to command center");
2191
- this.sendRelayStatus();
2604
+ const assetIndex = await this.client.getAssetIndex(instrument);
2605
+ const action = {
2606
+ type: "updateLeverage",
2607
+ asset: assetIndex,
2608
+ isCross,
2609
+ leverage
2610
+ };
2611
+ const nonce = getNextNonce();
2612
+ const { signature } = await this.signer.signAction(action, nonce);
2613
+ await this.exchangeRequest({
2614
+ action,
2615
+ nonce: Number(nonce),
2616
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2617
+ vaultAddress: null
2618
+ });
2619
+ console.log(`Leverage updated for ${instrument}: ${leverage}x (${isCross ? "cross" : "isolated"})`);
2620
+ return true;
2192
2621
  } catch (error) {
2193
- console.warn(
2194
- "Relay: Failed to connect (agent will work locally):",
2195
- error instanceof Error ? error.message : error
2196
- );
2622
+ const message = error instanceof Error ? error.message : String(error);
2623
+ console.error(`Leverage update failed for ${instrument}:`, message);
2624
+ return false;
2197
2625
  }
2198
2626
  }
2627
+ // ============================================================
2628
+ // PRIVATE HELPERS
2629
+ // ============================================================
2199
2630
  /**
2200
- * Initialize the vault manager based on config
2631
+ * Get a market price string for IOC orders.
2632
+ * Uses a generous slippage buffer to ensure fills.
2201
2633
  */
2202
- async initializeVaultManager() {
2203
- const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
2204
- this.vaultManager = new VaultManager({
2205
- agentId: BigInt(this.config.agentId),
2206
- agentName: this.config.name,
2207
- network: this.config.network,
2208
- walletKey: this.config.privateKey,
2209
- vaultConfig
2210
- });
2211
- console.log(`Vault policy: ${vaultConfig.policy}`);
2212
- const status = await this.vaultManager.getVaultStatus();
2213
- if (status.hasVault) {
2214
- console.log(`Vault exists: ${status.vaultAddress}`);
2215
- console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
2216
- } else {
2217
- console.log("No vault exists for this agent");
2218
- if (vaultConfig.policy === "manual") {
2219
- console.log("Vault creation is manual \u2014 use the command center to create one");
2634
+ getMarketPrice(signal) {
2635
+ const isBuy = signal.action === "open_long" || signal.action === "close_short";
2636
+ if (signal.price > 0) {
2637
+ const slippage = isBuy ? 1.005 : 0.995;
2638
+ return (signal.price * slippage).toString();
2639
+ }
2640
+ return "0";
2641
+ }
2642
+ /**
2643
+ * Parse Hyperliquid exchange response into OrderResult.
2644
+ */
2645
+ parseOrderResponse(resp) {
2646
+ if (resp?.status === "ok" && resp?.response?.type === "order") {
2647
+ const statuses = resp.response.data?.statuses || [];
2648
+ if (statuses.length > 0) {
2649
+ const status = statuses[0];
2650
+ if (status.filled) {
2651
+ return {
2652
+ success: true,
2653
+ orderId: status.filled.oid,
2654
+ status: "filled",
2655
+ avgPrice: status.filled.avgPx,
2656
+ filledSize: status.filled.totalSz
2657
+ };
2658
+ }
2659
+ if (status.resting) {
2660
+ return {
2661
+ success: true,
2662
+ orderId: status.resting.oid,
2663
+ status: "resting"
2664
+ };
2665
+ }
2666
+ if (status.error) {
2667
+ return {
2668
+ success: false,
2669
+ status: "error",
2670
+ error: status.error
2671
+ };
2672
+ }
2220
2673
  }
2221
2674
  }
2675
+ return {
2676
+ success: false,
2677
+ status: "error",
2678
+ error: `Unexpected response: ${JSON.stringify(resp)}`
2679
+ };
2222
2680
  }
2223
2681
  /**
2224
- * Ensure the current wallet is linked to the agent.
2225
- * If the trading wallet differs from the owner, enters a recovery loop
2226
- * that waits for the owner to link it from the website.
2682
+ * Send a signed request to the Hyperliquid Exchange API.
2227
2683
  */
2228
- async ensureWalletLinked() {
2229
- const agentId = BigInt(this.config.agentId);
2230
- const address = this.client.address;
2231
- const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
2684
+ async exchangeRequest(body) {
2685
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
2686
+ method: "POST",
2687
+ headers: { "Content-Type": "application/json" },
2688
+ body: JSON.stringify(body)
2689
+ });
2690
+ if (!resp.ok) {
2691
+ throw new Error(`Hyperliquid Exchange API error: ${resp.status} ${await resp.text()}`);
2692
+ }
2693
+ return resp.json();
2694
+ }
2695
+ };
2696
+
2697
+ // src/perp/positions.ts
2698
+ var PositionManager = class {
2699
+ client;
2700
+ userAddress;
2701
+ /** Cached positions (updated each cycle) */
2702
+ cachedPositions = [];
2703
+ cachedAccount = null;
2704
+ lastRefreshAt = 0;
2705
+ /** Cache TTL in ms (5 seconds — positions refresh each cycle anyway) */
2706
+ cacheTtlMs = 5e3;
2707
+ constructor(client, userAddress) {
2708
+ this.client = client;
2709
+ this.userAddress = userAddress;
2710
+ }
2711
+ // ============================================================
2712
+ // POSITION QUERIES
2713
+ // ============================================================
2714
+ /**
2715
+ * Get all open positions. Uses cache if fresh.
2716
+ */
2717
+ async getPositions(forceRefresh = false) {
2718
+ if (!forceRefresh && this.isCacheFresh()) {
2719
+ return this.cachedPositions;
2720
+ }
2721
+ await this.refresh();
2722
+ return this.cachedPositions;
2723
+ }
2724
+ /**
2725
+ * Get a specific position by instrument.
2726
+ * Returns null if no position is open.
2727
+ */
2728
+ async getPosition(instrument) {
2729
+ const positions = await this.getPositions();
2730
+ return positions.find((p) => p.instrument === instrument) ?? null;
2731
+ }
2732
+ /**
2733
+ * Get account summary (equity, margin, leverage).
2734
+ */
2735
+ async getAccountSummary(forceRefresh = false) {
2736
+ if (!forceRefresh && this.isCacheFresh() && this.cachedAccount) {
2737
+ return this.cachedAccount;
2738
+ }
2739
+ await this.refresh();
2740
+ return this.cachedAccount;
2741
+ }
2742
+ // ============================================================
2743
+ // LIQUIDATION MONITORING
2744
+ // ============================================================
2745
+ /**
2746
+ * Get liquidation proximity for all positions.
2747
+ * Returns a value between 0.0 (safe) and 1.0 (at liquidation price).
2748
+ * Values above 0.7 should trigger risk warnings.
2749
+ */
2750
+ async getLiquidationProximity() {
2751
+ const positions = await this.getPositions();
2752
+ const proximities = /* @__PURE__ */ new Map();
2753
+ for (const pos of positions) {
2754
+ if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) {
2755
+ proximities.set(pos.instrument, 0);
2756
+ continue;
2757
+ }
2758
+ let proximity;
2759
+ if (pos.size > 0) {
2760
+ if (pos.markPrice <= pos.liquidationPrice) {
2761
+ proximity = 1;
2762
+ } else {
2763
+ const distanceToLiq = pos.markPrice - pos.liquidationPrice;
2764
+ const entryToLiq = pos.entryPrice - pos.liquidationPrice;
2765
+ proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
2766
+ }
2767
+ } else {
2768
+ if (pos.markPrice >= pos.liquidationPrice) {
2769
+ proximity = 1;
2770
+ } else {
2771
+ const distanceToLiq = pos.liquidationPrice - pos.markPrice;
2772
+ const entryToLiq = pos.liquidationPrice - pos.entryPrice;
2773
+ proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
2774
+ }
2775
+ }
2776
+ proximities.set(pos.instrument, Math.max(0, Math.min(1, proximity)));
2777
+ }
2778
+ return proximities;
2779
+ }
2780
+ /**
2781
+ * Check if any position is dangerously close to liquidation.
2782
+ * Returns instruments with proximity > threshold.
2783
+ */
2784
+ async getDangerousPositions(threshold = 0.7) {
2785
+ const positions = await this.getPositions();
2786
+ const proximities = await this.getLiquidationProximity();
2787
+ return positions.filter((p) => {
2788
+ const prox = proximities.get(p.instrument) ?? 0;
2789
+ return prox > threshold;
2790
+ });
2791
+ }
2792
+ // ============================================================
2793
+ // SUMMARY HELPERS
2794
+ // ============================================================
2795
+ /**
2796
+ * Get total unrealized PnL across all positions.
2797
+ */
2798
+ async getTotalUnrealizedPnl() {
2799
+ const positions = await this.getPositions();
2800
+ return positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
2801
+ }
2802
+ /**
2803
+ * Get total notional exposure.
2804
+ */
2805
+ async getTotalNotional() {
2806
+ const positions = await this.getPositions();
2807
+ return positions.reduce((sum, p) => sum + p.notionalUSD, 0);
2808
+ }
2809
+ /**
2810
+ * Get position count.
2811
+ */
2812
+ async getPositionCount() {
2813
+ const positions = await this.getPositions();
2814
+ return positions.length;
2815
+ }
2816
+ // ============================================================
2817
+ // CACHE MANAGEMENT
2818
+ // ============================================================
2819
+ /**
2820
+ * Force refresh positions and account from Hyperliquid.
2821
+ */
2822
+ async refresh() {
2823
+ try {
2824
+ const [positions, account] = await Promise.all([
2825
+ this.client.getPositions(this.userAddress),
2826
+ this.client.getAccountSummary(this.userAddress)
2827
+ ]);
2828
+ this.cachedPositions = positions;
2829
+ this.cachedAccount = account;
2830
+ this.lastRefreshAt = Date.now();
2831
+ } catch (error) {
2832
+ const message = error instanceof Error ? error.message : String(error);
2833
+ console.error("Failed to refresh positions:", message);
2834
+ }
2835
+ }
2836
+ /**
2837
+ * Check if cache is still fresh.
2838
+ */
2839
+ isCacheFresh() {
2840
+ return Date.now() - this.lastRefreshAt < this.cacheTtlMs;
2841
+ }
2842
+ };
2843
+
2844
+ // src/perp/websocket.ts
2845
+ var import_ws2 = __toESM(require("ws"));
2846
+ var HyperliquidWebSocket = class {
2847
+ wsUrl;
2848
+ userAddress;
2849
+ client;
2850
+ ws = null;
2851
+ reconnectAttempts = 0;
2852
+ maxReconnectAttempts = 20;
2853
+ baseReconnectMs = 1e3;
2854
+ maxReconnectMs = 6e4;
2855
+ reconnectTimer = null;
2856
+ pingTimer = null;
2857
+ isConnecting = false;
2858
+ shouldReconnect = true;
2859
+ /** Last processed fill time (ms) — used for REST backfill on reconnect */
2860
+ lastProcessedFillTime = 0;
2861
+ /** Callbacks */
2862
+ onFill = null;
2863
+ onFunding = null;
2864
+ onLiquidation = null;
2865
+ constructor(config, userAddress, client) {
2866
+ this.wsUrl = config.wsUrl;
2867
+ this.userAddress = userAddress;
2868
+ this.client = client;
2869
+ }
2870
+ // ============================================================
2871
+ // CONNECTION
2872
+ // ============================================================
2873
+ /**
2874
+ * Connect to Hyperliquid WebSocket and subscribe to user events.
2875
+ */
2876
+ async connect() {
2877
+ if (this.ws?.readyState === import_ws2.default.OPEN || this.isConnecting) {
2878
+ return;
2879
+ }
2880
+ this.isConnecting = true;
2881
+ this.shouldReconnect = true;
2882
+ return new Promise((resolve, reject) => {
2883
+ try {
2884
+ this.ws = new import_ws2.default(this.wsUrl);
2885
+ this.ws.on("open", () => {
2886
+ this.isConnecting = false;
2887
+ this.reconnectAttempts = 0;
2888
+ console.log("Hyperliquid WebSocket connected");
2889
+ this.subscribe({
2890
+ type: "subscribe",
2891
+ subscription: { type: "userFills", user: this.userAddress }
2892
+ });
2893
+ this.subscribe({
2894
+ type: "subscribe",
2895
+ subscription: { type: "userFundings", user: this.userAddress }
2896
+ });
2897
+ this.startPing();
2898
+ this.backfillMissedFills().catch((err) => {
2899
+ console.warn("Fill backfill failed:", err instanceof Error ? err.message : err);
2900
+ });
2901
+ resolve();
2902
+ });
2903
+ this.ws.on("message", (data) => {
2904
+ this.handleMessage(data);
2905
+ });
2906
+ this.ws.on("close", (code, reason) => {
2907
+ this.isConnecting = false;
2908
+ console.log(`Hyperliquid WebSocket closed: ${code} ${reason.toString()}`);
2909
+ this.stopPing();
2910
+ this.scheduleReconnect();
2911
+ });
2912
+ this.ws.on("error", (error) => {
2913
+ this.isConnecting = false;
2914
+ console.error("Hyperliquid WebSocket error:", error.message);
2915
+ if (this.reconnectAttempts === 0) {
2916
+ reject(error);
2917
+ }
2918
+ });
2919
+ } catch (error) {
2920
+ this.isConnecting = false;
2921
+ reject(error);
2922
+ }
2923
+ });
2924
+ }
2925
+ /**
2926
+ * Disconnect and stop reconnecting.
2927
+ */
2928
+ disconnect() {
2929
+ this.shouldReconnect = false;
2930
+ if (this.reconnectTimer) {
2931
+ clearTimeout(this.reconnectTimer);
2932
+ this.reconnectTimer = null;
2933
+ }
2934
+ this.stopPing();
2935
+ if (this.ws) {
2936
+ this.ws.removeAllListeners();
2937
+ if (this.ws.readyState === import_ws2.default.OPEN) {
2938
+ this.ws.close(1e3, "Client disconnect");
2939
+ }
2940
+ this.ws = null;
2941
+ }
2942
+ console.log("Hyperliquid WebSocket disconnected");
2943
+ }
2944
+ /**
2945
+ * Check if WebSocket is connected.
2946
+ */
2947
+ get isConnected() {
2948
+ return this.ws?.readyState === import_ws2.default.OPEN;
2949
+ }
2950
+ // ============================================================
2951
+ // EVENT HANDLERS
2952
+ // ============================================================
2953
+ /**
2954
+ * Register callback for fill events.
2955
+ */
2956
+ onFillReceived(callback) {
2957
+ this.onFill = callback;
2958
+ }
2959
+ /**
2960
+ * Register callback for funding payment events.
2961
+ */
2962
+ onFundingReceived(callback) {
2963
+ this.onFunding = callback;
2964
+ }
2965
+ /**
2966
+ * Register callback for liquidation events.
2967
+ */
2968
+ onLiquidationDetected(callback) {
2969
+ this.onLiquidation = callback;
2970
+ }
2971
+ /**
2972
+ * Get the last processed fill time (for external checkpoint management).
2973
+ */
2974
+ getLastProcessedFillTime() {
2975
+ return this.lastProcessedFillTime;
2976
+ }
2977
+ // ============================================================
2978
+ // MESSAGE HANDLING
2979
+ // ============================================================
2980
+ handleMessage(data) {
2981
+ try {
2982
+ const msg = JSON.parse(data.toString());
2983
+ if (msg.channel === "userFills") {
2984
+ this.handleFillMessage(msg.data);
2985
+ } else if (msg.channel === "userFundings") {
2986
+ this.handleFundingMessage(msg.data);
2987
+ }
2988
+ } catch (error) {
2989
+ }
2990
+ }
2991
+ handleFillMessage(fills) {
2992
+ if (!Array.isArray(fills) || !this.onFill) return;
2993
+ for (const rawFill of fills) {
2994
+ const fill = this.client.parseFill(rawFill);
2995
+ if (fill.time > this.lastProcessedFillTime) {
2996
+ this.lastProcessedFillTime = fill.time;
2997
+ }
2998
+ if (fill.liquidation && this.onLiquidation) {
2999
+ this.onLiquidation(fill.coin, parseFloat(fill.sz));
3000
+ }
3001
+ this.onFill(fill);
3002
+ }
3003
+ }
3004
+ handleFundingMessage(fundings) {
3005
+ if (!Array.isArray(fundings) || !this.onFunding) return;
3006
+ for (const funding of fundings) {
3007
+ this.onFunding({
3008
+ time: funding.time,
3009
+ coin: funding.coin,
3010
+ usdc: funding.usdc,
3011
+ szi: funding.szi,
3012
+ fundingRate: funding.fundingRate
3013
+ });
3014
+ }
3015
+ }
3016
+ // ============================================================
3017
+ // BACKFILL
3018
+ // ============================================================
3019
+ /**
3020
+ * Backfill fills that may have been missed during WebSocket downtime.
3021
+ * Uses the last processed fill time as the starting point.
3022
+ */
3023
+ async backfillMissedFills() {
3024
+ if (this.lastProcessedFillTime === 0 || !this.onFill) {
3025
+ return;
3026
+ }
3027
+ console.log(`Backfilling fills since ${new Date(this.lastProcessedFillTime).toISOString()}`);
3028
+ const fills = await this.client.getUserFillsByTime(
3029
+ this.userAddress,
3030
+ this.lastProcessedFillTime + 1
3031
+ // +1 to avoid duplicate
3032
+ );
3033
+ if (fills.length > 0) {
3034
+ console.log(`Backfilled ${fills.length} missed fills`);
3035
+ for (const rawFill of fills) {
3036
+ const fill = this.client.parseFill(rawFill);
3037
+ if (fill.time > this.lastProcessedFillTime) {
3038
+ this.lastProcessedFillTime = fill.time;
3039
+ }
3040
+ if (fill.liquidation && this.onLiquidation) {
3041
+ this.onLiquidation(fill.coin, parseFloat(fill.sz));
3042
+ }
3043
+ this.onFill(fill);
3044
+ }
3045
+ }
3046
+ }
3047
+ // ============================================================
3048
+ // RECONNECTION
3049
+ // ============================================================
3050
+ scheduleReconnect() {
3051
+ if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
3052
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
3053
+ console.error(`Hyperliquid WebSocket: max reconnect attempts (${this.maxReconnectAttempts}) reached`);
3054
+ }
3055
+ return;
3056
+ }
3057
+ const delay = Math.min(
3058
+ this.baseReconnectMs * Math.pow(2, this.reconnectAttempts),
3059
+ this.maxReconnectMs
3060
+ );
3061
+ this.reconnectAttempts++;
3062
+ console.log(`Hyperliquid WebSocket: reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
3063
+ this.reconnectTimer = setTimeout(() => {
3064
+ this.connect().catch((err) => {
3065
+ console.error("Reconnect failed:", err instanceof Error ? err.message : err);
3066
+ });
3067
+ }, delay);
3068
+ }
3069
+ // ============================================================
3070
+ // KEEPALIVE
3071
+ // ============================================================
3072
+ startPing() {
3073
+ this.stopPing();
3074
+ this.pingTimer = setInterval(() => {
3075
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
3076
+ this.ws.send(JSON.stringify({ method: "ping" }));
3077
+ }
3078
+ }, 25e3);
3079
+ }
3080
+ stopPing() {
3081
+ if (this.pingTimer) {
3082
+ clearInterval(this.pingTimer);
3083
+ this.pingTimer = null;
3084
+ }
3085
+ }
3086
+ // ============================================================
3087
+ // HELPERS
3088
+ // ============================================================
3089
+ subscribe(msg) {
3090
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
3091
+ this.ws.send(JSON.stringify(msg));
3092
+ }
3093
+ }
3094
+ };
3095
+
3096
+ // src/perp/recorder.ts
3097
+ var import_viem4 = require("viem");
3098
+ var import_chains2 = require("viem/chains");
3099
+ var import_accounts3 = require("viem/accounts");
3100
+ var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
3101
+ var ROUTER_ABI = [
3102
+ {
3103
+ type: "function",
3104
+ name: "recordPerpTrade",
3105
+ stateMutability: "nonpayable",
3106
+ inputs: [
3107
+ { name: "agentId", type: "uint256" },
3108
+ { name: "configHash", type: "bytes32" },
3109
+ { name: "instrument", type: "string" },
3110
+ { name: "isLong", type: "bool" },
3111
+ { name: "notionalUSD", type: "uint256" },
3112
+ { name: "feeUSD", type: "uint256" },
3113
+ { name: "fillId", type: "bytes32" }
3114
+ ],
3115
+ outputs: [{ name: "", type: "bool" }]
3116
+ }
3117
+ ];
3118
+ var MAX_RETRIES = 3;
3119
+ var RETRY_DELAY_MS = 5e3;
3120
+ var PerpTradeRecorder = class {
3121
+ // Use `any` for viem client types to avoid L2 chain type conflicts (Base has "deposit" tx type)
3122
+ publicClient;
3123
+ // eslint-disable-line @typescript-eslint/no-explicit-any
3124
+ walletClient;
3125
+ // eslint-disable-line @typescript-eslint/no-explicit-any
3126
+ account;
3127
+ agentId;
3128
+ configHash;
3129
+ /** Retry queue for failed recordings */
3130
+ retryQueue = [];
3131
+ /** Set of fill IDs already recorded (or in-progress) to prevent local dups */
3132
+ recordedFills = /* @__PURE__ */ new Set();
3133
+ /** Timer for processing retry queue */
3134
+ retryTimer = null;
3135
+ constructor(opts) {
3136
+ this.agentId = opts.agentId;
3137
+ this.configHash = opts.configHash;
3138
+ this.account = (0, import_accounts3.privateKeyToAccount)(opts.privateKey);
3139
+ const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3140
+ const transport = (0, import_viem4.http)(rpcUrl);
3141
+ this.publicClient = (0, import_viem4.createPublicClient)({
3142
+ chain: import_chains2.base,
3143
+ transport
3144
+ });
3145
+ this.walletClient = (0, import_viem4.createWalletClient)({
3146
+ chain: import_chains2.base,
3147
+ transport,
3148
+ account: this.account
3149
+ });
3150
+ this.retryTimer = setInterval(() => this.processRetryQueue(), RETRY_DELAY_MS);
3151
+ }
3152
+ // ============================================================
3153
+ // PUBLIC API
3154
+ // ============================================================
3155
+ /**
3156
+ * Record a fill on-chain.
3157
+ * Converts the Hyperliquid fill into recordPerpTrade params and submits.
3158
+ */
3159
+ async recordFill(fill) {
3160
+ const fillId = fillHashToBytes32(fill.hash);
3161
+ const fillIdStr = fillId.toLowerCase();
3162
+ if (this.recordedFills.has(fillIdStr)) {
3163
+ return { success: true };
3164
+ }
3165
+ this.recordedFills.add(fillIdStr);
3166
+ const params = {
3167
+ agentId: this.agentId,
3168
+ configHash: this.configHash,
3169
+ instrument: fill.coin,
3170
+ isLong: fill.side === "B",
3171
+ notionalUSD: this.calculateNotionalUSD(fill),
3172
+ feeUSD: this.calculateFeeUSD(fill),
3173
+ fillId
3174
+ };
3175
+ return this.submitRecord(params);
3176
+ }
3177
+ /**
3178
+ * Update the config hash (when epoch changes).
3179
+ */
3180
+ updateConfigHash(configHash) {
3181
+ this.configHash = configHash;
3182
+ }
3183
+ /**
3184
+ * Get the number of fills pending retry.
3185
+ */
3186
+ get pendingRetries() {
3187
+ return this.retryQueue.length;
3188
+ }
3189
+ /**
3190
+ * Get the number of fills recorded (local dedup set size).
3191
+ */
3192
+ get recordedCount() {
3193
+ return this.recordedFills.size;
3194
+ }
3195
+ /**
3196
+ * Stop the recorder (clear retry timer).
3197
+ */
3198
+ stop() {
3199
+ if (this.retryTimer) {
3200
+ clearInterval(this.retryTimer);
3201
+ this.retryTimer = null;
3202
+ }
3203
+ }
3204
+ // ============================================================
3205
+ // PRIVATE
3206
+ // ============================================================
3207
+ /**
3208
+ * Submit a recordPerpTrade transaction on Base.
3209
+ */
3210
+ async submitRecord(params) {
3211
+ try {
3212
+ const { request } = await this.publicClient.simulateContract({
3213
+ address: ROUTER_ADDRESS,
3214
+ abi: ROUTER_ABI,
3215
+ functionName: "recordPerpTrade",
3216
+ args: [
3217
+ params.agentId,
3218
+ params.configHash,
3219
+ params.instrument,
3220
+ params.isLong,
3221
+ params.notionalUSD,
3222
+ params.feeUSD,
3223
+ params.fillId
3224
+ ],
3225
+ account: this.account
3226
+ });
3227
+ const txHash = await this.walletClient.writeContract(request);
3228
+ console.log(`Perp trade recorded: ${params.instrument} ${params.isLong ? "LONG" : "SHORT"} $${Number(params.notionalUSD) / 1e6} \u2014 tx: ${txHash}`);
3229
+ return { success: true, txHash };
3230
+ } catch (error) {
3231
+ const message = error instanceof Error ? error.message : String(error);
3232
+ if (message.includes("DuplicateFill") || message.includes("already recorded")) {
3233
+ console.log(`Fill already recorded on-chain: ${params.fillId}`);
3234
+ return { success: true };
3235
+ }
3236
+ console.error(`Failed to record perp trade: ${message}`);
3237
+ this.retryQueue.push({
3238
+ params,
3239
+ retries: 0,
3240
+ lastAttempt: Date.now()
3241
+ });
3242
+ return { success: false, error: message };
3243
+ }
3244
+ }
3245
+ /**
3246
+ * Process the retry queue — attempt to re-submit failed recordings.
3247
+ */
3248
+ async processRetryQueue() {
3249
+ if (this.retryQueue.length === 0) return;
3250
+ const now = Date.now();
3251
+ const toRetry = this.retryQueue.filter(
3252
+ (item) => now - item.lastAttempt >= RETRY_DELAY_MS
3253
+ );
3254
+ for (const item of toRetry) {
3255
+ item.retries++;
3256
+ item.lastAttempt = now;
3257
+ if (item.retries > MAX_RETRIES) {
3258
+ console.error(
3259
+ `Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
3260
+ );
3261
+ const idx = this.retryQueue.indexOf(item);
3262
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3263
+ continue;
3264
+ }
3265
+ console.log(
3266
+ `Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
3267
+ );
3268
+ const result = await this.submitRecord(item.params);
3269
+ if (result.success) {
3270
+ const idx = this.retryQueue.indexOf(item);
3271
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3272
+ }
3273
+ }
3274
+ }
3275
+ // ============================================================
3276
+ // CONVERSION HELPERS
3277
+ // ============================================================
3278
+ /**
3279
+ * Calculate notional USD from a fill (6-decimal).
3280
+ * notionalUSD = px * sz * 1e6
3281
+ */
3282
+ calculateNotionalUSD(fill) {
3283
+ const px = parseFloat(fill.px);
3284
+ const sz = parseFloat(fill.sz);
3285
+ return BigInt(Math.round(px * sz * 1e6));
3286
+ }
3287
+ /**
3288
+ * Calculate fee USD from a fill (6-decimal).
3289
+ * feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
3290
+ */
3291
+ calculateFeeUSD(fill) {
3292
+ const fee = parseFloat(fill.fee);
3293
+ const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
3294
+ return BigInt(Math.round((fee + builderFee) * 1e6));
3295
+ }
3296
+ };
3297
+
3298
+ // src/perp/onboarding.ts
3299
+ var PerpOnboarding = class {
3300
+ client;
3301
+ signer;
3302
+ config;
3303
+ constructor(client, signer, config) {
3304
+ this.client = client;
3305
+ this.signer = signer;
3306
+ this.config = config;
3307
+ }
3308
+ // ============================================================
3309
+ // BUILDER FEE
3310
+ // ============================================================
3311
+ /**
3312
+ * Check if the user has approved the builder fee.
3313
+ * Builder fee must be approved before orders can include builder fees.
3314
+ */
3315
+ async isBuilderFeeApproved() {
3316
+ try {
3317
+ const maxFee = await this.client.getMaxBuilderFee(
3318
+ this.signer.getAddress(),
3319
+ this.config.builderAddress
3320
+ );
3321
+ return maxFee >= this.config.builderFeeTenthsBps;
3322
+ } catch {
3323
+ return false;
3324
+ }
3325
+ }
3326
+ /**
3327
+ * Approve the builder fee on Hyperliquid.
3328
+ * This is a one-time approval per builder address.
3329
+ */
3330
+ async approveBuilderFee() {
3331
+ try {
3332
+ const action = {
3333
+ type: "approveBuilderFee",
3334
+ hyperliquidChain: "Mainnet",
3335
+ maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
3336
+ builder: this.config.builderAddress,
3337
+ nonce: Number(getNextNonce())
3338
+ };
3339
+ const { signature } = await this.signer.signApproval(action);
3340
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
3341
+ method: "POST",
3342
+ headers: { "Content-Type": "application/json" },
3343
+ body: JSON.stringify({
3344
+ action,
3345
+ signature: {
3346
+ r: signature.slice(0, 66),
3347
+ s: `0x${signature.slice(66, 130)}`,
3348
+ v: parseInt(signature.slice(130, 132), 16)
3349
+ },
3350
+ nonce: action.nonce,
3351
+ vaultAddress: null
3352
+ })
3353
+ });
3354
+ if (!resp.ok) {
3355
+ const text = await resp.text();
3356
+ console.error(`Builder fee approval failed: ${resp.status} ${text}`);
3357
+ return false;
3358
+ }
3359
+ console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
3360
+ return true;
3361
+ } catch (error) {
3362
+ const message = error instanceof Error ? error.message : String(error);
3363
+ console.error(`Builder fee approval failed: ${message}`);
3364
+ return false;
3365
+ }
3366
+ }
3367
+ // ============================================================
3368
+ // BALANCE & REQUIREMENTS
3369
+ // ============================================================
3370
+ /**
3371
+ * Check if the user has sufficient USDC balance on Hyperliquid.
3372
+ * Returns the account equity in USD.
3373
+ */
3374
+ async checkBalance() {
3375
+ try {
3376
+ const account = await this.client.getAccountSummary(this.signer.getAddress());
3377
+ return {
3378
+ hasBalance: account.totalEquity > 0,
3379
+ equity: account.totalEquity
3380
+ };
3381
+ } catch {
3382
+ return { hasBalance: false, equity: 0 };
3383
+ }
3384
+ }
3385
+ /**
3386
+ * Verify that the agent's risk universe allows perp trading.
3387
+ * Perps require risk universe >= 2 (Derivatives or higher).
3388
+ */
3389
+ verifyRiskUniverse(riskUniverse) {
3390
+ if (riskUniverse >= 2) {
3391
+ return {
3392
+ allowed: true,
3393
+ message: `Risk universe ${riskUniverse} allows perp trading`
3394
+ };
3395
+ }
3396
+ return {
3397
+ allowed: false,
3398
+ message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
3399
+ };
3400
+ }
3401
+ // ============================================================
3402
+ // FULL ONBOARDING CHECK
3403
+ // ============================================================
3404
+ /**
3405
+ * Run all onboarding checks and return status.
3406
+ * Does NOT auto-approve — caller must explicitly approve after review.
3407
+ */
3408
+ async checkOnboardingStatus(riskUniverse) {
3409
+ const riskCheck = this.verifyRiskUniverse(riskUniverse);
3410
+ const balanceCheck = await this.checkBalance();
3411
+ const builderFeeApproved = await this.isBuilderFeeApproved();
3412
+ const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
3413
+ return {
3414
+ ready,
3415
+ riskUniverseOk: riskCheck.allowed,
3416
+ riskUniverseMessage: riskCheck.message,
3417
+ hasBalance: balanceCheck.hasBalance,
3418
+ equity: balanceCheck.equity,
3419
+ builderFeeApproved,
3420
+ builderAddress: this.config.builderAddress,
3421
+ builderFeeBps: this.config.builderFeeTenthsBps / 10
3422
+ };
3423
+ }
3424
+ /**
3425
+ * Run full onboarding: check status and auto-approve builder fee if needed.
3426
+ * Returns the final status after all actions.
3427
+ */
3428
+ async onboard(riskUniverse) {
3429
+ let status = await this.checkOnboardingStatus(riskUniverse);
3430
+ if (!status.riskUniverseOk) {
3431
+ console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
3432
+ return status;
3433
+ }
3434
+ if (!status.hasBalance) {
3435
+ console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
3436
+ return status;
3437
+ }
3438
+ if (!status.builderFeeApproved) {
3439
+ console.log("Approving builder fee...");
3440
+ const approved = await this.approveBuilderFee();
3441
+ if (approved) {
3442
+ status = { ...status, builderFeeApproved: true, ready: true };
3443
+ }
3444
+ }
3445
+ if (status.ready) {
3446
+ console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
3447
+ }
3448
+ return status;
3449
+ }
3450
+ };
3451
+
3452
+ // src/perp/funding.ts
3453
+ var import_viem5 = require("viem");
3454
+ var import_chains3 = require("viem/chains");
3455
+ var import_accounts4 = require("viem/accounts");
3456
+ var ERC20_ABI = (0, import_viem5.parseAbi)([
3457
+ "function approve(address spender, uint256 amount) external returns (bool)",
3458
+ "function balanceOf(address account) external view returns (uint256)",
3459
+ "function allowance(address owner, address spender) external view returns (uint256)"
3460
+ ]);
3461
+ var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
3462
+ "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3463
+ "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3464
+ ]);
3465
+ var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
3466
+ "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3467
+ "event MessageSent(bytes message)"
3468
+ ]);
3469
+ var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
3470
+ "function deposit(uint256 amount, uint32 destinationDex) external"
3471
+ ]);
3472
+
3473
+ // src/runtime.ts
3474
+ var FUNDS_LOW_THRESHOLD = 5e-3;
3475
+ var FUNDS_CRITICAL_THRESHOLD = 1e-3;
3476
+ var AgentRuntime = class {
3477
+ config;
3478
+ client;
3479
+ llm;
3480
+ strategy;
3481
+ executor;
3482
+ riskManager;
3483
+ marketData;
3484
+ vaultManager;
3485
+ relay = null;
3486
+ isRunning = false;
3487
+ mode = "idle";
3488
+ configHash;
3489
+ cycleCount = 0;
3490
+ lastCycleAt = 0;
3491
+ lastPortfolioValue = 0;
3492
+ lastEthBalance = "0";
3493
+ processAlive = true;
3494
+ riskUniverse = 0;
3495
+ allowedTokens = /* @__PURE__ */ new Set();
3496
+ // Perp trading components (null if perp not enabled)
3497
+ perpClient = null;
3498
+ perpSigner = null;
3499
+ perpOrders = null;
3500
+ perpPositions = null;
3501
+ perpWebSocket = null;
3502
+ perpRecorder = null;
3503
+ perpOnboarding = null;
3504
+ perpStrategy = null;
3505
+ // Two-layer perp control:
3506
+ // perpConnected = Hyperliquid infrastructure is initialized (WS, signer, recorder ready)
3507
+ // perpTradingActive = Dedicated perp trading cycle is mandated to run
3508
+ // When perpConnected && !perpTradingActive: agent's strategy can optionally return perp signals
3509
+ // When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
3510
+ perpConnected = false;
3511
+ perpTradingActive = false;
3512
+ constructor(config) {
3513
+ this.config = config;
3514
+ }
3515
+ /**
3516
+ * Initialize the agent runtime
3517
+ */
3518
+ async initialize() {
3519
+ console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3520
+ this.client = new import_sdk2.ExagentClient({
3521
+ privateKey: this.config.privateKey,
3522
+ network: this.config.network
3523
+ });
3524
+ console.log(`Wallet: ${this.client.address}`);
3525
+ const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
3526
+ if (!agent) {
3527
+ throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
3528
+ }
3529
+ console.log(`Agent verified: ${agent.name}`);
3530
+ await this.ensureWalletLinked();
3531
+ await this.loadTradingRestrictions();
3532
+ console.log(`Initializing LLM: ${this.config.llm.provider}`);
3533
+ this.llm = await createLLMAdapter(this.config.llm);
3534
+ const llmMeta = this.llm.getMetadata();
3535
+ console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3536
+ await this.syncConfigHash();
3537
+ this.strategy = await loadStrategy();
3538
+ this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3539
+ this.riskManager = new RiskManager(this.config.trading);
3540
+ this.marketData = new MarketDataService(this.getRpcUrl());
3541
+ await this.initializeVaultManager();
3542
+ await this.initializePerp();
3543
+ await this.initializeRelay();
3544
+ console.log("Agent initialized successfully");
3545
+ }
3546
+ /**
3547
+ * Initialize the relay client for command center connectivity
3548
+ */
3549
+ async initializeRelay() {
3550
+ const relayConfig = this.config.relay;
3551
+ const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
3552
+ if (!relayConfig?.enabled || !relayEnabled) {
3553
+ console.log("Relay: Disabled");
3554
+ return;
3555
+ }
3556
+ const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
3557
+ if (!apiUrl) {
3558
+ console.log("Relay: No API URL configured, skipping");
3559
+ return;
3560
+ }
3561
+ this.relay = new RelayClient({
3562
+ agentId: String(this.config.agentId),
3563
+ privateKey: this.config.privateKey,
3564
+ relay: {
3565
+ ...relayConfig,
3566
+ apiUrl
3567
+ },
3568
+ onCommand: (cmd) => this.handleCommand(cmd)
3569
+ });
3570
+ try {
3571
+ await this.relay.connect();
3572
+ console.log("Relay: Connected to command center");
3573
+ this.sendRelayStatus();
3574
+ } catch (error) {
3575
+ console.warn(
3576
+ "Relay: Failed to connect (agent will work locally):",
3577
+ error instanceof Error ? error.message : error
3578
+ );
3579
+ }
3580
+ }
3581
+ /**
3582
+ * Initialize the vault manager based on config
3583
+ */
3584
+ async initializeVaultManager() {
3585
+ const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
3586
+ this.vaultManager = new VaultManager({
3587
+ agentId: BigInt(this.config.agentId),
3588
+ agentName: this.config.name,
3589
+ network: this.config.network,
3590
+ walletKey: this.config.privateKey,
3591
+ vaultConfig
3592
+ });
3593
+ console.log(`Vault policy: ${vaultConfig.policy}`);
3594
+ const status = await this.vaultManager.getVaultStatus();
3595
+ if (status.hasVault) {
3596
+ console.log(`Vault exists: ${status.vaultAddress}`);
3597
+ console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
3598
+ } else {
3599
+ console.log("No vault exists for this agent");
3600
+ if (vaultConfig.policy === "manual") {
3601
+ console.log("Vault creation is manual \u2014 use the command center to create one");
3602
+ }
3603
+ }
3604
+ }
3605
+ /**
3606
+ * Initialize Hyperliquid perp trading components.
3607
+ * Only initializes if perp is enabled in config AND risk universe >= 2.
3608
+ */
3609
+ async initializePerp() {
3610
+ const perpConfig = this.config.perp;
3611
+ if (!perpConfig?.enabled) {
3612
+ console.log("Perp trading: Disabled");
3613
+ return;
3614
+ }
3615
+ if (this.riskUniverse < 2) {
3616
+ console.warn(`Perp trading: Blocked by risk universe ${this.riskUniverse} (need >= 2)`);
3617
+ return;
3618
+ }
3619
+ try {
3620
+ const config = {
3621
+ enabled: true,
3622
+ apiUrl: perpConfig.apiUrl || "https://api.hyperliquid.xyz",
3623
+ wsUrl: perpConfig.wsUrl || "wss://api.hyperliquid.xyz/ws",
3624
+ builderAddress: perpConfig.builderAddress,
3625
+ builderFeeTenthsBps: perpConfig.builderFeeTenthsBps ?? 100,
3626
+ maxLeverage: perpConfig.maxLeverage ?? 10,
3627
+ maxNotionalUSD: perpConfig.maxNotionalUSD ?? 5e4,
3628
+ allowedInstruments: perpConfig.allowedInstruments
3629
+ };
3630
+ this.perpClient = new HyperliquidClient(config);
3631
+ const perpKey = perpConfig.perpRelayerKey || this.config.privateKey;
3632
+ const account = (0, import_accounts5.privateKeyToAccount)(perpKey);
3633
+ const walletClient = (0, import_viem6.createWalletClient)({
3634
+ chain: { id: 42161, name: "Arbitrum", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } } },
3635
+ transport: (0, import_viem6.http)("https://arb1.arbitrum.io/rpc"),
3636
+ account
3637
+ });
3638
+ this.perpSigner = new HyperliquidSigner(walletClient);
3639
+ this.perpOrders = new OrderManager(this.perpClient, this.perpSigner, config);
3640
+ this.perpPositions = new PositionManager(this.perpClient, this.perpSigner.getAddress());
3641
+ this.perpOnboarding = new PerpOnboarding(this.perpClient, this.perpSigner, config);
3642
+ const recorderKey = perpConfig.perpRelayerKey || this.config.privateKey;
3643
+ this.perpRecorder = new PerpTradeRecorder({
3644
+ privateKey: recorderKey,
3645
+ rpcUrl: this.getRpcUrl(),
3646
+ agentId: BigInt(this.config.agentId),
3647
+ configHash: this.configHash
3648
+ });
3649
+ this.perpWebSocket = new HyperliquidWebSocket(config, this.perpSigner.getAddress(), this.perpClient);
3650
+ this.perpWebSocket.onFillReceived(async (fill) => {
3651
+ console.log(`Perp fill: ${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz}@${fill.px}`);
3652
+ const result = await this.perpRecorder.recordFill(fill);
3653
+ if (result.success) {
3654
+ this.relay?.sendMessage(
3655
+ "perp_fill",
3656
+ "success",
3657
+ "Perp Fill",
3658
+ `${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz} @ $${fill.px}`,
3659
+ { instrument: fill.coin, side: fill.side, size: fill.sz, price: fill.px, txHash: result.txHash }
3660
+ );
3661
+ }
3662
+ });
3663
+ this.perpWebSocket.onLiquidationDetected((instrument, size) => {
3664
+ console.error(`LIQUIDATION: ${instrument} position (${size}) was liquidated`);
3665
+ this.relay?.sendMessage(
3666
+ "perp_liquidation_warning",
3667
+ "error",
3668
+ "Position Liquidated",
3669
+ `${instrument} position of ${Math.abs(size)} was liquidated.`,
3670
+ { instrument, size }
3671
+ );
3672
+ });
3673
+ this.perpWebSocket.onFundingReceived((funding) => {
3674
+ const amount = parseFloat(funding.usdc);
3675
+ if (Math.abs(amount) > 0.01) {
3676
+ this.relay?.sendMessage(
3677
+ "perp_funding",
3678
+ "info",
3679
+ "Funding Payment",
3680
+ `${funding.coin}: ${amount > 0 ? "+" : ""}$${amount.toFixed(4)}`,
3681
+ { instrument: funding.coin, amount: funding.usdc, rate: funding.fundingRate }
3682
+ );
3683
+ }
3684
+ });
3685
+ const onboardingStatus = await this.perpOnboarding.onboard(this.riskUniverse);
3686
+ if (!onboardingStatus.ready) {
3687
+ console.warn(`Perp onboarding incomplete \u2014 trading will be limited`);
3688
+ if (!onboardingStatus.hasBalance) {
3689
+ console.warn(" No USDC balance on Hyperliquid \u2014 deposit required");
3690
+ }
3691
+ if (!onboardingStatus.builderFeeApproved) {
3692
+ console.warn(" Builder fee not approved \u2014 orders may fail");
3693
+ }
3694
+ }
3695
+ try {
3696
+ await this.perpWebSocket.connect();
3697
+ console.log("Perp WebSocket: Connected");
3698
+ } catch (error) {
3699
+ console.warn("Perp WebSocket: Failed to connect (will retry):", error instanceof Error ? error.message : error);
3700
+ }
3701
+ this.perpConnected = true;
3702
+ console.log(`Hyperliquid: Connected (${config.allowedInstruments?.join(", ") || "all instruments"})`);
3703
+ console.log(` Builder: ${config.builderAddress} (${config.builderFeeTenthsBps / 10} bps)`);
3704
+ console.log(` Max leverage: ${config.maxLeverage}x, Max notional: $${config.maxNotionalUSD.toLocaleString()}`);
3705
+ } catch (error) {
3706
+ const message = error instanceof Error ? error.message : String(error);
3707
+ console.error(`Perp initialization failed: ${message}`);
3708
+ console.warn("Perp trading will be disabled for this session");
3709
+ }
3710
+ }
3711
+ /**
3712
+ * Ensure the current wallet is linked to the agent.
3713
+ * If the trading wallet differs from the owner, enters a recovery loop
3714
+ * that waits for the owner to link it from the website.
3715
+ */
3716
+ async ensureWalletLinked() {
3717
+ const agentId = BigInt(this.config.agentId);
3718
+ const address = this.client.address;
3719
+ const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
2232
3720
  if (!isLinked) {
2233
3721
  console.log("Wallet not linked, linking now...");
2234
3722
  const agent = await this.client.registry.getAgent(agentId);
@@ -2339,10 +3827,10 @@ var AgentRuntime = class {
2339
3827
  const message = error instanceof Error ? error.message : String(error);
2340
3828
  if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
2341
3829
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
2342
- const chain = import_chains2.base;
2343
- const publicClientInstance = (0, import_viem3.createPublicClient)({
3830
+ const chain = import_chains4.base;
3831
+ const publicClientInstance = (0, import_viem6.createPublicClient)({
2344
3832
  chain,
2345
- transport: (0, import_viem3.http)(this.getRpcUrl())
3833
+ transport: (0, import_viem6.http)(this.getRpcUrl())
2346
3834
  });
2347
3835
  console.log("");
2348
3836
  console.log("=== ETH NEEDED FOR GAS ===");
@@ -2553,6 +4041,131 @@ var AgentRuntime = class {
2553
4041
  }
2554
4042
  break;
2555
4043
  }
4044
+ case "enable_hyperliquid":
4045
+ if (this.perpConnected) {
4046
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already connected");
4047
+ break;
4048
+ }
4049
+ if (!this.config.perp?.enabled) {
4050
+ this.relay?.sendCommandResult(cmd.id, false, "Perp trading not configured in agent config");
4051
+ break;
4052
+ }
4053
+ if (this.riskUniverse < 2) {
4054
+ this.relay?.sendCommandResult(cmd.id, false, `Risk universe ${this.riskUniverse} too low (need >= 2)`);
4055
+ break;
4056
+ }
4057
+ try {
4058
+ await this.initializePerp();
4059
+ if (this.perpConnected) {
4060
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid connected");
4061
+ this.relay?.sendMessage(
4062
+ "system",
4063
+ "success",
4064
+ "Hyperliquid Enabled",
4065
+ 'Hyperliquid infrastructure connected. Agent can now include perp signals in its strategy. Use "Start Perp Trading" to mandate a dedicated perp cycle.'
4066
+ );
4067
+ } else {
4068
+ this.relay?.sendCommandResult(cmd.id, false, "Failed to connect to Hyperliquid");
4069
+ }
4070
+ } catch (error) {
4071
+ const msg = error instanceof Error ? error.message : String(error);
4072
+ this.relay?.sendCommandResult(cmd.id, false, `Hyperliquid init failed: ${msg}`);
4073
+ }
4074
+ this.sendRelayStatus();
4075
+ break;
4076
+ case "disable_hyperliquid":
4077
+ if (!this.perpConnected) {
4078
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already disconnected");
4079
+ break;
4080
+ }
4081
+ this.perpTradingActive = false;
4082
+ if (this.perpWebSocket) {
4083
+ this.perpWebSocket.disconnect();
4084
+ }
4085
+ if (this.perpRecorder) {
4086
+ this.perpRecorder.stop();
4087
+ }
4088
+ this.perpConnected = false;
4089
+ console.log("Hyperliquid disabled via command center");
4090
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid disconnected");
4091
+ this.relay?.sendMessage(
4092
+ "system",
4093
+ "info",
4094
+ "Hyperliquid Disabled",
4095
+ "Hyperliquid infrastructure disconnected. Agent will trade spot only."
4096
+ );
4097
+ this.sendRelayStatus();
4098
+ break;
4099
+ case "start_perp_trading":
4100
+ if (!this.perpConnected) {
4101
+ this.relay?.sendCommandResult(cmd.id, false, "Hyperliquid not connected. Enable Hyperliquid first.");
4102
+ break;
4103
+ }
4104
+ if (this.perpTradingActive) {
4105
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading already active");
4106
+ break;
4107
+ }
4108
+ this.perpTradingActive = true;
4109
+ if (this.mode !== "trading") {
4110
+ this.mode = "trading";
4111
+ this.isRunning = true;
4112
+ }
4113
+ console.log("Perp trading mandated via command center");
4114
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle active");
4115
+ this.relay?.sendMessage(
4116
+ "system",
4117
+ "success",
4118
+ "Perp Trading Active",
4119
+ "Dedicated perp trading cycle is now running every interval."
4120
+ );
4121
+ this.sendRelayStatus();
4122
+ break;
4123
+ case "stop_perp_trading":
4124
+ if (!this.perpTradingActive) {
4125
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading already stopped");
4126
+ break;
4127
+ }
4128
+ this.perpTradingActive = false;
4129
+ console.log("Perp trading cycle stopped via command center");
4130
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle stopped");
4131
+ this.relay?.sendMessage(
4132
+ "system",
4133
+ "info",
4134
+ "Perp Trading Stopped",
4135
+ "Dedicated perp cycle stopped. Hyperliquid remains connected \u2014 strategy can still include perp signals."
4136
+ );
4137
+ this.sendRelayStatus();
4138
+ break;
4139
+ case "update_perp_params": {
4140
+ const perpParams = cmd.params || {};
4141
+ let perpUpdated = false;
4142
+ if (this.config.perp && perpParams.maxLeverage !== void 0) {
4143
+ const val = Number(perpParams.maxLeverage);
4144
+ if (val >= 1 && val <= 50) {
4145
+ this.config.perp.maxLeverage = val;
4146
+ perpUpdated = true;
4147
+ }
4148
+ }
4149
+ if (this.config.perp && perpParams.maxNotionalUSD !== void 0) {
4150
+ const val = Number(perpParams.maxNotionalUSD);
4151
+ if (val >= 100) {
4152
+ this.config.perp.maxNotionalUSD = val;
4153
+ perpUpdated = true;
4154
+ }
4155
+ }
4156
+ if (perpUpdated) {
4157
+ this.relay?.sendCommandResult(cmd.id, true, "Perp parameters updated");
4158
+ this.relay?.sendMessage(
4159
+ "config_updated",
4160
+ "info",
4161
+ "Perp Params Updated",
4162
+ `Max leverage: ${this.config.perp?.maxLeverage}x, Max notional: $${this.config.perp?.maxNotionalUSD?.toLocaleString()}`
4163
+ );
4164
+ } else {
4165
+ this.relay?.sendCommandResult(cmd.id, false, "No valid perp parameters provided");
4166
+ }
4167
+ break;
4168
+ }
2556
4169
  case "refresh_status":
2557
4170
  this.sendRelayStatus();
2558
4171
  this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
@@ -2607,8 +4220,33 @@ var AgentRuntime = class {
2607
4220
  policy: vaultConfig.policy,
2608
4221
  hasVault: false,
2609
4222
  vaultAddress: null
2610
- }
4223
+ },
4224
+ perp: this.perpConnected ? {
4225
+ enabled: true,
4226
+ trading: this.perpTradingActive,
4227
+ equity: 0,
4228
+ unrealizedPnl: 0,
4229
+ marginUsed: 0,
4230
+ openPositions: 0,
4231
+ effectiveLeverage: 0,
4232
+ pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4233
+ } : void 0
2611
4234
  };
4235
+ if (this.perpConnected && this.perpPositions && status.perp) {
4236
+ this.perpPositions.getAccountSummary().then((account) => {
4237
+ if (status.perp) {
4238
+ status.perp.equity = account.totalEquity;
4239
+ status.perp.unrealizedPnl = account.totalUnrealizedPnl;
4240
+ status.perp.marginUsed = account.totalMarginUsed;
4241
+ status.perp.effectiveLeverage = account.effectiveLeverage;
4242
+ }
4243
+ }).catch(() => {
4244
+ });
4245
+ this.perpPositions.getPositionCount().then((count) => {
4246
+ if (status.perp) status.perp.openPositions = count;
4247
+ }).catch(() => {
4248
+ });
4249
+ }
2612
4250
  this.relay.sendHeartbeat(status);
2613
4251
  }
2614
4252
  /**
@@ -2707,8 +4345,75 @@ var AgentRuntime = class {
2707
4345
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2708
4346
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
2709
4347
  }
4348
+ if (this.perpConnected && this.perpTradingActive) {
4349
+ try {
4350
+ await this.runPerpCycle();
4351
+ } catch (error) {
4352
+ const message = error instanceof Error ? error.message : String(error);
4353
+ console.error("Error in perp cycle:", message);
4354
+ this.relay?.sendMessage("system", "error", "Perp Cycle Error", message);
4355
+ }
4356
+ }
2710
4357
  this.sendRelayStatus();
2711
4358
  }
4359
+ /**
4360
+ * Run a single perp trading cycle.
4361
+ * Fetches market data, positions, calls perp strategy, applies risk filters, executes.
4362
+ * Fills arrive async via WebSocket and are auto-recorded on Base.
4363
+ */
4364
+ async runPerpCycle() {
4365
+ if (!this.perpClient || !this.perpPositions || !this.perpOrders || !this.perpConnected) return;
4366
+ const perpConfig = this.config.perp;
4367
+ if (!perpConfig?.enabled) return;
4368
+ console.log(" [PERP] Running perp cycle...");
4369
+ const [positions, account] = await Promise.all([
4370
+ this.perpPositions.getPositions(true),
4371
+ this.perpPositions.getAccountSummary(true)
4372
+ ]);
4373
+ console.log(` [PERP] Equity: $${account.totalEquity.toFixed(2)}, Positions: ${positions.length}, Leverage: ${account.effectiveLeverage.toFixed(1)}x`);
4374
+ const dangerousPositions = await this.perpPositions.getDangerousPositions(0.7);
4375
+ for (const pos of dangerousPositions) {
4376
+ console.warn(` [PERP] WARNING: ${pos.instrument} near liquidation`);
4377
+ this.relay?.sendMessage(
4378
+ "perp_liquidation_warning",
4379
+ "warning",
4380
+ "Near Liquidation",
4381
+ `${pos.instrument} ${pos.size > 0 ? "LONG" : "SHORT"} \u2014 close to liquidation price $${pos.liquidationPrice.toFixed(2)}`,
4382
+ { instrument: pos.instrument, liquidationPrice: pos.liquidationPrice, markPrice: pos.markPrice }
4383
+ );
4384
+ }
4385
+ const instruments = perpConfig.allowedInstruments || ["ETH", "BTC", "SOL"];
4386
+ const marketData = await this.perpClient.getMarketData(instruments);
4387
+ if (!this.perpStrategy) {
4388
+ return;
4389
+ }
4390
+ let signals;
4391
+ try {
4392
+ signals = await this.perpStrategy(marketData, positions, account, this.llm, this.config);
4393
+ } catch (error) {
4394
+ const message = error instanceof Error ? error.message : String(error);
4395
+ console.error(" [PERP] Strategy error:", message);
4396
+ return;
4397
+ }
4398
+ console.log(` [PERP] Strategy generated ${signals.length} signals`);
4399
+ const maxLeverage = perpConfig.maxLeverage ?? 10;
4400
+ const maxNotionalUSD = perpConfig.maxNotionalUSD ?? 5e4;
4401
+ const filteredSignals = this.riskManager.filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD);
4402
+ console.log(` [PERP] ${filteredSignals.length} signals passed risk checks`);
4403
+ for (const signal of filteredSignals) {
4404
+ if (signal.action === "hold") continue;
4405
+ if (signal.price === 0) {
4406
+ const md = marketData.find((m) => m.instrument === signal.instrument);
4407
+ if (md) signal.price = md.midPrice;
4408
+ }
4409
+ const result = await this.perpOrders.placeOrder(signal);
4410
+ if (result.success) {
4411
+ console.log(` [PERP] Order placed: ${signal.instrument} ${signal.action} \u2014 ${result.status}`);
4412
+ } else {
4413
+ console.warn(` [PERP] Order failed: ${signal.instrument} ${signal.action} \u2014 ${result.error}`);
4414
+ }
4415
+ }
4416
+ }
2712
4417
  /**
2713
4418
  * Check if ETH balance is below threshold and notify.
2714
4419
  * Returns true if trading should continue, false if ETH is critically low.
@@ -2754,6 +4459,12 @@ var AgentRuntime = class {
2754
4459
  this.isRunning = false;
2755
4460
  this.processAlive = false;
2756
4461
  this.mode = "idle";
4462
+ if (this.perpWebSocket) {
4463
+ this.perpWebSocket.disconnect();
4464
+ }
4465
+ if (this.perpRecorder) {
4466
+ this.perpRecorder.stop();
4467
+ }
2757
4468
  if (this.relay) {
2758
4469
  this.relay.disconnect();
2759
4470
  }
@@ -3094,12 +4805,21 @@ function loadSecureEnv(basePath, passphrase) {
3094
4805
  DeepSeekAdapter,
3095
4806
  GoogleAdapter,
3096
4807
  GroqAdapter,
4808
+ HYPERLIQUID_DOMAIN,
4809
+ HyperliquidClient,
4810
+ HyperliquidSigner,
4811
+ HyperliquidWebSocket,
3097
4812
  LLMConfigSchema,
3098
4813
  LLMProviderSchema,
3099
4814
  MarketDataService,
3100
4815
  MistralAdapter,
3101
4816
  OllamaAdapter,
3102
4817
  OpenAIAdapter,
4818
+ OrderManager,
4819
+ PerpConfigSchema,
4820
+ PerpOnboarding,
4821
+ PerpTradeRecorder,
4822
+ PositionManager,
3103
4823
  RelayClient,
3104
4824
  RelayConfigSchema,
3105
4825
  RiskManager,
@@ -3115,7 +4835,10 @@ function loadSecureEnv(basePath, passphrase) {
3115
4835
  createSampleConfig,
3116
4836
  decryptEnvFile,
3117
4837
  encryptEnvFile,
4838
+ fillHashToBytes32,
4839
+ fillOidToBytes32,
3118
4840
  getAllStrategyTemplates,
4841
+ getNextNonce,
3119
4842
  getStrategyTemplate,
3120
4843
  loadConfig,
3121
4844
  loadSecureEnv,