@exagent/agent 0.1.17 → 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/cli.js CHANGED
@@ -32,8 +32,9 @@ var path2 = __toESM(require("path"));
32
32
 
33
33
  // src/runtime.ts
34
34
  var import_sdk2 = require("@exagent/sdk");
35
- var import_viem3 = require("viem");
36
- var import_chains2 = require("viem/chains");
35
+ var import_viem6 = require("viem");
36
+ var import_chains4 = require("viem/chains");
37
+ var import_accounts5 = require("viem/accounts");
37
38
 
38
39
  // src/llm/openai.ts
39
40
  var import_openai = __toESM(require("openai"));
@@ -893,6 +894,26 @@ var RelayConfigSchema = import_zod.z.object({
893
894
  apiUrl: import_zod.z.string().url(),
894
895
  heartbeatIntervalMs: import_zod.z.number().min(5e3).default(3e4)
895
896
  }).optional();
897
+ var PerpConfigSchema = import_zod.z.object({
898
+ /** Enable perp trading */
899
+ enabled: import_zod.z.boolean().default(false),
900
+ /** Hyperliquid REST API URL */
901
+ apiUrl: import_zod.z.string().url().default("https://api.hyperliquid.xyz"),
902
+ /** Hyperliquid WebSocket URL */
903
+ wsUrl: import_zod.z.string().default("wss://api.hyperliquid.xyz/ws"),
904
+ /** Builder address for fee collection (must have >= 100 USDC on HL) */
905
+ builderAddress: import_zod.z.string(),
906
+ /** Builder fee in tenths of basis points (100 = 10 bps = 0.10%) */
907
+ builderFeeTenthsBps: import_zod.z.number().min(0).max(500).default(100),
908
+ /** Private key for the perp relayer (calls recordPerpTrade on Base). Falls back to agent wallet. */
909
+ perpRelayerKey: import_zod.z.string().optional(),
910
+ /** Maximum leverage per position (default: 10) */
911
+ maxLeverage: import_zod.z.number().min(1).max(50).default(10),
912
+ /** Maximum notional position size in USD (default: 50000) */
913
+ maxNotionalUSD: import_zod.z.number().min(100).default(5e4),
914
+ /** Allowed perp instruments (e.g. ["ETH", "BTC", "SOL"]). If empty, all instruments allowed. */
915
+ allowedInstruments: import_zod.z.array(import_zod.z.string()).optional()
916
+ }).optional();
896
917
  var AgentConfigSchema = import_zod.z.object({
897
918
  // Identity (from on-chain registration)
898
919
  agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
@@ -910,6 +931,8 @@ var AgentConfigSchema = import_zod.z.object({
910
931
  vault: VaultConfigSchema.default({}),
911
932
  // Relay configuration (command center)
912
933
  relay: RelayConfigSchema,
934
+ // Perp trading configuration (Hyperliquid)
935
+ perp: PerpConfigSchema,
913
936
  // Allowed tokens (addresses)
914
937
  allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
915
938
  });
@@ -1465,6 +1488,101 @@ var RiskManager = class {
1465
1488
  isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
1466
1489
  };
1467
1490
  }
1491
+ // ============================================================
1492
+ // PERP RISK FILTERING
1493
+ // ============================================================
1494
+ /**
1495
+ * Filter perp trade signals through risk checks.
1496
+ * Reduce-only signals (closes) always pass.
1497
+ *
1498
+ * @param signals - Raw perp signals from strategy
1499
+ * @param positions - Current open positions on Hyperliquid
1500
+ * @param account - Current account equity and margin
1501
+ * @param maxLeverage - Maximum allowed leverage
1502
+ * @param maxNotionalUSD - Maximum notional per position
1503
+ * @returns Signals that pass risk checks
1504
+ */
1505
+ filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD) {
1506
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1507
+ if (today !== this.lastResetDate) {
1508
+ this.dailyPnL = 0;
1509
+ this.dailyFees = 0;
1510
+ this.lastResetDate = today;
1511
+ }
1512
+ if (this.isDailyLossLimitHit(account.totalEquity)) {
1513
+ console.warn("Daily loss limit reached \u2014 blocking new perp trades");
1514
+ return signals.filter((s) => s.reduceOnly);
1515
+ }
1516
+ return signals.filter((signal) => this.validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD));
1517
+ }
1518
+ /**
1519
+ * Validate an individual perp signal.
1520
+ */
1521
+ validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD) {
1522
+ if (signal.action === "hold") {
1523
+ return true;
1524
+ }
1525
+ if (signal.reduceOnly || signal.action === "close_long" || signal.action === "close_short") {
1526
+ return true;
1527
+ }
1528
+ if (signal.confidence < 0.5) {
1529
+ console.warn(`Perp signal confidence too low: ${signal.confidence} for ${signal.instrument}`);
1530
+ return false;
1531
+ }
1532
+ if (signal.leverage > maxLeverage) {
1533
+ console.warn(`Perp signal leverage ${signal.leverage}x exceeds max ${maxLeverage}x for ${signal.instrument}`);
1534
+ return false;
1535
+ }
1536
+ const signalNotional = signal.size * signal.price;
1537
+ if (signalNotional > maxNotionalUSD) {
1538
+ console.warn(`Perp signal notional $${signalNotional.toFixed(0)} exceeds max $${maxNotionalUSD} for ${signal.instrument}`);
1539
+ return false;
1540
+ }
1541
+ const currentNotional = account.totalNotional;
1542
+ const projectedNotional = currentNotional + signalNotional;
1543
+ const projectedLeverage = account.totalEquity > 0 ? projectedNotional / account.totalEquity : 0;
1544
+ if (projectedLeverage > maxLeverage) {
1545
+ console.warn(
1546
+ `Perp signal would push aggregate leverage to ${projectedLeverage.toFixed(1)}x (max: ${maxLeverage}x) \u2014 blocked`
1547
+ );
1548
+ return false;
1549
+ }
1550
+ const existingPos = positions.find((p) => p.instrument === signal.instrument);
1551
+ if (existingPos) {
1552
+ const liqProximity = this.calculateLiquidationProximity(existingPos);
1553
+ if (liqProximity > 0.7) {
1554
+ console.warn(
1555
+ `Position ${signal.instrument} liquidation proximity ${(liqProximity * 100).toFixed(0)}% \u2014 blocking new entry`
1556
+ );
1557
+ return false;
1558
+ }
1559
+ }
1560
+ const requiredMargin = signalNotional / signal.leverage;
1561
+ if (requiredMargin > account.availableMargin) {
1562
+ console.warn(
1563
+ `Insufficient margin for ${signal.instrument}: need $${requiredMargin.toFixed(0)}, have $${account.availableMargin.toFixed(0)}`
1564
+ );
1565
+ return false;
1566
+ }
1567
+ return true;
1568
+ }
1569
+ /**
1570
+ * Calculate liquidation proximity for a position (0.0 = safe, 1.0 = liquidated).
1571
+ */
1572
+ calculateLiquidationProximity(pos) {
1573
+ if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) return 0;
1574
+ if (pos.size > 0) {
1575
+ if (pos.markPrice <= pos.liquidationPrice) return 1;
1576
+ const distance = pos.markPrice - pos.liquidationPrice;
1577
+ const total = pos.entryPrice - pos.liquidationPrice;
1578
+ return total > 0 ? 1 - distance / total : 0;
1579
+ } else {
1580
+ if (pos.markPrice >= pos.liquidationPrice) return 1;
1581
+ const distance = pos.liquidationPrice - pos.markPrice;
1582
+ const total = pos.liquidationPrice - pos.entryPrice;
1583
+ return total > 0 ? 1 - distance / total : 0;
1584
+ }
1585
+ }
1468
1586
  };
1469
1587
 
1470
1588
  // src/vault/manager.ts
@@ -2032,139 +2150,1494 @@ function openBrowser(url) {
2032
2150
  }
2033
2151
  }
2034
2152
 
2035
- // src/runtime.ts
2036
- var FUNDS_LOW_THRESHOLD = 5e-3;
2037
- var FUNDS_CRITICAL_THRESHOLD = 1e-3;
2038
- var AgentRuntime = class {
2039
- config;
2040
- client;
2041
- llm;
2042
- strategy;
2043
- executor;
2044
- riskManager;
2045
- marketData;
2046
- vaultManager;
2047
- relay = null;
2048
- isRunning = false;
2049
- mode = "idle";
2050
- configHash;
2051
- cycleCount = 0;
2052
- lastCycleAt = 0;
2053
- lastPortfolioValue = 0;
2054
- lastEthBalance = "0";
2055
- processAlive = true;
2056
- riskUniverse = 0;
2057
- allowedTokens = /* @__PURE__ */ new Set();
2153
+ // src/perp/client.ts
2154
+ var HyperliquidClient = class {
2155
+ apiUrl;
2156
+ meta = null;
2157
+ assetIndexCache = /* @__PURE__ */ new Map();
2058
2158
  constructor(config) {
2059
- this.config = config;
2159
+ this.apiUrl = config.apiUrl;
2160
+ }
2161
+ // ============================================================
2162
+ // INFO API (read-only)
2163
+ // ============================================================
2164
+ /** Fetch perpetuals metadata (asset specs, names, indices) */
2165
+ async getMeta() {
2166
+ if (this.meta) return this.meta;
2167
+ const resp = await this.infoRequest({ type: "meta" });
2168
+ this.meta = resp.universe;
2169
+ this.meta.forEach((asset, idx) => {
2170
+ this.assetIndexCache.set(asset.name, idx);
2171
+ });
2172
+ return this.meta;
2173
+ }
2174
+ /** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
2175
+ async getAssetIndex(coin) {
2176
+ if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
2177
+ await this.getMeta();
2178
+ const idx = this.assetIndexCache.get(coin);
2179
+ if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
2180
+ return idx;
2181
+ }
2182
+ /** Get mid-market prices for all perpetuals */
2183
+ async getAllMids() {
2184
+ return this.infoRequest({ type: "allMids" });
2185
+ }
2186
+ /** Get clearinghouse state (positions, margin, equity) for a user */
2187
+ async getClearinghouseState(user) {
2188
+ return this.infoRequest({ type: "clearinghouseState", user });
2189
+ }
2190
+ /** Get user's recent fills */
2191
+ async getUserFills(user, startTime) {
2192
+ return this.infoRequest({
2193
+ type: "userFills",
2194
+ user,
2195
+ ...startTime !== void 0 && { startTime }
2196
+ });
2197
+ }
2198
+ /** Get user's fills in a time range */
2199
+ async getUserFillsByTime(user, startTime, endTime) {
2200
+ return this.infoRequest({
2201
+ type: "userFillsByTime",
2202
+ user,
2203
+ startTime,
2204
+ ...endTime !== void 0 && { endTime }
2205
+ });
2206
+ }
2207
+ /** Get user's open orders */
2208
+ async getOpenOrders(user) {
2209
+ return this.infoRequest({ type: "openOrders", user });
2210
+ }
2211
+ /** Get user's funding history */
2212
+ async getUserFunding(user, startTime) {
2213
+ return this.infoRequest({ type: "userFunding", user, startTime });
2214
+ }
2215
+ /** Check max approved builder fee for a user */
2216
+ async getMaxBuilderFee(user, builder) {
2217
+ const resp = await this.infoRequest({ type: "maxBuilderFee", user, builder });
2218
+ return resp;
2219
+ }
2220
+ /** Get L2 order book for a coin */
2221
+ async getL2Book(coin, depth) {
2222
+ return this.infoRequest({
2223
+ type: "l2Book",
2224
+ coin,
2225
+ ...depth !== void 0 && { nSigFigs: depth }
2226
+ });
2227
+ }
2228
+ // ============================================================
2229
+ // HIGH-LEVEL HELPERS
2230
+ // ============================================================
2231
+ /** Get parsed positions for a user address */
2232
+ async getPositions(user) {
2233
+ const state = await this.getClearinghouseState(user);
2234
+ return state.assetPositions.filter((p) => parseFloat(p.position.szi) !== 0).map((p) => this.parsePosition(p));
2235
+ }
2236
+ /** Get account summary (equity, margin, leverage) */
2237
+ async getAccountSummary(user) {
2238
+ const state = await this.getClearinghouseState(user);
2239
+ const crossMarginSummary = state.crossMarginSummary;
2240
+ const totalEquity = parseFloat(crossMarginSummary.accountValue);
2241
+ const totalNotional = parseFloat(crossMarginSummary.totalNtlPos);
2242
+ const totalMarginUsed = parseFloat(crossMarginSummary.totalMarginUsed);
2243
+ return {
2244
+ totalEquity,
2245
+ availableMargin: totalEquity - totalMarginUsed,
2246
+ totalMarginUsed,
2247
+ totalUnrealizedPnl: parseFloat(crossMarginSummary.totalRawUsd) - totalEquity,
2248
+ totalNotional,
2249
+ maintenanceMargin: totalMarginUsed * 0.5,
2250
+ // Approximate
2251
+ effectiveLeverage: totalEquity > 0 ? totalNotional / totalEquity : 0,
2252
+ cashBalance: parseFloat(state.crossMarginSummary.accountValue) - state.assetPositions.reduce(
2253
+ (sum, p) => sum + parseFloat(p.position.unrealizedPnl),
2254
+ 0
2255
+ )
2256
+ };
2257
+ }
2258
+ /** Get market data for a list of instruments */
2259
+ async getMarketData(instruments) {
2260
+ const mids = await this.getAllMids();
2261
+ const meta = await this.getMeta();
2262
+ return instruments.filter((inst) => mids[inst] !== void 0).map((inst) => {
2263
+ const midPrice = parseFloat(mids[inst]);
2264
+ const assetMeta = meta.find((m) => m.name === inst);
2265
+ return {
2266
+ instrument: inst,
2267
+ midPrice,
2268
+ bestBid: midPrice,
2269
+ // Approximate — use L2 book for exact
2270
+ bestAsk: midPrice,
2271
+ funding8h: 0,
2272
+ // Would need separate funding API call
2273
+ openInterest: 0,
2274
+ // Would need separate meta call
2275
+ volume24h: 0,
2276
+ priceChange24h: 0
2277
+ };
2278
+ });
2279
+ }
2280
+ /** Convert fills to our PerpFill type */
2281
+ parseFill(fill) {
2282
+ return {
2283
+ oid: fill.oid,
2284
+ coin: fill.coin,
2285
+ side: fill.side,
2286
+ px: fill.px,
2287
+ sz: fill.sz,
2288
+ fee: fill.fee,
2289
+ time: fill.time,
2290
+ hash: fill.hash,
2291
+ isMaker: fill.startPosition !== fill.px,
2292
+ // Approximate maker detection
2293
+ builderFee: fill.builderFee,
2294
+ liquidation: fill.liquidation
2295
+ };
2296
+ }
2297
+ // ============================================================
2298
+ // PRIVATE HELPERS
2299
+ // ============================================================
2300
+ parsePosition(ap) {
2301
+ const pos = ap.position;
2302
+ const size = parseFloat(pos.szi);
2303
+ const entryPrice = parseFloat(pos.entryPx || "0");
2304
+ const markPrice = parseFloat(pos.positionValue || "0") / Math.abs(size || 1);
2305
+ return {
2306
+ instrument: pos.coin,
2307
+ assetIndex: this.assetIndexCache.get(pos.coin) ?? -1,
2308
+ size,
2309
+ entryPrice,
2310
+ markPrice,
2311
+ unrealizedPnl: parseFloat(pos.unrealizedPnl),
2312
+ leverage: parseFloat(pos.leverage?.value || "1"),
2313
+ marginType: pos.leverage?.type === "isolated" ? "isolated" : "cross",
2314
+ liquidationPrice: parseFloat(pos.liquidationPx || "0"),
2315
+ notionalUSD: Math.abs(size) * markPrice,
2316
+ marginUsed: parseFloat(pos.marginUsed)
2317
+ };
2318
+ }
2319
+ async infoRequest(body) {
2320
+ const resp = await fetch(`${this.apiUrl}/info`, {
2321
+ method: "POST",
2322
+ headers: { "Content-Type": "application/json" },
2323
+ body: JSON.stringify(body)
2324
+ });
2325
+ if (!resp.ok) {
2326
+ throw new Error(`Hyperliquid Info API error: ${resp.status} ${await resp.text()}`);
2327
+ }
2328
+ return resp.json();
2329
+ }
2330
+ };
2331
+
2332
+ // src/perp/signer.ts
2333
+ var import_viem3 = require("viem");
2334
+ var HYPERLIQUID_DOMAIN = {
2335
+ name: "HyperliquidSignTransaction",
2336
+ version: "1",
2337
+ chainId: 42161n,
2338
+ // Always Arbitrum chain ID
2339
+ verifyingContract: "0x0000000000000000000000000000000000000000"
2340
+ };
2341
+ var HYPERLIQUID_TYPES = {
2342
+ HyperliquidTransaction: [
2343
+ { name: "hyperliquidChain", type: "string" },
2344
+ { name: "action", type: "string" },
2345
+ { name: "nonce", type: "uint64" }
2346
+ ]
2347
+ };
2348
+ var lastNonce = 0n;
2349
+ function getNextNonce() {
2350
+ const now = BigInt(Date.now());
2351
+ if (now <= lastNonce) {
2352
+ lastNonce = lastNonce + 1n;
2353
+ } else {
2354
+ lastNonce = now;
2355
+ }
2356
+ return lastNonce;
2357
+ }
2358
+ var HyperliquidSigner = class {
2359
+ constructor(walletClient) {
2360
+ this.walletClient = walletClient;
2060
2361
  }
2061
2362
  /**
2062
- * Initialize the agent runtime
2363
+ * Sign an exchange action (order, cancel, etc.)
2364
+ *
2365
+ * @param action - The action object (will be JSON-serialized)
2366
+ * @param nonce - Nonce (defaults to current timestamp)
2367
+ * @returns Signature hex string
2063
2368
  */
2064
- async initialize() {
2065
- console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
2066
- this.client = new import_sdk2.ExagentClient({
2067
- privateKey: this.config.privateKey,
2068
- network: this.config.network
2369
+ async signAction(action, nonce) {
2370
+ const actionNonce = nonce ?? getNextNonce();
2371
+ const actionStr = JSON.stringify(action);
2372
+ const account = this.walletClient.account;
2373
+ if (!account) throw new Error("Wallet client has no account");
2374
+ const signature = await this.walletClient.signTypedData({
2375
+ account,
2376
+ domain: HYPERLIQUID_DOMAIN,
2377
+ types: HYPERLIQUID_TYPES,
2378
+ primaryType: "HyperliquidTransaction",
2379
+ message: {
2380
+ hyperliquidChain: "Mainnet",
2381
+ action: actionStr,
2382
+ nonce: actionNonce
2383
+ }
2069
2384
  });
2070
- console.log(`Wallet: ${this.client.address}`);
2071
- const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
2072
- if (!agent) {
2073
- throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
2074
- }
2075
- console.log(`Agent verified: ${agent.name}`);
2076
- await this.ensureWalletLinked();
2077
- await this.loadTradingRestrictions();
2078
- console.log(`Initializing LLM: ${this.config.llm.provider}`);
2079
- this.llm = await createLLMAdapter(this.config.llm);
2080
- const llmMeta = this.llm.getMetadata();
2081
- console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
2082
- await this.syncConfigHash();
2083
- this.strategy = await loadStrategy();
2084
- this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
2085
- this.riskManager = new RiskManager(this.config.trading);
2086
- this.marketData = new MarketDataService(this.getRpcUrl());
2087
- await this.initializeVaultManager();
2088
- await this.initializeRelay();
2089
- console.log("Agent initialized successfully");
2385
+ return { signature, nonce: actionNonce };
2090
2386
  }
2091
2387
  /**
2092
- * Initialize the relay client for command center connectivity
2388
+ * Sign a user-level approval action (approve builder fee, approve agent).
2389
+ * These use the same EIP-712 structure but with different action payloads.
2093
2390
  */
2094
- async initializeRelay() {
2095
- const relayConfig = this.config.relay;
2096
- const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
2097
- if (!relayConfig?.enabled || !relayEnabled) {
2098
- console.log("Relay: Disabled");
2099
- return;
2391
+ async signApproval(action, nonce) {
2392
+ return this.signAction(action, nonce);
2393
+ }
2394
+ /**
2395
+ * Get the signer's address
2396
+ */
2397
+ getAddress() {
2398
+ const account = this.walletClient.account;
2399
+ if (!account) throw new Error("Wallet client has no account");
2400
+ return account.address;
2401
+ }
2402
+ };
2403
+ function fillHashToBytes32(fillHash) {
2404
+ if (fillHash.startsWith("0x") && fillHash.length === 66) {
2405
+ return fillHash;
2406
+ }
2407
+ return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["string"], [fillHash]));
2408
+ }
2409
+
2410
+ // src/perp/orders.ts
2411
+ var OrderManager = class {
2412
+ client;
2413
+ signer;
2414
+ config;
2415
+ constructor(client, signer, config) {
2416
+ this.client = client;
2417
+ this.signer = signer;
2418
+ this.config = config;
2419
+ }
2420
+ /**
2421
+ * Place an order on Hyperliquid from a trade signal.
2422
+ * Attaches builder fee for revenue collection.
2423
+ */
2424
+ async placeOrder(signal) {
2425
+ try {
2426
+ const assetIndex = await this.client.getAssetIndex(signal.instrument);
2427
+ const isBuy = signal.action === "open_long" || signal.action === "close_short";
2428
+ const side = isBuy ? "B" : "A";
2429
+ const orderWire = {
2430
+ a: assetIndex,
2431
+ b: isBuy,
2432
+ p: signal.orderType === "market" ? this.getMarketPrice(signal) : signal.price.toString(),
2433
+ s: signal.size.toString(),
2434
+ r: signal.reduceOnly,
2435
+ t: signal.orderType === "market" ? { limit: { tif: "Ioc" } } : { limit: { tif: "Gtc" } }
2436
+ };
2437
+ const action = {
2438
+ type: "order",
2439
+ orders: [orderWire],
2440
+ grouping: "na",
2441
+ builder: {
2442
+ b: this.config.builderAddress,
2443
+ f: this.config.builderFeeTenthsBps
2444
+ }
2445
+ };
2446
+ const nonce = getNextNonce();
2447
+ const { signature } = await this.signer.signAction(action, nonce);
2448
+ const address = this.signer.getAddress();
2449
+ const resp = await this.exchangeRequest({
2450
+ action,
2451
+ nonce: Number(nonce),
2452
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2453
+ vaultAddress: null
2454
+ });
2455
+ return this.parseOrderResponse(resp);
2456
+ } catch (error) {
2457
+ const message = error instanceof Error ? error.message : String(error);
2458
+ console.error(`Order placement failed for ${signal.instrument}:`, message);
2459
+ return {
2460
+ success: false,
2461
+ status: "error",
2462
+ error: message
2463
+ };
2100
2464
  }
2101
- const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
2102
- if (!apiUrl) {
2103
- console.log("Relay: No API URL configured, skipping");
2104
- return;
2465
+ }
2466
+ /**
2467
+ * Cancel an open order by ID.
2468
+ */
2469
+ async cancelOrder(instrument, orderId) {
2470
+ try {
2471
+ const assetIndex = await this.client.getAssetIndex(instrument);
2472
+ const action = {
2473
+ type: "cancel",
2474
+ cancels: [{ a: assetIndex, o: orderId }]
2475
+ };
2476
+ const nonce = getNextNonce();
2477
+ const { signature } = await this.signer.signAction(action, nonce);
2478
+ await this.exchangeRequest({
2479
+ action,
2480
+ nonce: Number(nonce),
2481
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2482
+ vaultAddress: null
2483
+ });
2484
+ console.log(`Cancelled order ${orderId} for ${instrument}`);
2485
+ return true;
2486
+ } catch (error) {
2487
+ const message = error instanceof Error ? error.message : String(error);
2488
+ console.error(`Cancel failed for order ${orderId}:`, message);
2489
+ return false;
2105
2490
  }
2106
- this.relay = new RelayClient({
2107
- agentId: String(this.config.agentId),
2108
- privateKey: this.config.privateKey,
2109
- relay: {
2110
- ...relayConfig,
2111
- apiUrl
2112
- },
2113
- onCommand: (cmd) => this.handleCommand(cmd)
2114
- });
2491
+ }
2492
+ /**
2493
+ * Close an entire position for an instrument.
2494
+ * Uses a market order with reduceOnly flag.
2495
+ */
2496
+ async closePosition(instrument, positionSize) {
2497
+ const isLong = positionSize > 0;
2498
+ const signal = {
2499
+ action: isLong ? "close_long" : "close_short",
2500
+ instrument,
2501
+ size: Math.abs(positionSize),
2502
+ price: 0,
2503
+ leverage: 1,
2504
+ orderType: "market",
2505
+ reduceOnly: true,
2506
+ confidence: 1,
2507
+ reasoning: "Position close"
2508
+ };
2509
+ return this.placeOrder(signal);
2510
+ }
2511
+ /**
2512
+ * Update leverage for an instrument.
2513
+ */
2514
+ async updateLeverage(instrument, leverage, isCross = true) {
2115
2515
  try {
2116
- await this.relay.connect();
2117
- console.log("Relay: Connected to command center");
2118
- this.sendRelayStatus();
2516
+ const assetIndex = await this.client.getAssetIndex(instrument);
2517
+ const action = {
2518
+ type: "updateLeverage",
2519
+ asset: assetIndex,
2520
+ isCross,
2521
+ leverage
2522
+ };
2523
+ const nonce = getNextNonce();
2524
+ const { signature } = await this.signer.signAction(action, nonce);
2525
+ await this.exchangeRequest({
2526
+ action,
2527
+ nonce: Number(nonce),
2528
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2529
+ vaultAddress: null
2530
+ });
2531
+ console.log(`Leverage updated for ${instrument}: ${leverage}x (${isCross ? "cross" : "isolated"})`);
2532
+ return true;
2119
2533
  } catch (error) {
2120
- console.warn(
2121
- "Relay: Failed to connect (agent will work locally):",
2122
- error instanceof Error ? error.message : error
2123
- );
2534
+ const message = error instanceof Error ? error.message : String(error);
2535
+ console.error(`Leverage update failed for ${instrument}:`, message);
2536
+ return false;
2124
2537
  }
2125
2538
  }
2539
+ // ============================================================
2540
+ // PRIVATE HELPERS
2541
+ // ============================================================
2126
2542
  /**
2127
- * Initialize the vault manager based on config
2543
+ * Get a market price string for IOC orders.
2544
+ * Uses a generous slippage buffer to ensure fills.
2128
2545
  */
2129
- async initializeVaultManager() {
2130
- const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
2131
- this.vaultManager = new VaultManager({
2132
- agentId: BigInt(this.config.agentId),
2133
- agentName: this.config.name,
2134
- network: this.config.network,
2135
- walletKey: this.config.privateKey,
2136
- vaultConfig
2137
- });
2138
- console.log(`Vault policy: ${vaultConfig.policy}`);
2139
- const status = await this.vaultManager.getVaultStatus();
2140
- if (status.hasVault) {
2141
- console.log(`Vault exists: ${status.vaultAddress}`);
2142
- console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
2143
- } else {
2144
- console.log("No vault exists for this agent");
2145
- if (vaultConfig.policy === "manual") {
2146
- console.log("Vault creation is manual \u2014 use the command center to create one");
2546
+ getMarketPrice(signal) {
2547
+ const isBuy = signal.action === "open_long" || signal.action === "close_short";
2548
+ if (signal.price > 0) {
2549
+ const slippage = isBuy ? 1.005 : 0.995;
2550
+ return (signal.price * slippage).toString();
2551
+ }
2552
+ return "0";
2553
+ }
2554
+ /**
2555
+ * Parse Hyperliquid exchange response into OrderResult.
2556
+ */
2557
+ parseOrderResponse(resp) {
2558
+ if (resp?.status === "ok" && resp?.response?.type === "order") {
2559
+ const statuses = resp.response.data?.statuses || [];
2560
+ if (statuses.length > 0) {
2561
+ const status = statuses[0];
2562
+ if (status.filled) {
2563
+ return {
2564
+ success: true,
2565
+ orderId: status.filled.oid,
2566
+ status: "filled",
2567
+ avgPrice: status.filled.avgPx,
2568
+ filledSize: status.filled.totalSz
2569
+ };
2570
+ }
2571
+ if (status.resting) {
2572
+ return {
2573
+ success: true,
2574
+ orderId: status.resting.oid,
2575
+ status: "resting"
2576
+ };
2577
+ }
2578
+ if (status.error) {
2579
+ return {
2580
+ success: false,
2581
+ status: "error",
2582
+ error: status.error
2583
+ };
2584
+ }
2147
2585
  }
2148
2586
  }
2587
+ return {
2588
+ success: false,
2589
+ status: "error",
2590
+ error: `Unexpected response: ${JSON.stringify(resp)}`
2591
+ };
2149
2592
  }
2150
2593
  /**
2151
- * Ensure the current wallet is linked to the agent.
2152
- * If the trading wallet differs from the owner, enters a recovery loop
2153
- * that waits for the owner to link it from the website.
2594
+ * Send a signed request to the Hyperliquid Exchange API.
2154
2595
  */
2155
- async ensureWalletLinked() {
2156
- const agentId = BigInt(this.config.agentId);
2157
- const address = this.client.address;
2158
- const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
2159
- if (!isLinked) {
2160
- console.log("Wallet not linked, linking now...");
2161
- const agent = await this.client.registry.getAgent(agentId);
2162
- if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
2163
- const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
2164
- const nonce = await this.client.registry.getNonce(address);
2165
- const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
2166
- address,
2167
- agentId,
2596
+ async exchangeRequest(body) {
2597
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
2598
+ method: "POST",
2599
+ headers: { "Content-Type": "application/json" },
2600
+ body: JSON.stringify(body)
2601
+ });
2602
+ if (!resp.ok) {
2603
+ throw new Error(`Hyperliquid Exchange API error: ${resp.status} ${await resp.text()}`);
2604
+ }
2605
+ return resp.json();
2606
+ }
2607
+ };
2608
+
2609
+ // src/perp/positions.ts
2610
+ var PositionManager = class {
2611
+ client;
2612
+ userAddress;
2613
+ /** Cached positions (updated each cycle) */
2614
+ cachedPositions = [];
2615
+ cachedAccount = null;
2616
+ lastRefreshAt = 0;
2617
+ /** Cache TTL in ms (5 seconds — positions refresh each cycle anyway) */
2618
+ cacheTtlMs = 5e3;
2619
+ constructor(client, userAddress) {
2620
+ this.client = client;
2621
+ this.userAddress = userAddress;
2622
+ }
2623
+ // ============================================================
2624
+ // POSITION QUERIES
2625
+ // ============================================================
2626
+ /**
2627
+ * Get all open positions. Uses cache if fresh.
2628
+ */
2629
+ async getPositions(forceRefresh = false) {
2630
+ if (!forceRefresh && this.isCacheFresh()) {
2631
+ return this.cachedPositions;
2632
+ }
2633
+ await this.refresh();
2634
+ return this.cachedPositions;
2635
+ }
2636
+ /**
2637
+ * Get a specific position by instrument.
2638
+ * Returns null if no position is open.
2639
+ */
2640
+ async getPosition(instrument) {
2641
+ const positions = await this.getPositions();
2642
+ return positions.find((p) => p.instrument === instrument) ?? null;
2643
+ }
2644
+ /**
2645
+ * Get account summary (equity, margin, leverage).
2646
+ */
2647
+ async getAccountSummary(forceRefresh = false) {
2648
+ if (!forceRefresh && this.isCacheFresh() && this.cachedAccount) {
2649
+ return this.cachedAccount;
2650
+ }
2651
+ await this.refresh();
2652
+ return this.cachedAccount;
2653
+ }
2654
+ // ============================================================
2655
+ // LIQUIDATION MONITORING
2656
+ // ============================================================
2657
+ /**
2658
+ * Get liquidation proximity for all positions.
2659
+ * Returns a value between 0.0 (safe) and 1.0 (at liquidation price).
2660
+ * Values above 0.7 should trigger risk warnings.
2661
+ */
2662
+ async getLiquidationProximity() {
2663
+ const positions = await this.getPositions();
2664
+ const proximities = /* @__PURE__ */ new Map();
2665
+ for (const pos of positions) {
2666
+ if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) {
2667
+ proximities.set(pos.instrument, 0);
2668
+ continue;
2669
+ }
2670
+ let proximity;
2671
+ if (pos.size > 0) {
2672
+ if (pos.markPrice <= pos.liquidationPrice) {
2673
+ proximity = 1;
2674
+ } else {
2675
+ const distanceToLiq = pos.markPrice - pos.liquidationPrice;
2676
+ const entryToLiq = pos.entryPrice - pos.liquidationPrice;
2677
+ proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
2678
+ }
2679
+ } else {
2680
+ if (pos.markPrice >= pos.liquidationPrice) {
2681
+ proximity = 1;
2682
+ } else {
2683
+ const distanceToLiq = pos.liquidationPrice - pos.markPrice;
2684
+ const entryToLiq = pos.liquidationPrice - pos.entryPrice;
2685
+ proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
2686
+ }
2687
+ }
2688
+ proximities.set(pos.instrument, Math.max(0, Math.min(1, proximity)));
2689
+ }
2690
+ return proximities;
2691
+ }
2692
+ /**
2693
+ * Check if any position is dangerously close to liquidation.
2694
+ * Returns instruments with proximity > threshold.
2695
+ */
2696
+ async getDangerousPositions(threshold = 0.7) {
2697
+ const positions = await this.getPositions();
2698
+ const proximities = await this.getLiquidationProximity();
2699
+ return positions.filter((p) => {
2700
+ const prox = proximities.get(p.instrument) ?? 0;
2701
+ return prox > threshold;
2702
+ });
2703
+ }
2704
+ // ============================================================
2705
+ // SUMMARY HELPERS
2706
+ // ============================================================
2707
+ /**
2708
+ * Get total unrealized PnL across all positions.
2709
+ */
2710
+ async getTotalUnrealizedPnl() {
2711
+ const positions = await this.getPositions();
2712
+ return positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
2713
+ }
2714
+ /**
2715
+ * Get total notional exposure.
2716
+ */
2717
+ async getTotalNotional() {
2718
+ const positions = await this.getPositions();
2719
+ return positions.reduce((sum, p) => sum + p.notionalUSD, 0);
2720
+ }
2721
+ /**
2722
+ * Get position count.
2723
+ */
2724
+ async getPositionCount() {
2725
+ const positions = await this.getPositions();
2726
+ return positions.length;
2727
+ }
2728
+ // ============================================================
2729
+ // CACHE MANAGEMENT
2730
+ // ============================================================
2731
+ /**
2732
+ * Force refresh positions and account from Hyperliquid.
2733
+ */
2734
+ async refresh() {
2735
+ try {
2736
+ const [positions, account] = await Promise.all([
2737
+ this.client.getPositions(this.userAddress),
2738
+ this.client.getAccountSummary(this.userAddress)
2739
+ ]);
2740
+ this.cachedPositions = positions;
2741
+ this.cachedAccount = account;
2742
+ this.lastRefreshAt = Date.now();
2743
+ } catch (error) {
2744
+ const message = error instanceof Error ? error.message : String(error);
2745
+ console.error("Failed to refresh positions:", message);
2746
+ }
2747
+ }
2748
+ /**
2749
+ * Check if cache is still fresh.
2750
+ */
2751
+ isCacheFresh() {
2752
+ return Date.now() - this.lastRefreshAt < this.cacheTtlMs;
2753
+ }
2754
+ };
2755
+
2756
+ // src/perp/websocket.ts
2757
+ var import_ws2 = __toESM(require("ws"));
2758
+ var HyperliquidWebSocket = class {
2759
+ wsUrl;
2760
+ userAddress;
2761
+ client;
2762
+ ws = null;
2763
+ reconnectAttempts = 0;
2764
+ maxReconnectAttempts = 20;
2765
+ baseReconnectMs = 1e3;
2766
+ maxReconnectMs = 6e4;
2767
+ reconnectTimer = null;
2768
+ pingTimer = null;
2769
+ isConnecting = false;
2770
+ shouldReconnect = true;
2771
+ /** Last processed fill time (ms) — used for REST backfill on reconnect */
2772
+ lastProcessedFillTime = 0;
2773
+ /** Callbacks */
2774
+ onFill = null;
2775
+ onFunding = null;
2776
+ onLiquidation = null;
2777
+ constructor(config, userAddress, client) {
2778
+ this.wsUrl = config.wsUrl;
2779
+ this.userAddress = userAddress;
2780
+ this.client = client;
2781
+ }
2782
+ // ============================================================
2783
+ // CONNECTION
2784
+ // ============================================================
2785
+ /**
2786
+ * Connect to Hyperliquid WebSocket and subscribe to user events.
2787
+ */
2788
+ async connect() {
2789
+ if (this.ws?.readyState === import_ws2.default.OPEN || this.isConnecting) {
2790
+ return;
2791
+ }
2792
+ this.isConnecting = true;
2793
+ this.shouldReconnect = true;
2794
+ return new Promise((resolve, reject) => {
2795
+ try {
2796
+ this.ws = new import_ws2.default(this.wsUrl);
2797
+ this.ws.on("open", () => {
2798
+ this.isConnecting = false;
2799
+ this.reconnectAttempts = 0;
2800
+ console.log("Hyperliquid WebSocket connected");
2801
+ this.subscribe({
2802
+ type: "subscribe",
2803
+ subscription: { type: "userFills", user: this.userAddress }
2804
+ });
2805
+ this.subscribe({
2806
+ type: "subscribe",
2807
+ subscription: { type: "userFundings", user: this.userAddress }
2808
+ });
2809
+ this.startPing();
2810
+ this.backfillMissedFills().catch((err) => {
2811
+ console.warn("Fill backfill failed:", err instanceof Error ? err.message : err);
2812
+ });
2813
+ resolve();
2814
+ });
2815
+ this.ws.on("message", (data) => {
2816
+ this.handleMessage(data);
2817
+ });
2818
+ this.ws.on("close", (code, reason) => {
2819
+ this.isConnecting = false;
2820
+ console.log(`Hyperliquid WebSocket closed: ${code} ${reason.toString()}`);
2821
+ this.stopPing();
2822
+ this.scheduleReconnect();
2823
+ });
2824
+ this.ws.on("error", (error) => {
2825
+ this.isConnecting = false;
2826
+ console.error("Hyperliquid WebSocket error:", error.message);
2827
+ if (this.reconnectAttempts === 0) {
2828
+ reject(error);
2829
+ }
2830
+ });
2831
+ } catch (error) {
2832
+ this.isConnecting = false;
2833
+ reject(error);
2834
+ }
2835
+ });
2836
+ }
2837
+ /**
2838
+ * Disconnect and stop reconnecting.
2839
+ */
2840
+ disconnect() {
2841
+ this.shouldReconnect = false;
2842
+ if (this.reconnectTimer) {
2843
+ clearTimeout(this.reconnectTimer);
2844
+ this.reconnectTimer = null;
2845
+ }
2846
+ this.stopPing();
2847
+ if (this.ws) {
2848
+ this.ws.removeAllListeners();
2849
+ if (this.ws.readyState === import_ws2.default.OPEN) {
2850
+ this.ws.close(1e3, "Client disconnect");
2851
+ }
2852
+ this.ws = null;
2853
+ }
2854
+ console.log("Hyperliquid WebSocket disconnected");
2855
+ }
2856
+ /**
2857
+ * Check if WebSocket is connected.
2858
+ */
2859
+ get isConnected() {
2860
+ return this.ws?.readyState === import_ws2.default.OPEN;
2861
+ }
2862
+ // ============================================================
2863
+ // EVENT HANDLERS
2864
+ // ============================================================
2865
+ /**
2866
+ * Register callback for fill events.
2867
+ */
2868
+ onFillReceived(callback) {
2869
+ this.onFill = callback;
2870
+ }
2871
+ /**
2872
+ * Register callback for funding payment events.
2873
+ */
2874
+ onFundingReceived(callback) {
2875
+ this.onFunding = callback;
2876
+ }
2877
+ /**
2878
+ * Register callback for liquidation events.
2879
+ */
2880
+ onLiquidationDetected(callback) {
2881
+ this.onLiquidation = callback;
2882
+ }
2883
+ /**
2884
+ * Get the last processed fill time (for external checkpoint management).
2885
+ */
2886
+ getLastProcessedFillTime() {
2887
+ return this.lastProcessedFillTime;
2888
+ }
2889
+ // ============================================================
2890
+ // MESSAGE HANDLING
2891
+ // ============================================================
2892
+ handleMessage(data) {
2893
+ try {
2894
+ const msg = JSON.parse(data.toString());
2895
+ if (msg.channel === "userFills") {
2896
+ this.handleFillMessage(msg.data);
2897
+ } else if (msg.channel === "userFundings") {
2898
+ this.handleFundingMessage(msg.data);
2899
+ }
2900
+ } catch (error) {
2901
+ }
2902
+ }
2903
+ handleFillMessage(fills) {
2904
+ if (!Array.isArray(fills) || !this.onFill) return;
2905
+ for (const rawFill of fills) {
2906
+ const fill = this.client.parseFill(rawFill);
2907
+ if (fill.time > this.lastProcessedFillTime) {
2908
+ this.lastProcessedFillTime = fill.time;
2909
+ }
2910
+ if (fill.liquidation && this.onLiquidation) {
2911
+ this.onLiquidation(fill.coin, parseFloat(fill.sz));
2912
+ }
2913
+ this.onFill(fill);
2914
+ }
2915
+ }
2916
+ handleFundingMessage(fundings) {
2917
+ if (!Array.isArray(fundings) || !this.onFunding) return;
2918
+ for (const funding of fundings) {
2919
+ this.onFunding({
2920
+ time: funding.time,
2921
+ coin: funding.coin,
2922
+ usdc: funding.usdc,
2923
+ szi: funding.szi,
2924
+ fundingRate: funding.fundingRate
2925
+ });
2926
+ }
2927
+ }
2928
+ // ============================================================
2929
+ // BACKFILL
2930
+ // ============================================================
2931
+ /**
2932
+ * Backfill fills that may have been missed during WebSocket downtime.
2933
+ * Uses the last processed fill time as the starting point.
2934
+ */
2935
+ async backfillMissedFills() {
2936
+ if (this.lastProcessedFillTime === 0 || !this.onFill) {
2937
+ return;
2938
+ }
2939
+ console.log(`Backfilling fills since ${new Date(this.lastProcessedFillTime).toISOString()}`);
2940
+ const fills = await this.client.getUserFillsByTime(
2941
+ this.userAddress,
2942
+ this.lastProcessedFillTime + 1
2943
+ // +1 to avoid duplicate
2944
+ );
2945
+ if (fills.length > 0) {
2946
+ console.log(`Backfilled ${fills.length} missed fills`);
2947
+ for (const rawFill of fills) {
2948
+ const fill = this.client.parseFill(rawFill);
2949
+ if (fill.time > this.lastProcessedFillTime) {
2950
+ this.lastProcessedFillTime = fill.time;
2951
+ }
2952
+ if (fill.liquidation && this.onLiquidation) {
2953
+ this.onLiquidation(fill.coin, parseFloat(fill.sz));
2954
+ }
2955
+ this.onFill(fill);
2956
+ }
2957
+ }
2958
+ }
2959
+ // ============================================================
2960
+ // RECONNECTION
2961
+ // ============================================================
2962
+ scheduleReconnect() {
2963
+ if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
2964
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
2965
+ console.error(`Hyperliquid WebSocket: max reconnect attempts (${this.maxReconnectAttempts}) reached`);
2966
+ }
2967
+ return;
2968
+ }
2969
+ const delay = Math.min(
2970
+ this.baseReconnectMs * Math.pow(2, this.reconnectAttempts),
2971
+ this.maxReconnectMs
2972
+ );
2973
+ this.reconnectAttempts++;
2974
+ console.log(`Hyperliquid WebSocket: reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
2975
+ this.reconnectTimer = setTimeout(() => {
2976
+ this.connect().catch((err) => {
2977
+ console.error("Reconnect failed:", err instanceof Error ? err.message : err);
2978
+ });
2979
+ }, delay);
2980
+ }
2981
+ // ============================================================
2982
+ // KEEPALIVE
2983
+ // ============================================================
2984
+ startPing() {
2985
+ this.stopPing();
2986
+ this.pingTimer = setInterval(() => {
2987
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
2988
+ this.ws.send(JSON.stringify({ method: "ping" }));
2989
+ }
2990
+ }, 25e3);
2991
+ }
2992
+ stopPing() {
2993
+ if (this.pingTimer) {
2994
+ clearInterval(this.pingTimer);
2995
+ this.pingTimer = null;
2996
+ }
2997
+ }
2998
+ // ============================================================
2999
+ // HELPERS
3000
+ // ============================================================
3001
+ subscribe(msg) {
3002
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
3003
+ this.ws.send(JSON.stringify(msg));
3004
+ }
3005
+ }
3006
+ };
3007
+
3008
+ // src/perp/recorder.ts
3009
+ var import_viem4 = require("viem");
3010
+ var import_chains2 = require("viem/chains");
3011
+ var import_accounts3 = require("viem/accounts");
3012
+ var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
3013
+ var ROUTER_ABI = [
3014
+ {
3015
+ type: "function",
3016
+ name: "recordPerpTrade",
3017
+ stateMutability: "nonpayable",
3018
+ inputs: [
3019
+ { name: "agentId", type: "uint256" },
3020
+ { name: "configHash", type: "bytes32" },
3021
+ { name: "instrument", type: "string" },
3022
+ { name: "isLong", type: "bool" },
3023
+ { name: "notionalUSD", type: "uint256" },
3024
+ { name: "feeUSD", type: "uint256" },
3025
+ { name: "fillId", type: "bytes32" }
3026
+ ],
3027
+ outputs: [{ name: "", type: "bool" }]
3028
+ }
3029
+ ];
3030
+ var MAX_RETRIES = 3;
3031
+ var RETRY_DELAY_MS = 5e3;
3032
+ var PerpTradeRecorder = class {
3033
+ // Use `any` for viem client types to avoid L2 chain type conflicts (Base has "deposit" tx type)
3034
+ publicClient;
3035
+ // eslint-disable-line @typescript-eslint/no-explicit-any
3036
+ walletClient;
3037
+ // eslint-disable-line @typescript-eslint/no-explicit-any
3038
+ account;
3039
+ agentId;
3040
+ configHash;
3041
+ /** Retry queue for failed recordings */
3042
+ retryQueue = [];
3043
+ /** Set of fill IDs already recorded (or in-progress) to prevent local dups */
3044
+ recordedFills = /* @__PURE__ */ new Set();
3045
+ /** Timer for processing retry queue */
3046
+ retryTimer = null;
3047
+ constructor(opts) {
3048
+ this.agentId = opts.agentId;
3049
+ this.configHash = opts.configHash;
3050
+ this.account = (0, import_accounts3.privateKeyToAccount)(opts.privateKey);
3051
+ const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3052
+ const transport = (0, import_viem4.http)(rpcUrl);
3053
+ this.publicClient = (0, import_viem4.createPublicClient)({
3054
+ chain: import_chains2.base,
3055
+ transport
3056
+ });
3057
+ this.walletClient = (0, import_viem4.createWalletClient)({
3058
+ chain: import_chains2.base,
3059
+ transport,
3060
+ account: this.account
3061
+ });
3062
+ this.retryTimer = setInterval(() => this.processRetryQueue(), RETRY_DELAY_MS);
3063
+ }
3064
+ // ============================================================
3065
+ // PUBLIC API
3066
+ // ============================================================
3067
+ /**
3068
+ * Record a fill on-chain.
3069
+ * Converts the Hyperliquid fill into recordPerpTrade params and submits.
3070
+ */
3071
+ async recordFill(fill) {
3072
+ const fillId = fillHashToBytes32(fill.hash);
3073
+ const fillIdStr = fillId.toLowerCase();
3074
+ if (this.recordedFills.has(fillIdStr)) {
3075
+ return { success: true };
3076
+ }
3077
+ this.recordedFills.add(fillIdStr);
3078
+ const params = {
3079
+ agentId: this.agentId,
3080
+ configHash: this.configHash,
3081
+ instrument: fill.coin,
3082
+ isLong: fill.side === "B",
3083
+ notionalUSD: this.calculateNotionalUSD(fill),
3084
+ feeUSD: this.calculateFeeUSD(fill),
3085
+ fillId
3086
+ };
3087
+ return this.submitRecord(params);
3088
+ }
3089
+ /**
3090
+ * Update the config hash (when epoch changes).
3091
+ */
3092
+ updateConfigHash(configHash) {
3093
+ this.configHash = configHash;
3094
+ }
3095
+ /**
3096
+ * Get the number of fills pending retry.
3097
+ */
3098
+ get pendingRetries() {
3099
+ return this.retryQueue.length;
3100
+ }
3101
+ /**
3102
+ * Get the number of fills recorded (local dedup set size).
3103
+ */
3104
+ get recordedCount() {
3105
+ return this.recordedFills.size;
3106
+ }
3107
+ /**
3108
+ * Stop the recorder (clear retry timer).
3109
+ */
3110
+ stop() {
3111
+ if (this.retryTimer) {
3112
+ clearInterval(this.retryTimer);
3113
+ this.retryTimer = null;
3114
+ }
3115
+ }
3116
+ // ============================================================
3117
+ // PRIVATE
3118
+ // ============================================================
3119
+ /**
3120
+ * Submit a recordPerpTrade transaction on Base.
3121
+ */
3122
+ async submitRecord(params) {
3123
+ try {
3124
+ const { request } = await this.publicClient.simulateContract({
3125
+ address: ROUTER_ADDRESS,
3126
+ abi: ROUTER_ABI,
3127
+ functionName: "recordPerpTrade",
3128
+ args: [
3129
+ params.agentId,
3130
+ params.configHash,
3131
+ params.instrument,
3132
+ params.isLong,
3133
+ params.notionalUSD,
3134
+ params.feeUSD,
3135
+ params.fillId
3136
+ ],
3137
+ account: this.account
3138
+ });
3139
+ const txHash = await this.walletClient.writeContract(request);
3140
+ console.log(`Perp trade recorded: ${params.instrument} ${params.isLong ? "LONG" : "SHORT"} $${Number(params.notionalUSD) / 1e6} \u2014 tx: ${txHash}`);
3141
+ return { success: true, txHash };
3142
+ } catch (error) {
3143
+ const message = error instanceof Error ? error.message : String(error);
3144
+ if (message.includes("DuplicateFill") || message.includes("already recorded")) {
3145
+ console.log(`Fill already recorded on-chain: ${params.fillId}`);
3146
+ return { success: true };
3147
+ }
3148
+ console.error(`Failed to record perp trade: ${message}`);
3149
+ this.retryQueue.push({
3150
+ params,
3151
+ retries: 0,
3152
+ lastAttempt: Date.now()
3153
+ });
3154
+ return { success: false, error: message };
3155
+ }
3156
+ }
3157
+ /**
3158
+ * Process the retry queue — attempt to re-submit failed recordings.
3159
+ */
3160
+ async processRetryQueue() {
3161
+ if (this.retryQueue.length === 0) return;
3162
+ const now = Date.now();
3163
+ const toRetry = this.retryQueue.filter(
3164
+ (item) => now - item.lastAttempt >= RETRY_DELAY_MS
3165
+ );
3166
+ for (const item of toRetry) {
3167
+ item.retries++;
3168
+ item.lastAttempt = now;
3169
+ if (item.retries > MAX_RETRIES) {
3170
+ console.error(
3171
+ `Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
3172
+ );
3173
+ const idx = this.retryQueue.indexOf(item);
3174
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3175
+ continue;
3176
+ }
3177
+ console.log(
3178
+ `Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
3179
+ );
3180
+ const result = await this.submitRecord(item.params);
3181
+ if (result.success) {
3182
+ const idx = this.retryQueue.indexOf(item);
3183
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3184
+ }
3185
+ }
3186
+ }
3187
+ // ============================================================
3188
+ // CONVERSION HELPERS
3189
+ // ============================================================
3190
+ /**
3191
+ * Calculate notional USD from a fill (6-decimal).
3192
+ * notionalUSD = px * sz * 1e6
3193
+ */
3194
+ calculateNotionalUSD(fill) {
3195
+ const px = parseFloat(fill.px);
3196
+ const sz = parseFloat(fill.sz);
3197
+ return BigInt(Math.round(px * sz * 1e6));
3198
+ }
3199
+ /**
3200
+ * Calculate fee USD from a fill (6-decimal).
3201
+ * feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
3202
+ */
3203
+ calculateFeeUSD(fill) {
3204
+ const fee = parseFloat(fill.fee);
3205
+ const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
3206
+ return BigInt(Math.round((fee + builderFee) * 1e6));
3207
+ }
3208
+ };
3209
+
3210
+ // src/perp/onboarding.ts
3211
+ var PerpOnboarding = class {
3212
+ client;
3213
+ signer;
3214
+ config;
3215
+ constructor(client, signer, config) {
3216
+ this.client = client;
3217
+ this.signer = signer;
3218
+ this.config = config;
3219
+ }
3220
+ // ============================================================
3221
+ // BUILDER FEE
3222
+ // ============================================================
3223
+ /**
3224
+ * Check if the user has approved the builder fee.
3225
+ * Builder fee must be approved before orders can include builder fees.
3226
+ */
3227
+ async isBuilderFeeApproved() {
3228
+ try {
3229
+ const maxFee = await this.client.getMaxBuilderFee(
3230
+ this.signer.getAddress(),
3231
+ this.config.builderAddress
3232
+ );
3233
+ return maxFee >= this.config.builderFeeTenthsBps;
3234
+ } catch {
3235
+ return false;
3236
+ }
3237
+ }
3238
+ /**
3239
+ * Approve the builder fee on Hyperliquid.
3240
+ * This is a one-time approval per builder address.
3241
+ */
3242
+ async approveBuilderFee() {
3243
+ try {
3244
+ const action = {
3245
+ type: "approveBuilderFee",
3246
+ hyperliquidChain: "Mainnet",
3247
+ maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
3248
+ builder: this.config.builderAddress,
3249
+ nonce: Number(getNextNonce())
3250
+ };
3251
+ const { signature } = await this.signer.signApproval(action);
3252
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
3253
+ method: "POST",
3254
+ headers: { "Content-Type": "application/json" },
3255
+ body: JSON.stringify({
3256
+ action,
3257
+ signature: {
3258
+ r: signature.slice(0, 66),
3259
+ s: `0x${signature.slice(66, 130)}`,
3260
+ v: parseInt(signature.slice(130, 132), 16)
3261
+ },
3262
+ nonce: action.nonce,
3263
+ vaultAddress: null
3264
+ })
3265
+ });
3266
+ if (!resp.ok) {
3267
+ const text = await resp.text();
3268
+ console.error(`Builder fee approval failed: ${resp.status} ${text}`);
3269
+ return false;
3270
+ }
3271
+ console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
3272
+ return true;
3273
+ } catch (error) {
3274
+ const message = error instanceof Error ? error.message : String(error);
3275
+ console.error(`Builder fee approval failed: ${message}`);
3276
+ return false;
3277
+ }
3278
+ }
3279
+ // ============================================================
3280
+ // BALANCE & REQUIREMENTS
3281
+ // ============================================================
3282
+ /**
3283
+ * Check if the user has sufficient USDC balance on Hyperliquid.
3284
+ * Returns the account equity in USD.
3285
+ */
3286
+ async checkBalance() {
3287
+ try {
3288
+ const account = await this.client.getAccountSummary(this.signer.getAddress());
3289
+ return {
3290
+ hasBalance: account.totalEquity > 0,
3291
+ equity: account.totalEquity
3292
+ };
3293
+ } catch {
3294
+ return { hasBalance: false, equity: 0 };
3295
+ }
3296
+ }
3297
+ /**
3298
+ * Verify that the agent's risk universe allows perp trading.
3299
+ * Perps require risk universe >= 2 (Derivatives or higher).
3300
+ */
3301
+ verifyRiskUniverse(riskUniverse) {
3302
+ if (riskUniverse >= 2) {
3303
+ return {
3304
+ allowed: true,
3305
+ message: `Risk universe ${riskUniverse} allows perp trading`
3306
+ };
3307
+ }
3308
+ return {
3309
+ allowed: false,
3310
+ message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
3311
+ };
3312
+ }
3313
+ // ============================================================
3314
+ // FULL ONBOARDING CHECK
3315
+ // ============================================================
3316
+ /**
3317
+ * Run all onboarding checks and return status.
3318
+ * Does NOT auto-approve — caller must explicitly approve after review.
3319
+ */
3320
+ async checkOnboardingStatus(riskUniverse) {
3321
+ const riskCheck = this.verifyRiskUniverse(riskUniverse);
3322
+ const balanceCheck = await this.checkBalance();
3323
+ const builderFeeApproved = await this.isBuilderFeeApproved();
3324
+ const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
3325
+ return {
3326
+ ready,
3327
+ riskUniverseOk: riskCheck.allowed,
3328
+ riskUniverseMessage: riskCheck.message,
3329
+ hasBalance: balanceCheck.hasBalance,
3330
+ equity: balanceCheck.equity,
3331
+ builderFeeApproved,
3332
+ builderAddress: this.config.builderAddress,
3333
+ builderFeeBps: this.config.builderFeeTenthsBps / 10
3334
+ };
3335
+ }
3336
+ /**
3337
+ * Run full onboarding: check status and auto-approve builder fee if needed.
3338
+ * Returns the final status after all actions.
3339
+ */
3340
+ async onboard(riskUniverse) {
3341
+ let status = await this.checkOnboardingStatus(riskUniverse);
3342
+ if (!status.riskUniverseOk) {
3343
+ console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
3344
+ return status;
3345
+ }
3346
+ if (!status.hasBalance) {
3347
+ console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
3348
+ return status;
3349
+ }
3350
+ if (!status.builderFeeApproved) {
3351
+ console.log("Approving builder fee...");
3352
+ const approved = await this.approveBuilderFee();
3353
+ if (approved) {
3354
+ status = { ...status, builderFeeApproved: true, ready: true };
3355
+ }
3356
+ }
3357
+ if (status.ready) {
3358
+ console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
3359
+ }
3360
+ return status;
3361
+ }
3362
+ };
3363
+
3364
+ // src/perp/funding.ts
3365
+ var import_viem5 = require("viem");
3366
+ var import_chains3 = require("viem/chains");
3367
+ var import_accounts4 = require("viem/accounts");
3368
+ var ERC20_ABI = (0, import_viem5.parseAbi)([
3369
+ "function approve(address spender, uint256 amount) external returns (bool)",
3370
+ "function balanceOf(address account) external view returns (uint256)",
3371
+ "function allowance(address owner, address spender) external view returns (uint256)"
3372
+ ]);
3373
+ var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
3374
+ "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3375
+ "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3376
+ ]);
3377
+ var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
3378
+ "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3379
+ "event MessageSent(bytes message)"
3380
+ ]);
3381
+ var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
3382
+ "function deposit(uint256 amount, uint32 destinationDex) external"
3383
+ ]);
3384
+
3385
+ // src/runtime.ts
3386
+ var FUNDS_LOW_THRESHOLD = 5e-3;
3387
+ var FUNDS_CRITICAL_THRESHOLD = 1e-3;
3388
+ var AgentRuntime = class {
3389
+ config;
3390
+ client;
3391
+ llm;
3392
+ strategy;
3393
+ executor;
3394
+ riskManager;
3395
+ marketData;
3396
+ vaultManager;
3397
+ relay = null;
3398
+ isRunning = false;
3399
+ mode = "idle";
3400
+ configHash;
3401
+ cycleCount = 0;
3402
+ lastCycleAt = 0;
3403
+ lastPortfolioValue = 0;
3404
+ lastEthBalance = "0";
3405
+ processAlive = true;
3406
+ riskUniverse = 0;
3407
+ allowedTokens = /* @__PURE__ */ new Set();
3408
+ // Perp trading components (null if perp not enabled)
3409
+ perpClient = null;
3410
+ perpSigner = null;
3411
+ perpOrders = null;
3412
+ perpPositions = null;
3413
+ perpWebSocket = null;
3414
+ perpRecorder = null;
3415
+ perpOnboarding = null;
3416
+ perpStrategy = null;
3417
+ // Two-layer perp control:
3418
+ // perpConnected = Hyperliquid infrastructure is initialized (WS, signer, recorder ready)
3419
+ // perpTradingActive = Dedicated perp trading cycle is mandated to run
3420
+ // When perpConnected && !perpTradingActive: agent's strategy can optionally return perp signals
3421
+ // When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
3422
+ perpConnected = false;
3423
+ perpTradingActive = false;
3424
+ constructor(config) {
3425
+ this.config = config;
3426
+ }
3427
+ /**
3428
+ * Initialize the agent runtime
3429
+ */
3430
+ async initialize() {
3431
+ console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3432
+ this.client = new import_sdk2.ExagentClient({
3433
+ privateKey: this.config.privateKey,
3434
+ network: this.config.network
3435
+ });
3436
+ console.log(`Wallet: ${this.client.address}`);
3437
+ const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
3438
+ if (!agent) {
3439
+ throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
3440
+ }
3441
+ console.log(`Agent verified: ${agent.name}`);
3442
+ await this.ensureWalletLinked();
3443
+ await this.loadTradingRestrictions();
3444
+ console.log(`Initializing LLM: ${this.config.llm.provider}`);
3445
+ this.llm = await createLLMAdapter(this.config.llm);
3446
+ const llmMeta = this.llm.getMetadata();
3447
+ console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3448
+ await this.syncConfigHash();
3449
+ this.strategy = await loadStrategy();
3450
+ this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3451
+ this.riskManager = new RiskManager(this.config.trading);
3452
+ this.marketData = new MarketDataService(this.getRpcUrl());
3453
+ await this.initializeVaultManager();
3454
+ await this.initializePerp();
3455
+ await this.initializeRelay();
3456
+ console.log("Agent initialized successfully");
3457
+ }
3458
+ /**
3459
+ * Initialize the relay client for command center connectivity
3460
+ */
3461
+ async initializeRelay() {
3462
+ const relayConfig = this.config.relay;
3463
+ const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
3464
+ if (!relayConfig?.enabled || !relayEnabled) {
3465
+ console.log("Relay: Disabled");
3466
+ return;
3467
+ }
3468
+ const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
3469
+ if (!apiUrl) {
3470
+ console.log("Relay: No API URL configured, skipping");
3471
+ return;
3472
+ }
3473
+ this.relay = new RelayClient({
3474
+ agentId: String(this.config.agentId),
3475
+ privateKey: this.config.privateKey,
3476
+ relay: {
3477
+ ...relayConfig,
3478
+ apiUrl
3479
+ },
3480
+ onCommand: (cmd) => this.handleCommand(cmd)
3481
+ });
3482
+ try {
3483
+ await this.relay.connect();
3484
+ console.log("Relay: Connected to command center");
3485
+ this.sendRelayStatus();
3486
+ } catch (error) {
3487
+ console.warn(
3488
+ "Relay: Failed to connect (agent will work locally):",
3489
+ error instanceof Error ? error.message : error
3490
+ );
3491
+ }
3492
+ }
3493
+ /**
3494
+ * Initialize the vault manager based on config
3495
+ */
3496
+ async initializeVaultManager() {
3497
+ const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
3498
+ this.vaultManager = new VaultManager({
3499
+ agentId: BigInt(this.config.agentId),
3500
+ agentName: this.config.name,
3501
+ network: this.config.network,
3502
+ walletKey: this.config.privateKey,
3503
+ vaultConfig
3504
+ });
3505
+ console.log(`Vault policy: ${vaultConfig.policy}`);
3506
+ const status = await this.vaultManager.getVaultStatus();
3507
+ if (status.hasVault) {
3508
+ console.log(`Vault exists: ${status.vaultAddress}`);
3509
+ console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
3510
+ } else {
3511
+ console.log("No vault exists for this agent");
3512
+ if (vaultConfig.policy === "manual") {
3513
+ console.log("Vault creation is manual \u2014 use the command center to create one");
3514
+ }
3515
+ }
3516
+ }
3517
+ /**
3518
+ * Initialize Hyperliquid perp trading components.
3519
+ * Only initializes if perp is enabled in config AND risk universe >= 2.
3520
+ */
3521
+ async initializePerp() {
3522
+ const perpConfig = this.config.perp;
3523
+ if (!perpConfig?.enabled) {
3524
+ console.log("Perp trading: Disabled");
3525
+ return;
3526
+ }
3527
+ if (this.riskUniverse < 2) {
3528
+ console.warn(`Perp trading: Blocked by risk universe ${this.riskUniverse} (need >= 2)`);
3529
+ return;
3530
+ }
3531
+ try {
3532
+ const config = {
3533
+ enabled: true,
3534
+ apiUrl: perpConfig.apiUrl || "https://api.hyperliquid.xyz",
3535
+ wsUrl: perpConfig.wsUrl || "wss://api.hyperliquid.xyz/ws",
3536
+ builderAddress: perpConfig.builderAddress,
3537
+ builderFeeTenthsBps: perpConfig.builderFeeTenthsBps ?? 100,
3538
+ maxLeverage: perpConfig.maxLeverage ?? 10,
3539
+ maxNotionalUSD: perpConfig.maxNotionalUSD ?? 5e4,
3540
+ allowedInstruments: perpConfig.allowedInstruments
3541
+ };
3542
+ this.perpClient = new HyperliquidClient(config);
3543
+ const perpKey = perpConfig.perpRelayerKey || this.config.privateKey;
3544
+ const account = (0, import_accounts5.privateKeyToAccount)(perpKey);
3545
+ const walletClient = (0, import_viem6.createWalletClient)({
3546
+ chain: { id: 42161, name: "Arbitrum", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } } },
3547
+ transport: (0, import_viem6.http)("https://arb1.arbitrum.io/rpc"),
3548
+ account
3549
+ });
3550
+ this.perpSigner = new HyperliquidSigner(walletClient);
3551
+ this.perpOrders = new OrderManager(this.perpClient, this.perpSigner, config);
3552
+ this.perpPositions = new PositionManager(this.perpClient, this.perpSigner.getAddress());
3553
+ this.perpOnboarding = new PerpOnboarding(this.perpClient, this.perpSigner, config);
3554
+ const recorderKey = perpConfig.perpRelayerKey || this.config.privateKey;
3555
+ this.perpRecorder = new PerpTradeRecorder({
3556
+ privateKey: recorderKey,
3557
+ rpcUrl: this.getRpcUrl(),
3558
+ agentId: BigInt(this.config.agentId),
3559
+ configHash: this.configHash
3560
+ });
3561
+ this.perpWebSocket = new HyperliquidWebSocket(config, this.perpSigner.getAddress(), this.perpClient);
3562
+ this.perpWebSocket.onFillReceived(async (fill) => {
3563
+ console.log(`Perp fill: ${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz}@${fill.px}`);
3564
+ const result = await this.perpRecorder.recordFill(fill);
3565
+ if (result.success) {
3566
+ this.relay?.sendMessage(
3567
+ "perp_fill",
3568
+ "success",
3569
+ "Perp Fill",
3570
+ `${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz} @ $${fill.px}`,
3571
+ { instrument: fill.coin, side: fill.side, size: fill.sz, price: fill.px, txHash: result.txHash }
3572
+ );
3573
+ }
3574
+ });
3575
+ this.perpWebSocket.onLiquidationDetected((instrument, size) => {
3576
+ console.error(`LIQUIDATION: ${instrument} position (${size}) was liquidated`);
3577
+ this.relay?.sendMessage(
3578
+ "perp_liquidation_warning",
3579
+ "error",
3580
+ "Position Liquidated",
3581
+ `${instrument} position of ${Math.abs(size)} was liquidated.`,
3582
+ { instrument, size }
3583
+ );
3584
+ });
3585
+ this.perpWebSocket.onFundingReceived((funding) => {
3586
+ const amount = parseFloat(funding.usdc);
3587
+ if (Math.abs(amount) > 0.01) {
3588
+ this.relay?.sendMessage(
3589
+ "perp_funding",
3590
+ "info",
3591
+ "Funding Payment",
3592
+ `${funding.coin}: ${amount > 0 ? "+" : ""}$${amount.toFixed(4)}`,
3593
+ { instrument: funding.coin, amount: funding.usdc, rate: funding.fundingRate }
3594
+ );
3595
+ }
3596
+ });
3597
+ const onboardingStatus = await this.perpOnboarding.onboard(this.riskUniverse);
3598
+ if (!onboardingStatus.ready) {
3599
+ console.warn(`Perp onboarding incomplete \u2014 trading will be limited`);
3600
+ if (!onboardingStatus.hasBalance) {
3601
+ console.warn(" No USDC balance on Hyperliquid \u2014 deposit required");
3602
+ }
3603
+ if (!onboardingStatus.builderFeeApproved) {
3604
+ console.warn(" Builder fee not approved \u2014 orders may fail");
3605
+ }
3606
+ }
3607
+ try {
3608
+ await this.perpWebSocket.connect();
3609
+ console.log("Perp WebSocket: Connected");
3610
+ } catch (error) {
3611
+ console.warn("Perp WebSocket: Failed to connect (will retry):", error instanceof Error ? error.message : error);
3612
+ }
3613
+ this.perpConnected = true;
3614
+ console.log(`Hyperliquid: Connected (${config.allowedInstruments?.join(", ") || "all instruments"})`);
3615
+ console.log(` Builder: ${config.builderAddress} (${config.builderFeeTenthsBps / 10} bps)`);
3616
+ console.log(` Max leverage: ${config.maxLeverage}x, Max notional: $${config.maxNotionalUSD.toLocaleString()}`);
3617
+ } catch (error) {
3618
+ const message = error instanceof Error ? error.message : String(error);
3619
+ console.error(`Perp initialization failed: ${message}`);
3620
+ console.warn("Perp trading will be disabled for this session");
3621
+ }
3622
+ }
3623
+ /**
3624
+ * Ensure the current wallet is linked to the agent.
3625
+ * If the trading wallet differs from the owner, enters a recovery loop
3626
+ * that waits for the owner to link it from the website.
3627
+ */
3628
+ async ensureWalletLinked() {
3629
+ const agentId = BigInt(this.config.agentId);
3630
+ const address = this.client.address;
3631
+ const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
3632
+ if (!isLinked) {
3633
+ console.log("Wallet not linked, linking now...");
3634
+ const agent = await this.client.registry.getAgent(agentId);
3635
+ if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3636
+ const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3637
+ const nonce = await this.client.registry.getNonce(address);
3638
+ const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
3639
+ address,
3640
+ agentId,
2168
3641
  nonce
2169
3642
  );
2170
3643
  const linkSignature = await this.client.signMessage({ raw: linkMessage });
@@ -2266,10 +3739,10 @@ var AgentRuntime = class {
2266
3739
  const message = error instanceof Error ? error.message : String(error);
2267
3740
  if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
2268
3741
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
2269
- const chain = import_chains2.base;
2270
- const publicClientInstance = (0, import_viem3.createPublicClient)({
3742
+ const chain = import_chains4.base;
3743
+ const publicClientInstance = (0, import_viem6.createPublicClient)({
2271
3744
  chain,
2272
- transport: (0, import_viem3.http)(this.getRpcUrl())
3745
+ transport: (0, import_viem6.http)(this.getRpcUrl())
2273
3746
  });
2274
3747
  console.log("");
2275
3748
  console.log("=== ETH NEEDED FOR GAS ===");
@@ -2480,6 +3953,131 @@ var AgentRuntime = class {
2480
3953
  }
2481
3954
  break;
2482
3955
  }
3956
+ case "enable_hyperliquid":
3957
+ if (this.perpConnected) {
3958
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already connected");
3959
+ break;
3960
+ }
3961
+ if (!this.config.perp?.enabled) {
3962
+ this.relay?.sendCommandResult(cmd.id, false, "Perp trading not configured in agent config");
3963
+ break;
3964
+ }
3965
+ if (this.riskUniverse < 2) {
3966
+ this.relay?.sendCommandResult(cmd.id, false, `Risk universe ${this.riskUniverse} too low (need >= 2)`);
3967
+ break;
3968
+ }
3969
+ try {
3970
+ await this.initializePerp();
3971
+ if (this.perpConnected) {
3972
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid connected");
3973
+ this.relay?.sendMessage(
3974
+ "system",
3975
+ "success",
3976
+ "Hyperliquid Enabled",
3977
+ 'Hyperliquid infrastructure connected. Agent can now include perp signals in its strategy. Use "Start Perp Trading" to mandate a dedicated perp cycle.'
3978
+ );
3979
+ } else {
3980
+ this.relay?.sendCommandResult(cmd.id, false, "Failed to connect to Hyperliquid");
3981
+ }
3982
+ } catch (error) {
3983
+ const msg = error instanceof Error ? error.message : String(error);
3984
+ this.relay?.sendCommandResult(cmd.id, false, `Hyperliquid init failed: ${msg}`);
3985
+ }
3986
+ this.sendRelayStatus();
3987
+ break;
3988
+ case "disable_hyperliquid":
3989
+ if (!this.perpConnected) {
3990
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already disconnected");
3991
+ break;
3992
+ }
3993
+ this.perpTradingActive = false;
3994
+ if (this.perpWebSocket) {
3995
+ this.perpWebSocket.disconnect();
3996
+ }
3997
+ if (this.perpRecorder) {
3998
+ this.perpRecorder.stop();
3999
+ }
4000
+ this.perpConnected = false;
4001
+ console.log("Hyperliquid disabled via command center");
4002
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid disconnected");
4003
+ this.relay?.sendMessage(
4004
+ "system",
4005
+ "info",
4006
+ "Hyperliquid Disabled",
4007
+ "Hyperliquid infrastructure disconnected. Agent will trade spot only."
4008
+ );
4009
+ this.sendRelayStatus();
4010
+ break;
4011
+ case "start_perp_trading":
4012
+ if (!this.perpConnected) {
4013
+ this.relay?.sendCommandResult(cmd.id, false, "Hyperliquid not connected. Enable Hyperliquid first.");
4014
+ break;
4015
+ }
4016
+ if (this.perpTradingActive) {
4017
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading already active");
4018
+ break;
4019
+ }
4020
+ this.perpTradingActive = true;
4021
+ if (this.mode !== "trading") {
4022
+ this.mode = "trading";
4023
+ this.isRunning = true;
4024
+ }
4025
+ console.log("Perp trading mandated via command center");
4026
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle active");
4027
+ this.relay?.sendMessage(
4028
+ "system",
4029
+ "success",
4030
+ "Perp Trading Active",
4031
+ "Dedicated perp trading cycle is now running every interval."
4032
+ );
4033
+ this.sendRelayStatus();
4034
+ break;
4035
+ case "stop_perp_trading":
4036
+ if (!this.perpTradingActive) {
4037
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading already stopped");
4038
+ break;
4039
+ }
4040
+ this.perpTradingActive = false;
4041
+ console.log("Perp trading cycle stopped via command center");
4042
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle stopped");
4043
+ this.relay?.sendMessage(
4044
+ "system",
4045
+ "info",
4046
+ "Perp Trading Stopped",
4047
+ "Dedicated perp cycle stopped. Hyperliquid remains connected \u2014 strategy can still include perp signals."
4048
+ );
4049
+ this.sendRelayStatus();
4050
+ break;
4051
+ case "update_perp_params": {
4052
+ const perpParams = cmd.params || {};
4053
+ let perpUpdated = false;
4054
+ if (this.config.perp && perpParams.maxLeverage !== void 0) {
4055
+ const val = Number(perpParams.maxLeverage);
4056
+ if (val >= 1 && val <= 50) {
4057
+ this.config.perp.maxLeverage = val;
4058
+ perpUpdated = true;
4059
+ }
4060
+ }
4061
+ if (this.config.perp && perpParams.maxNotionalUSD !== void 0) {
4062
+ const val = Number(perpParams.maxNotionalUSD);
4063
+ if (val >= 100) {
4064
+ this.config.perp.maxNotionalUSD = val;
4065
+ perpUpdated = true;
4066
+ }
4067
+ }
4068
+ if (perpUpdated) {
4069
+ this.relay?.sendCommandResult(cmd.id, true, "Perp parameters updated");
4070
+ this.relay?.sendMessage(
4071
+ "config_updated",
4072
+ "info",
4073
+ "Perp Params Updated",
4074
+ `Max leverage: ${this.config.perp?.maxLeverage}x, Max notional: $${this.config.perp?.maxNotionalUSD?.toLocaleString()}`
4075
+ );
4076
+ } else {
4077
+ this.relay?.sendCommandResult(cmd.id, false, "No valid perp parameters provided");
4078
+ }
4079
+ break;
4080
+ }
2483
4081
  case "refresh_status":
2484
4082
  this.sendRelayStatus();
2485
4083
  this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
@@ -2534,8 +4132,33 @@ var AgentRuntime = class {
2534
4132
  policy: vaultConfig.policy,
2535
4133
  hasVault: false,
2536
4134
  vaultAddress: null
2537
- }
4135
+ },
4136
+ perp: this.perpConnected ? {
4137
+ enabled: true,
4138
+ trading: this.perpTradingActive,
4139
+ equity: 0,
4140
+ unrealizedPnl: 0,
4141
+ marginUsed: 0,
4142
+ openPositions: 0,
4143
+ effectiveLeverage: 0,
4144
+ pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4145
+ } : void 0
2538
4146
  };
4147
+ if (this.perpConnected && this.perpPositions && status.perp) {
4148
+ this.perpPositions.getAccountSummary().then((account) => {
4149
+ if (status.perp) {
4150
+ status.perp.equity = account.totalEquity;
4151
+ status.perp.unrealizedPnl = account.totalUnrealizedPnl;
4152
+ status.perp.marginUsed = account.totalMarginUsed;
4153
+ status.perp.effectiveLeverage = account.effectiveLeverage;
4154
+ }
4155
+ }).catch(() => {
4156
+ });
4157
+ this.perpPositions.getPositionCount().then((count) => {
4158
+ if (status.perp) status.perp.openPositions = count;
4159
+ }).catch(() => {
4160
+ });
4161
+ }
2539
4162
  this.relay.sendHeartbeat(status);
2540
4163
  }
2541
4164
  /**
@@ -2634,8 +4257,75 @@ var AgentRuntime = class {
2634
4257
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2635
4258
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
2636
4259
  }
4260
+ if (this.perpConnected && this.perpTradingActive) {
4261
+ try {
4262
+ await this.runPerpCycle();
4263
+ } catch (error) {
4264
+ const message = error instanceof Error ? error.message : String(error);
4265
+ console.error("Error in perp cycle:", message);
4266
+ this.relay?.sendMessage("system", "error", "Perp Cycle Error", message);
4267
+ }
4268
+ }
2637
4269
  this.sendRelayStatus();
2638
4270
  }
4271
+ /**
4272
+ * Run a single perp trading cycle.
4273
+ * Fetches market data, positions, calls perp strategy, applies risk filters, executes.
4274
+ * Fills arrive async via WebSocket and are auto-recorded on Base.
4275
+ */
4276
+ async runPerpCycle() {
4277
+ if (!this.perpClient || !this.perpPositions || !this.perpOrders || !this.perpConnected) return;
4278
+ const perpConfig = this.config.perp;
4279
+ if (!perpConfig?.enabled) return;
4280
+ console.log(" [PERP] Running perp cycle...");
4281
+ const [positions, account] = await Promise.all([
4282
+ this.perpPositions.getPositions(true),
4283
+ this.perpPositions.getAccountSummary(true)
4284
+ ]);
4285
+ console.log(` [PERP] Equity: $${account.totalEquity.toFixed(2)}, Positions: ${positions.length}, Leverage: ${account.effectiveLeverage.toFixed(1)}x`);
4286
+ const dangerousPositions = await this.perpPositions.getDangerousPositions(0.7);
4287
+ for (const pos of dangerousPositions) {
4288
+ console.warn(` [PERP] WARNING: ${pos.instrument} near liquidation`);
4289
+ this.relay?.sendMessage(
4290
+ "perp_liquidation_warning",
4291
+ "warning",
4292
+ "Near Liquidation",
4293
+ `${pos.instrument} ${pos.size > 0 ? "LONG" : "SHORT"} \u2014 close to liquidation price $${pos.liquidationPrice.toFixed(2)}`,
4294
+ { instrument: pos.instrument, liquidationPrice: pos.liquidationPrice, markPrice: pos.markPrice }
4295
+ );
4296
+ }
4297
+ const instruments = perpConfig.allowedInstruments || ["ETH", "BTC", "SOL"];
4298
+ const marketData = await this.perpClient.getMarketData(instruments);
4299
+ if (!this.perpStrategy) {
4300
+ return;
4301
+ }
4302
+ let signals;
4303
+ try {
4304
+ signals = await this.perpStrategy(marketData, positions, account, this.llm, this.config);
4305
+ } catch (error) {
4306
+ const message = error instanceof Error ? error.message : String(error);
4307
+ console.error(" [PERP] Strategy error:", message);
4308
+ return;
4309
+ }
4310
+ console.log(` [PERP] Strategy generated ${signals.length} signals`);
4311
+ const maxLeverage = perpConfig.maxLeverage ?? 10;
4312
+ const maxNotionalUSD = perpConfig.maxNotionalUSD ?? 5e4;
4313
+ const filteredSignals = this.riskManager.filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD);
4314
+ console.log(` [PERP] ${filteredSignals.length} signals passed risk checks`);
4315
+ for (const signal of filteredSignals) {
4316
+ if (signal.action === "hold") continue;
4317
+ if (signal.price === 0) {
4318
+ const md = marketData.find((m) => m.instrument === signal.instrument);
4319
+ if (md) signal.price = md.midPrice;
4320
+ }
4321
+ const result = await this.perpOrders.placeOrder(signal);
4322
+ if (result.success) {
4323
+ console.log(` [PERP] Order placed: ${signal.instrument} ${signal.action} \u2014 ${result.status}`);
4324
+ } else {
4325
+ console.warn(` [PERP] Order failed: ${signal.instrument} ${signal.action} \u2014 ${result.error}`);
4326
+ }
4327
+ }
4328
+ }
2639
4329
  /**
2640
4330
  * Check if ETH balance is below threshold and notify.
2641
4331
  * Returns true if trading should continue, false if ETH is critically low.
@@ -2681,6 +4371,12 @@ var AgentRuntime = class {
2681
4371
  this.isRunning = false;
2682
4372
  this.processAlive = false;
2683
4373
  this.mode = "idle";
4374
+ if (this.perpWebSocket) {
4375
+ this.perpWebSocket.disconnect();
4376
+ }
4377
+ if (this.perpRecorder) {
4378
+ this.perpRecorder.stop();
4379
+ }
2684
4380
  if (this.relay) {
2685
4381
  this.relay.disconnect();
2686
4382
  }
@@ -2830,7 +4526,7 @@ var AgentRuntime = class {
2830
4526
  };
2831
4527
 
2832
4528
  // src/cli.ts
2833
- var import_accounts3 = require("viem/accounts");
4529
+ var import_accounts6 = require("viem/accounts");
2834
4530
 
2835
4531
  // src/secure-env.ts
2836
4532
  var crypto = __toESM(require("crypto"));
@@ -3093,8 +4789,8 @@ async function checkFirstRunSetup(configPath) {
3093
4789
  if (walletSetup === "generate") {
3094
4790
  console.log("[WALLET] Generating a new wallet for your agent...");
3095
4791
  console.log("");
3096
- const generatedKey = (0, import_accounts3.generatePrivateKey)();
3097
- const account = (0, import_accounts3.privateKeyToAccount)(generatedKey);
4792
+ const generatedKey = (0, import_accounts6.generatePrivateKey)();
4793
+ const account = (0, import_accounts6.privateKeyToAccount)(generatedKey);
3098
4794
  privateKey = generatedKey;
3099
4795
  walletAddress = account.address;
3100
4796
  console.log(" New wallet created!");
@@ -3124,7 +4820,7 @@ async function checkFirstRunSetup(configPath) {
3124
4820
  process.exit(1);
3125
4821
  }
3126
4822
  try {
3127
- const account = (0, import_accounts3.privateKeyToAccount)(privateKey);
4823
+ const account = (0, import_accounts6.privateKeyToAccount)(privateKey);
3128
4824
  walletAddress = account.address;
3129
4825
  console.log("");
3130
4826
  console.log(` Wallet address: ${walletAddress}`);