Bloxwapdocs
Developers

Agent permissions

Delegated wallet scopes and risk management for automated trading agents.

Agent wallets enable automated trading without exposing user private keys. This guide covers Hyperliquid's built-in agent system and proposes an additional permission layer for risk management.

Hyperliquid agent wallets

Hyperliquid supports agent wallet delegation at the protocol level. Users can approve an agent address for prompt-free trading.

Current capabilities

Agent approval on Hyperliquid is binary: an agent is either fully approved or not approved. Once approved, an agent can:

  • Place limit and market orders on any listed asset
  • Modify or cancel orders
  • Update leverage settings
  • Close positions

Limitations:

  • ❌ No max notional limits
  • ❌ No leverage restrictions
  • ❌ No required stop losses
  • ❌ No per-market permissions
  • ❌ No time-based restrictions

The agent has the same trading permissions as the user wallet that approved it.

Approve an agent

import { ExchangeClient, HttpTransport } from '@bloxwap/hyperliquid';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { createFastLocalWallet } from '@bloxwap/hyperliquid/signing';

// 1. Generate agent key (do this once, store securely)
const agentKey = generatePrivateKey();
const agentAccount = privateKeyToAccount(agentKey);

console.log('Agent address:', agentAccount.address);
console.log('Agent key (store securely):', agentKey);

// 2. User approves the agent
const userWallet = /* ... user's wallet ... */;
const userClient = new ExchangeClient({
  transport: new HttpTransport(),
  wallet: userWallet,
});

await userClient.approveAgent({
  agentAddress: agentAccount.address,
  agentName: 'MyTradingBot', // Optional label for tracking
});

// 3. Agent can now trade on behalf of user
const agentWallet = await createFastLocalWallet(agentKey);
const agentClient = new ExchangeClient({
  transport: new HttpTransport(),
  wallet: agentWallet,
});

await agentClient.limitOrder({
  coin: 'BTC',
  isBuy: true,
  sz: 0.1,
  limitPx: 50000,
  orderType: { limit: { tif: 'Gtc' } },
});

Revoke an agent

Users can revoke agent approval at any time:

await userClient.revokeAgent({
  agentAddress: agentAccount.address,
});

After revocation, the agent can no longer place new orders or modify existing ones.

Check agent status

Verify if an agent is currently approved:

const agents = await info.agents(userAddress);

const isApproved = agents.some(
  (agent) => 
    agent.address.toLowerCase() === agentAddress.toLowerCase() &&
    agent.validUntil != null &&
    agent.validUntil > Date.now()
);

Proposed permission layer

For agents that need risk controls beyond Hyperliquid's binary approval, implement an application-level permission layer.

Design goals:

  1. Enforce max notional exposure per position
  2. Limit leverage multipliers
  3. Require stop losses on every position
  4. Support per-market allowlists/denylists
  5. Time-based trading windows
  6. Emergency circuit breakers

Implementation approach:

This layer lives in your application code, not on-chain. The agent checks permissions before submitting orders to Hyperliquid.

Permission schema

interface AgentPermissions {
  agentAddress: string;
  userAddress: string;
  
  // Position limits
  maxNotionalPerPosition?: number;  // USD
  maxTotalNotional?: number;        // USD
  maxLeverage?: number;             // e.g., 10x
  
  // Required risk controls
  requireStopLoss: boolean;
  maxStopLossDistance?: number;     // % from entry
  
  // Market restrictions
  allowedMarkets?: string[];        // ['BTC', 'ETH', ...]
  deniedMarkets?: string[];         // ['MEME', ...]
  
  // Time restrictions
  tradingHours?: {
    start: string;  // HH:mm UTC
    end: string;    // HH:mm UTC
    days: number[]; // [1,2,3,4,5] = Mon-Fri
  };
  
  // Circuit breakers
  maxDailyLoss?: number;            // USD
  maxConsecutiveLosses?: number;
  
  // Metadata
  createdAt: number;
  expiresAt?: number;
  enabled: boolean;
}

Permission enforcement

Wrap your trading logic with permission checks:

async function placeOrderWithPermissions(
  permissions: AgentPermissions,
  order: OrderRequest,
) {
  // 1. Check if agent is enabled
  if (!permissions.enabled) {
    throw new Error('Agent is disabled');
  }
  
  // 2. Check expiry
  if (permissions.expiresAt && Date.now() > permissions.expiresAt) {
    throw new Error('Agent permissions expired');
  }
  
  // 3. Check market allowlist
  if (permissions.allowedMarkets && 
      !permissions.allowedMarkets.includes(order.coin)) {
    throw new Error(`Market ${order.coin} not in allowlist`);
  }
  
  // 4. Check market denylist
  if (permissions.deniedMarkets && 
      permissions.deniedMarkets.includes(order.coin)) {
    throw new Error(`Market ${order.coin} is denied`);
  }
  
  // 5. Check notional limit
  const notional = order.sz * order.limitPx;
  if (permissions.maxNotionalPerPosition && 
      notional > permissions.maxNotionalPerPosition) {
    throw new Error(
      `Notional ${notional} exceeds limit ${permissions.maxNotionalPerPosition}`
    );
  }
  
  // 6. Check leverage limit
  if (permissions.maxLeverage && 
      order.leverage > permissions.maxLeverage) {
    throw new Error(
      `Leverage ${order.leverage}x exceeds limit ${permissions.maxLeverage}x`
    );
  }
  
  // 7. Check stop loss requirement
  if (permissions.requireStopLoss && !order.stopLoss) {
    throw new Error('Stop loss required but not provided');
  }
  
  // 8. Check trading hours
  if (permissions.tradingHours && !isWithinTradingHours(permissions.tradingHours)) {
    throw new Error('Outside allowed trading hours');
  }
  
  // 9. Check circuit breakers
  const dailyLoss = await getDailyLoss(permissions.userAddress);
  if (permissions.maxDailyLoss && dailyLoss >= permissions.maxDailyLoss) {
    throw new Error('Daily loss limit reached');
  }
  
  // All checks passed - place order
  return await client.limitOrder(order);
}

function isWithinTradingHours(hours: AgentPermissions['tradingHours']): boolean {
  if (!hours) return true;
  
  const now = new Date();
  const day = now.getUTCDay();
  const time = now.getUTCHours() * 100 + now.getUTCMinutes();
  
  const [startHour, startMin] = hours.start.split(':').map(Number);
  const [endHour, endMin] = hours.end.split(':').map(Number);
  const start = startHour * 100 + startMin;
  const end = endHour * 100 + endMin;
  
  return hours.days.includes(day) && time >= start && time <= end;
}

Storage

Store permissions in a database, not in code:

// Example with PostgreSQL
async function getAgentPermissions(
  agentAddress: string,
  userAddress: string,
): Promise<AgentPermissions | null> {
  const result = await db.query(
    `SELECT * FROM agent_permissions 
     WHERE agent_address = $1 AND user_address = $2 AND enabled = true`,
    [agentAddress, userAddress]
  );
  
  return result.rows[0] || null;
}

async function updateAgentPermissions(
  agentAddress: string,
  userAddress: string,
  updates: Partial<AgentPermissions>,
) {
  await db.query(
    `UPDATE agent_permissions 
     SET ${Object.keys(updates).map((k, i) => `${k} = $${i + 3}`).join(', ')}
     WHERE agent_address = $1 AND user_address = $2`,
    [agentAddress, userAddress, ...Object.values(updates)]
  );
}

Circuit breakers

Implement automatic safety shutoffs:

async function checkCircuitBreakers(
  permissions: AgentPermissions,
  userAddress: string,
) {
  // 1. Daily loss limit
  const dailyLoss = await getDailyLoss(userAddress);
  if (permissions.maxDailyLoss && dailyLoss >= permissions.maxDailyLoss) {
    await disableAgent(permissions.agentAddress, userAddress);
    await notifyUser(userAddress, 'Agent disabled: daily loss limit reached');
    return false;
  }
  
  // 2. Consecutive losses
  const losses = await getRecentLosses(userAddress);
  const consecutive = countConsecutive(losses);
  if (permissions.maxConsecutiveLosses && 
      consecutive >= permissions.maxConsecutiveLosses) {
    await disableAgent(permissions.agentAddress, userAddress);
    await notifyUser(userAddress, 
      `Agent disabled: ${consecutive} consecutive losses`);
    return false;
  }
  
  return true;
}

async function disableAgent(agentAddress: string, userAddress: string) {
  await db.query(
    `UPDATE agent_permissions SET enabled = false 
     WHERE agent_address = $1 AND user_address = $2`,
    [agentAddress, userAddress]
  );
}

Safety best practices

Start small

  • Begin with conservative limits (e.g., max $100 notional)
  • Gradually increase as the agent proves reliable
  • Use testnet extensively before mainnet

Always require stops

Never allow naked positions without stop losses:

const permissions: AgentPermissions = {
  // ...
  requireStopLoss: true,
  maxStopLossDistance: 5, // Max 5% stop loss distance
};

Monitor continuously

Track agent performance and alert on anomalies:

setInterval(async () => {
  const positions = await client.positions(userAddress);
  const totalNotional = positions.reduce((sum, p) => sum + p.notional, 0);
  
  if (totalNotional > permissions.maxTotalNotional * 0.9) {
    await notifyUser(userAddress, 'Warning: approaching notional limit');
  }
}, 60_000); // Check every minute

User control

Provide an emergency stop button:

async function emergencyStop(userAddress: string) {
  // 1. Disable all agents
  await db.query(
    `UPDATE agent_permissions SET enabled = false WHERE user_address = $1`,
    [userAddress]
  );
  
  // 2. Close all positions (optional)
  const positions = await client.positions(userAddress);
  for (const position of positions) {
    await client.marketClose({
      coin: position.coin,
      sz: position.szi,
    });
  }
  
  // 3. Notify user
  await notifyUser(userAddress, 'Emergency stop activated');
}

Regulatory considerations

If building a permission layer that manages user funds, consult legal counsel. Automated trading services may require:

  • Money transmitter licenses
  • Securities dealer registration
  • Customer account segregation
  • Audit trails and reporting

This guide provides technical patterns, not legal advice.

Next steps

On this page