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:
- Enforce max notional exposure per position
- Limit leverage multipliers
- Require stop losses on every position
- Support per-market allowlists/denylists
- Time-based trading windows
- 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 minuteUser 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
- Signing flows — Wallet integration options
- Low-latency recipes — Performance optimization
- Agent templates — Reference implementations