Guides

Build a Hyperliquid Trading App with Builder Codes

Implement builder fee approval, agent wallet sessions, and order placement using Dwellir's Hyperliquid endpoints.

Builder codes let Hyperliquid application developers earn a fee on every trade routed through their app. The protocol enforces fee caps (10 basis points for perpetuals, 100 basis points for spot), handles settlement automatically, and has already paid out over $40M to builders. But integrating builder codes into a real application involves more than attaching a fee field to an order. You need to handle EIP-712 wallet signatures, work around chain ID mismatches that break every major wallet provider, manage ephemeral session keys, and wire up approval and revocation flows.

This guide walks through the complete implementation. You will build a Next.js application that covers the full builder code lifecycle, from fee approval through order placement to revocation, using Dwellir's Hyperliquid endpoints for production-grade infrastructure.

What you'll build

  • A Next.js trading application with a guided wizard that walks users through each step of the builder code lifecycle
  • EIP-712 builder fee approval and revocation using viem's typed data signing on Arbitrum
  • Ephemeral agent wallet sessions that solve Hyperliquid's chainId 1337 problem and eliminate repeated wallet popups
  • Order placement with builder code attribution routed through Dwellir's low-latency Hyperliquid endpoints

The companion repository at github.com/dwellir-public/hyperliquid-builder-codes-demo contains the complete working implementation. Every code example in this guide comes directly from that repo.

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ installed
  • A browser wallet such as MetaMask
  • A WalletConnect project ID from Reown's dashboard (free tier available)
  • A Dwellir API key from the Dwellir dashboard (optional for development, recommended for production)
  • Basic familiarity with TypeScript, React, and EIP-712 typed data signatures

How builder codes work

Builder codes create a revenue channel between Hyperliquid's protocol and third-party application developers. The lifecycle has four stages: approval, agent key creation, order placement with fee, and revocation.

The approval flow

Before your application can charge a builder fee, each user must explicitly authorize it. The user signs an EIP-712 typed data message specifying your builder address and a maximum fee rate. This signature is submitted to Hyperliquid as an approveBuilderFee action. Once confirmed, your application can attach fees up to that approved maximum on any order the user places through your app.

The approval is persistent. Users do not need to re-approve on subsequent visits unless they revoke the permission. Your application should check approval status on load and skip the approval step if it is already active.

The agent key pattern

Hyperliquid L1 actions (orders, cancels, transfers) use EIP-712 typed data signatures with a domain chainId of 1337. This value does not correspond to any real EVM chain. When your application asks MetaMask, WalletConnect, or another wallet provider to sign a message with chainId 1337, the wallet rejects it because the domain chain ID does not match the connected network.

The official Hyperliquid application solves this with agent keys, and the demo uses the same approach. The pattern separates two concerns:

  • Authorization: The user's real wallet signs an approveAgent action on chainId 42161 (Arbitrum), which the wallet accepts without issue
  • Execution: An ephemeral private key, generated in the browser and stored in sessionStorage, signs all subsequent L1 actions (chainId 1337) locally in JavaScript

The agent key is scoped to the user's wallet address and authorized only for trading actions. It cannot withdraw funds or perform administrative operations. When the user closes the browser tab, the key is cleared from sessionStorage.

Fee mechanics

Builder fees are denominated in tenths of basis points. A value of 10 means 10 tenths of a basis point, which equals 1 basis point (0.01%). The protocol enforces maximum caps:

Market typeMaximum fee
Perpetuals10 bps (0.10%)
Spot100 bps (1.00%)

The fee is deducted from the user's margin and sent to the builder address on each filled order. Settlement is automatic and on-chain.

Revocation

Users can revoke a builder's approval at any time by signing a new approveBuilderFee action with the maximum fee set to zero. Your application should provide this as an explicit step in the UI.

Clone and configure the demo

Start by cloning the companion repository and installing dependencies:

Bash
git clone https://github.com/dwellir-public/hyperliquid-builder-codes-demo.git
cd hyperliquid-builder-codes-demo
npm install

Copy the example environment file and add your WalletConnect project ID:

Bash
cp .env.local.example .env.local

Edit .env.local with your credentials:

Bash
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_here
NEXT_PUBLIC_DWELLIR_API_KEY=your_dwellir_api_key

The WalletConnect project ID is required. The Dwellir API key is optional for development. Without it, the app falls back to Hyperliquid's public endpoints. With it, requests route through Dwellir's managed infrastructure for higher rate limits and lower latency.

Start the development server:

Bash
npm run dev

Open http://localhost:3000. The app defaults to mainnet. To experiment on testnet without risking real funds, change the default network in src/components/Providers.tsx:

TypeScript
const [network, setNetwork] = useState<NetworkKey>("testnet");

The codebase has full testnet support with separate endpoints, contract addresses, and wallet chain switching configured.

Customizing the builder address

To use your own builder address and fee rate, update src/config/constants.ts:

TypeScript
export const DWELLIR_BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS';
export const DEFAULT_BUILDER_FEE = 10; // tenths of basis points (10 = 1 bps = 0.01%)

Project structure

The demo follows a standard Next.js App Router layout:

Text
src/
├── app/          # Next.js pages and layouts
├── components/   # UI components for each wizard step
├── hooks/        # React hooks for wallet, agent, and builder state
├── lib/          # API client and utility functions
└── config/       # Constants, wagmi setup, and endpoint configuration

Key files referenced throughout this guide:

FilePurpose
src/config/constants.tsBuilder address, fee rates, endpoint URLs
src/hooks/useBuilderApproval.tsApproval status polling and fee approval logic
src/hooks/useAgentWallet.tsxAgent key generation, storage, and context
src/components/ApproveBuilder.tsxBuilder fee approval UI and signing flow
src/components/PlaceOrderStep.tsxOrder construction and submission

Builder fee approval

The first step in the builder code lifecycle is getting the user's permission to charge fees. This happens through an EIP-712 typed data signature on Arbitrum.

Checking existing approval

Before prompting the user, check whether they have already approved your builder address. In the demo repo's src/hooks/useBuilderApproval.ts, a React Query hook polls the approval status:

TypeScript
const { data: maxFee } = useQuery({
  queryKey: ['maxBuilderFee', network, address, builder],
  queryFn: () => queryMaxBuilderFee(network, address, builder),
  refetchInterval: network === 'mainnet' ? 5_000 : 10_000,
});

The queryMaxBuilderFee function calls Hyperliquid's info API to retrieve the current maximum fee the user has approved for your builder address. If the returned value is greater than zero, the user has an active approval and your app can skip the approval step.

The polling interval is set to 5 seconds on mainnet and 10 seconds on testnet. This allows the UI to update automatically once the approval transaction confirms, without requiring a manual refresh.

Signing the approval

When the user needs to approve (or has not yet approved), the app presents a signing flow. In src/components/ApproveBuilder.tsx, the approval action uses viem's wallet client:

TypeScript
await walletClient.approveBuilderFee({
  builder: DWELLIR_BUILDER_ADDRESS,
  maxFeeRate: '0.0100%', // 1 basis point
});

The maxFeeRate parameter takes a percentage string. The demo includes a helper function in src/config/constants.ts that converts the raw integer fee (tenths of basis points) to the SDK's expected format:

TypeScript
export const DEFAULT_BUILDER_FEE = 10; // 10 tenths-of-a-bps = 1 bps = 0.01%

export function feeToPercent(fee: number): string {
  return `${(fee / 100).toFixed(4)}%`;
}

When feeToPercent(10) is called, it returns "0.0100%", which the SDK interprets as 1 basis point.

What happens under the hood

The approveBuilderFee call constructs an EIP-712 typed data structure with the following fields:

  • domain: chainId 42161 (Arbitrum), which the wallet recognizes and accepts
  • types: The ApproveBuilderFee action type, defining the builder address and fee rate
  • message: The specific builder address and maximum fee the user is authorizing

The wallet presents this as a human-readable signature request. Once the user confirms, the signed message is submitted to Hyperliquid's exchange API. The protocol records the approval on-chain, and subsequent orders from this user can include builder fees up to the approved maximum.

Agent wallet session management

With the builder fee approved, the next challenge is signing orders. Hyperliquid's L1 actions require EIP-712 signatures with chainId 1337, which wallet providers reject. Agent wallets solve this by creating an ephemeral signing key that operates locally in the browser.

Generating the agent key

In the demo repo's src/hooks/useAgentWallet.tsx, the agent approval flow generates a random private key and registers it with Hyperliquid:

TypeScript
const approveAgent = async () => {
  const pk = generatePrivateKey();
  const account = privateKeyToAccount(pk);

  // Register with Hyperliquid via your real wallet (chainId 42161)
  await userWalletClient.approveAgent({
    agentAddress: account.address,
    agentName: 'DwellirBuilder',
  });

  // Store in sessionStorage for the tab's lifetime
  sessionStorage.setItem(`hl-agent-key-${userAddress}`, pk);
};

This flow works in three stages:

  1. Key generation: generatePrivateKey() from viem creates a cryptographically random private key entirely in the browser. No server communication occurs.
  2. On-chain registration: The user's real wallet signs an approveAgent action using chainId 42161 (Arbitrum). This is the only wallet popup in the entire order flow. Hyperliquid records the association between the user's address and the agent key's address.
  3. Local storage: The private key is stored in sessionStorage, scoped to the current browser tab. Closing the tab destroys the key.

Session scoping and security

The agent key pattern provides several security properties:

  • Tab-scoped lifetime: sessionStorage is cleared when the tab closes, limiting the window of exposure
  • Trade-only authorization: The agent key can sign orders and cancellations but cannot withdraw funds, transfer assets, or modify account settings
  • User-scoped binding: The key is registered against a specific user address and cannot act on behalf of other accounts
  • No server transmission: The private key never leaves the browser. All signing happens in JavaScript using viem's privateKeyToAccount

For production deployments, harden against XSS attacks by implementing strict Content Security Policy (CSP) headers. An XSS vulnerability could read the agent key from sessionStorage, and while the key cannot withdraw funds, it could place unauthorized trades.

Sharing the agent wallet via context

The agent wallet client is shared across the application using React Context. In the demo, useAgentWallet.tsx exports a provider and hook:

TypeScript
// Any component that needs to place or cancel orders
const { agentWalletClient, isAgentActive } = useAgentWallet();

Components that need to place or cancel orders consume the agent wallet client from context. The client signs all L1 actions (chainId 1337) entirely in memory using the ephemeral key. No wallet popup appears for individual orders.

Restoring sessions

When the user returns to the app in the same tab, the hook checks sessionStorage for an existing agent key:

TypeScript
const existingKey = sessionStorage.getItem(`hl-agent-key-${userAddress}`);

If a valid key exists and is still registered with Hyperliquid, the app restores the session automatically. The wizard skips the agent activation step and jumps to order placement.

Place orders with builder codes

With a valid agent wallet session and an active builder fee approval, placing orders with builder code attribution is straightforward. The builder fee attaches to each order as a single field.

Constructing the order

In the demo repo's src/components/PlaceOrderStep.tsx, the order action uses the agent wallet client:

TypeScript
await agentWalletClient.order({
  orders: [{
    coin: selectedAsset,
    isBuy: side === 'buy',
    sz: size,
    limitPx: price,
    orderType: { limit: { tif: 'Gtc' } },
    reduceOnly: false,
  }],
  builder: {
    b: DWELLIR_BUILDER_ADDRESS,
    f: DEFAULT_BUILDER_FEE,
  },
});

The order structure has two parts:

The order parameters:

  • coin: The trading pair (e.g., "ETH", "BTC")
  • isBuy: Direction of the trade
  • sz: Position size in the asset's base unit
  • limitPx: Price for limit orders, or the market price for immediate execution
  • orderType: Order type with time-in-force. Gtc (Good til Cancelled) for limit orders, or { limit: { tif: 'Ioc' } } for market-like immediate-or-cancel orders
  • reduceOnly: Whether the order can only reduce an existing position

The builder field:

  • b: Your builder address. This must match the address the user approved in the earlier step.
  • f: The fee in tenths of basis points. This must not exceed the user's approved maximum.

Signing and submission

The agentWalletClient.order() call handles signing internally. It constructs an EIP-712 typed data structure with chainId 1337, signs it with the ephemeral agent key, and submits the signed payload to Hyperliquid's exchange API. Because the signing happens locally in JavaScript, there is no wallet popup and no chain ID validation issue.

The response includes the order status (filled, partially filled, or resting on the book), the fill price, and any error messages. The demo displays this information in the UI along with the open position, entry price, and unrealized PnL.

Batch orders

The orders parameter accepts an array, so you can submit multiple orders in a single call:

TypeScript
await agentWalletClient.order({
  orders: [
    {
      coin: 'ETH',
      isBuy: true,
      sz: '0.1',
      limitPx: '3000',
      orderType: { limit: { tif: 'Gtc' } },
      reduceOnly: false,
    },
    {
      coin: 'ETH',
      isBuy: false,
      sz: '0.1',
      limitPx: '3500',
      orderType: { limit: { tif: 'Gtc' } },
      reduceOnly: false,
    },
  ],
  builder: {
    b: DWELLIR_BUILDER_ADDRESS,
    f: DEFAULT_BUILDER_FEE,
  },
});

All orders in the batch share the same builder attribution. This is useful for placing bracket orders (entry + take-profit + stop-loss) in a single atomic submission.

Configure Dwellir endpoint routing

The demo supports two routing modes: Hyperliquid's public endpoints and Dwellir's managed infrastructure. The routing decision happens at configuration time based on the presence of a Dwellir API key.

Endpoint configuration

In src/config/constants.ts, the endpoint URLs are constructed conditionally:

TypeScript
const DWELLIR_API_KEY = process.env.NEXT_PUBLIC_DWELLIR_API_KEY;

const mainnetConfig = {
  infoUrl: DWELLIR_API_KEY
    ? `https://api-hyperliquid-mainnet-info.n.dwellir.com/${DWELLIR_API_KEY}/info`
    : 'https://api.hyperliquid.xyz/info',
  wsUrl: DWELLIR_API_KEY
    ? `wss://api-hyperliquid-mainnet-orderbook.n.dwellir.com/${DWELLIR_API_KEY}/ws`
    : 'wss://api.hyperliquid.xyz/ws',
};

When NEXT_PUBLIC_DWELLIR_API_KEY is set in .env.local, all API requests and WebSocket connections route through Dwellir's infrastructure. Without it, the app uses Hyperliquid's public endpoints. No code changes are required to switch between the two, just set or remove the environment variable.

Why route through Dwellir

Hyperliquid's public endpoints are functional for development and testing, but production trading applications benefit from managed infrastructure:

  • Higher rate limits: Public endpoints enforce strict rate limits that trading applications can exceed during periods of high market activity. Dwellir endpoints provide configurable limits scaled to your application's needs.
  • Deeper order book data: Dwellir's order book server provides up to 100 levels of bid/ask depth compared to the 5 levels available on public endpoints. For applications that display order books or calculate market impact, this is a significant difference.
  • Lower latency: Dwellir operates geographically distributed infrastructure optimized for Hyperliquid's network topology. For latency-sensitive order placement, this reduces round-trip times.
  • WebSocket stability: Long-lived WebSocket connections for order book and trade feeds benefit from Dwellir's connection management and automatic reconnection handling.

Testnet configuration

The demo includes a parallel configuration for Hyperliquid's testnet:

TypeScript
const testnetConfig = {
  infoUrl: DWELLIR_API_KEY
    ? `https://api-hyperliquid-testnet-info.n.dwellir.com/${DWELLIR_API_KEY}/info`
    : 'https://api.hyperliquid-testnet.xyz/info',
  wsUrl: DWELLIR_API_KEY
    ? `wss://api-hyperliquid-testnet-orderbook.n.dwellir.com/${DWELLIR_API_KEY}/ws`
    : 'wss://api.hyperliquid-testnet.xyz/ws',
};

The app switches between mainnet and testnet configurations at runtime. Both environments support Dwellir endpoint routing, so you can test your integration against managed infrastructure before going live.

Production tips

Moving from the demo to a production trading application requires attention to several details that the demo intentionally keeps simple.

Fee formatting

The most common integration bug is fee formatting. Hyperliquid's protocol expects fees in tenths of basis points as an integer. The SDK's approveBuilderFee method expects a percentage string. Mixing these up silently results in fees that are 100x too high or 100x too low.

RepresentationValueMeaning
Tenths of bps (integer)10Used in the builder.f field on orders
Basis points10.01% of trade value
Percentage string"0.0100%"Used in approveBuilderFee

Always use a conversion function like feeToPercent() rather than constructing percentage strings manually. Test with small orders on testnet to verify the fee is what you expect.

Session key rotation

The demo stores agent keys in sessionStorage, which clears on tab close. For production applications, consider these alternatives:

  • Periodic rotation: Generate a new agent key every N hours or after N orders, even within the same session. This limits the window of exposure if a key is compromised.
  • Per-session naming: Include a timestamp or session ID in the agentName parameter to make key auditing easier.
  • Explicit cleanup: Provide a "disconnect" or "end session" button that clears the agent key from storage and optionally revokes the agent registration on-chain.

Avoid storing agent keys in localStorage. Unlike sessionStorage, localStorage persists across tab closures and browser restarts, extending the key's lifetime beyond what most users expect.

Error handling for rejected signatures

Wallet signature requests can fail for several reasons. Your application should handle each case with a clear user-facing message:

  • User rejection: The user clicked "Reject" in the wallet popup. Allow them to retry.
  • Wrong network: The wallet is connected to a chain other than Arbitrum. Prompt the user to switch networks using wagmi's switchChain.
  • Insufficient balance: For deposit steps, the user may not have enough USDC on Arbitrum. Show current balances before initiating the transaction.
  • Rate limiting: If the exchange API returns a rate limit error, implement exponential backoff before retrying.

The demo catches these errors and displays human-readable messages. In production, log the raw error details to your monitoring system for debugging while showing a simplified message to the user.

Monitoring builder fees

Track builder fee revenue by querying Hyperliquid's info API for fills associated with your builder address. Key metrics to monitor:

  • Fill volume per day: Total notional value of trades with your builder code attached
  • Fee revenue: Sum of fees collected, broken down by trading pair
  • Approval count: Number of unique addresses that have approved your builder address
  • Revocation rate: How often users revoke approval, which may indicate the fee is too high

Content Security Policy

Since the agent key lives in sessionStorage, an XSS attack could read it. While the key cannot withdraw funds, it could place unauthorized trades. Mitigate this risk with strict CSP headers:

Text
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self' https://*.dwellir.com https://api.hyperliquid.xyz wss://api.hyperliquid.xyz

Adjust the allowed domains based on your deployment's needs, but keep the script-src directive as restrictive as possible.

Next steps

With the builder code lifecycle implemented, you have a working foundation for a Hyperliquid trading application. Here are some directions to explore:

For production infrastructure with higher rate limits and dedicated nodes, sign up at the Dwellir dashboard or contact the Dwellir team to discuss your requirements.