Skip to main content
Glama
README.md
<div align="center">

```
  ██████╗ ██████╗ ███████╗██╗  ██╗   ██████╗ ██████╗  ██████╗
 ██╔════╝ ██╔══██╗██╔════╝╚██╗██╔╝   ██╔══██╗██╔══██╗██╔═══██╗
 ██║  ███╗██║  ██║█████╗   ╚███╔╝    ██████╔╝██████╔╝██║   ██║
 ██║   ██║██║  ██║██╔══╝   ██╔██╗    ██╔═══╝ ██╔══██╗██║   ██║
 ╚██████╔╝██████╔╝███████╗██╔╝ ██╗   ██║     ██║  ██║╚██████╔╝
  ╚═════╝ ╚═════╝ ╚══════╝╚═╝  ╚═╝   ╚═╝     ╚═╝  ╚═╝ ╚═════╝
               · p r o ·    powered by GEMACH
```

**AI Agent Skill for [GDEX Pro](https://gdex.pro)** — the self-custody trading terminal by [Gemach](https://gemach.io)  
Cross-chain spot · HyperLiquid perps · Copy trading · Portfolio · Token discovery · Managed custody

[![npm version](https://img.shields.io/npm/v/@gemachdao/gdex-mcp-server.svg?style=for-the-badge&label=mcp-server)](https://www.npmjs.com/package/@gemachdao/gdex-mcp-server)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-F7DF1E.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
[![skills.sh](https://img.shields.io/badge/skills.sh-compatible-8B5CF6.svg?style=for-the-badge)](https://skills.sh)
[![Tests](https://img.shields.io/badge/tests-103%20passing-22C55E.svg?style=for-the-badge)](#testing)

</div>


---

## Why agents use GDEX

GDEX is the trading surface. This skill is how AI agents (Claude, Cursor, Codex, and 40+ others) install and trade on it — spot, perps, copy-trade, and bridge — without building their own exchange stack.

**Fastest path for an agent:**

```bash
npx skills add GemachDAO/gdex-skill --all --agent '*' -g
```

Then open [gdex.pro](https://gdex.pro) for the human terminal, or run the [MCP server](#-mcp-server) so the agent can execute trades itself.

---

## Table of Contents

- [Install as an Agent Skill](#-install-as-an-agent-skill)
- [MCP Server](#-mcp-server)
- [SDK Installation](#-sdk-installation)
- [Quick Start](#-quick-start)
- [Verify (offline)](#-verify-offline)
- [Authentication](#-authentication)
- [API Reference](#-api-reference)
  - [Spot Trading](#spot-trading)
  - [Perpetual Futures](#perpetual-futures-hyperliquid)
  - [Limit Orders](#limit-orders)
  - [Copy Trading](#copy-trading)
  - [HL Perp Copy Trading](#hl-perp-copy-trading)
  - [Portfolio](#portfolio)
  - [Token Information](#token-information)
  - [Top Traders](#top-traders)
  - [Bridge](#bridge)
  - [Wallet Info](#wallet-info)
  - [Wallet Generation](#wallet-generation)
- [Supported Chains](#-supported-chains)
- [Error Handling](#-error-handling)
- [Utility Functions](#-utility-functions)
- [Testing](#-testing)
- [Architecture](#-architecture)

---

## 🤖 Install as an Agent Skill

Install directly into Claude Code, Cursor, Codex, Windsurf, and [40+ other agents](https://github.com/vercel-labs/skills#supported-agents) using the [skills CLI](https://skills.sh):

```bash
# Install all skills (recommended)
npx skills add GemachDAO/gdex-skill --all --agent '*' -g

# Pick skills interactively
npx skills add GemachDAO/gdex-skill

# Install a specific skill
npx skills add GemachDAO/gdex-skill --skill gdex-spot-trading
```

GDEX uses a **multi-skill architecture** — agents load only the skills they need, keeping context lean and focused.

### Available Skills

| Skill | Description |
|-------|-------------|
| `gdex-onboarding` | Platform overview, architecture, supported chains, quickstart |
| `gdex-retailer-onboarding` | Retailer partner integrations — branded onboarding partners on the GDEX stack |
| `gdex-authentication` | Managed-custody auth, encryption, session keys, API key login |
| `gdex-spot-trading` | Buy/sell tokens on any chain with DEX routing |
| `gdex-perp-trading` | HyperLiquid perpetual futures — positions, orders, leverage |
| `gdex-perp-funding` | Deposit/withdraw USDC to/from HyperLiquid |
| `gdex-limit-orders` | Create, cancel, and list limit orders |
| `gdex-portfolio` | Cross-chain portfolio, balances, trade history |
| `gdex-token-discovery` | Token details, trending tokens, OHLCV charts (no auth) |
| `gdex-token-import` | Import custom tokens into details, balances and portfolio |
| `gdex-livestream-discovery` | Solana livestream tokens, live status, big-buy alerts |
| `gdex-watchlist-social` | Watchlists, token comments, sentiment voting |
| `gdex-trending-promotion` | Book paid trending slots and check booking status |
| `gdex-xstocks` | Tokenised equities (xStocks) listing |
| `gdex-content-coins` | Zora content coins and creator coins on Base |
| `gdex-copy-trading` | Copy trade create/delete, leaderboards, tx history, DEX list (Solana only for writes) |
| `gdex-perp-copy-trading` | HL perp copy trading — top traders, create/manage configs, market data |
| `gdex-hl-outcomes` | HyperLiquid outcome (event) markets — list, order, manage positions |
| `gdex-hl-referral` | HyperLiquid referral info and reward claims |
| `gdex-bridge` | Cross-chain bridging with quotes |
| `gdex-transfers` | Native and ERC20/SPL transfers via managed custody |
| `gdex-wallet-setup` | Generate EVM wallets, session keys, wallet info (no auth) |
| `gdex-ui-install-setup` | React/Next.js project setup, SDK context providers, environment variables |
| `gdex-ui-trading-components` | React component patterns for order forms, position tables, copy trade panels |
| `gdex-ui-portfolio-dashboard` | Portfolio dashboard components — balances, trade history, chain selectors |
| `gdex-ui-wallet-connection` | Wallet connection UI — connect buttons, auth state, chain switching |
| `gdex-ui-theming` | CSS theming — dark/light mode, trading colors, responsive breakpoints, Tailwind |
| `gdex-ui-page-layouts` | Full page compositions — trading, portfolio, copy trading, bridge pages |
| `gdex-sdk-debugging` | Troubleshoot errors — error codes, encryption debugging, chain quirks, HL gotchas |

Each skill's `description` tells the agent when to load it. **No API key setup required** for trading skills (shared keys are built in).

### Risk & Market Data Skills

Deterministic data feeds and events for risk and research. Each ships a **standard-library Python script** that
prints NDJSON: no API key, no install, and every number comes from the script, not a model.

| Skill | Output |
|-------|--------|
| `gdex-hl-market-risk` | ~234 HyperLiquid core perps: funding, open interest, oracle premium, leverage caps, delisting |
| `gdex-hl-anomaly` | Scored, time-stamped HyperLiquid anomaly events (oracle divergence, funding extremity, liquidity shock) against per-market learned baselines, plus a coverage record per run. [Backtest](skills/gdex-hl-anomaly/backtest/RESULTS.md) |
| `gdex-token-risk` | GDEX token screen on 12 chains: price, liquidity, volume, honeypot, taxes, LP lock, holder concentration (missing security data is never "safe") |
| `gvault` | GVault (GMACL, Enzyme on Ethereum): NAV, share price, holdings, cumulative and annualised return |

```bash
python3 skills/gdex-hl-market-risk/scripts/hl_market_risk.py > hl.ndjson
```

### Skills Harness (bring your own key)

[`harness/`](harness/README.md) runs these skills with Claude on **your own** Anthropic API key.
`export` produces every feed with no model; `ask` answers questions with an agent that can only
cite figures that skill scripts printed. Every script run is logged with a sha256 of its output.

---

## 🔌 MCP Server

The GDEX MCP server exposes **117 tools** — full trading execution + SDK documentation — as [Model Context Protocol](https://modelcontextprotocol.io) tools. Any MCP-compatible AI agent can trade autonomously.

**Source code:** the server is implemented in this repository, in [`mcp-server/`](mcp-server). The entry point is [`mcp-server/src/index.ts`](mcp-server/src/index.ts) and the tool handlers are in [`mcp-server/src/tools/`](mcp-server/src/tools). The npm package `@gemachdao/gdex-mcp-server` is built from that directory by the release workflow. See [`mcp-server/README.md`](mcp-server/README.md) to run it from source or with the repo's `Dockerfile`.

### Quick Setup

```bash
# Auto-generate config for your AI client
npx @gemachdao/gdex-mcp-server init --client claude   # → .mcp.json
npx @gemachdao/gdex-mcp-server init --client cursor   # → .cursor/mcp.json
npx @gemachdao/gdex-mcp-server init --client vscode   # → .vscode/mcp.json
npx @gemachdao/gdex-mcp-server init --client codex    # → .codex/config.toml
npx @gemachdao/gdex-mcp-server init --client opencode  # → .opencode/mcp.json
```

### Claude Code plugin

```
/plugin marketplace add GemachDAO/gdex-skill
/plugin install gdex@gemachdao
```

Installs every GDEX skill plus the MCP server (`npx -y @gemachdao/gdex-mcp-server`). The optional GDEX API key is kept in your system credential store.

### Gemini CLI extension

```bash
gemini extensions install https://github.com/GemachDAO/gdex-skill
```

### Manual Config

Add to your client's MCP config:

```json
{
  "mcpServers": {
    "gdex-mcp-server": {
      "command": "npx",
      "args": ["@gemachdao/gdex-mcp-server"],
      "env": {
        "GDEX_API_KEY": "your-api-key"
      }
    }
  }
}
```

### Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| `GDEX_API_KEY` | GDEX API key — auto-authenticates on startup | Optional |
| `GDEX_API_URL` | Override API base URL (default: `https://trade-api.gemach.io/v1`) | Optional |

### MCP Execution Tools (109 tools)

| Category | Tools | Description |
|----------|-------|-------------|
| **Auth** | `auth_login`, `generate_session_keypair`, `managed_sign_in`, `build_sign_in_payload` | API key login, session keys, managed custody sign-in |
| **Spot Trading** | `buy_token`, `sell_token` | Buy/sell on Solana, Sui and 10 EVM chains (Ethereum, Base, Arbitrum, BSC and more) |
| **Perp Trading** | `open_perp_position`, `place_perp_order`, `close_perp_position`, `close_all_positions`, `cancel_perp_order`, `cancel_all_perp_orders`, `set_leverage`, `perp_deposit`, `perp_withdraw` | Full HyperLiquid perpetual futures — long/short, TP/SL; leverage up to each market's cap (40x on core BTC) |
| **Perp Data** | `get_account_state`, `get_perp_positions`, `get_mark_price`, `get_all_mid_prices`, `get_usdc_balance`, `get_hl_open_orders`, `get_hl_trade_history`, `get_hl_spot_state`, `get_trader_leverage` | Real-time HyperLiquid account, positions, prices |
| **Direct Execution** | `execute_cross_perp`, `execute_isolated_perp`, `execute_spot`, `direct_cancel_order` | Private-key execution — cross/isolated margin, spot, cancel |
| **Limit Orders** | `limit_buy`, `limit_sell`, `update_order`, `get_limit_orders` | Limit buy/sell with TP/SL, order management |
| **Copy Trading (Solana)** | `get_copy_trade_wallets`, `get_copy_trade_custom_wallets`, `get_copy_trade_gems`, `get_copy_trade_dexes`, `get_copy_trade_list`, `get_copy_trade_tx_list`, `create_copy_trade`, `update_copy_trade` | Auto-mirror top Solana traders |
| **Copy Trading (HL Perp)** | `get_hl_top_traders`, `get_hl_top_traders_by_pnl`, `get_hl_user_stats`, `get_hl_perp_dexes`, `get_hl_all_assets`, `get_hl_clearinghouse_state`, `get_hl_meta_and_asset_ctxs`, `get_hl_deposit_tokens`, `get_hl_copy_trade_list`, `get_hl_copy_trade_tx_list`, `create_hl_copy_trade`, `update_hl_copy_trade` | Copy HyperLiquid perp traders |
| **Portfolio & Data** | `get_portfolio`, `get_balances`, `get_trade_history`, `get_token_details`, `get_trending_tokens`, `get_ohlcv`, `get_top_traders`, `get_wallet_info`, `generate_evm_wallet` | Cross-chain portfolio, market data, OHLCV candles |
| **Bridge** | `estimate_bridge`, `execute_bridge`, `get_bridge_orders` | Cross-chain native token bridging |
| **Managed Custody** | `managed_purchase`, `managed_sell`, `managed_trade_status`, `build_trade_payload` | Low-level encrypted trade submission |
| **Transfers** | `transfer_native`, `transfer_token` | Send native and ERC20/SPL tokens via managed custody |
| **Social & Watchlist** | `add_comment`, `get_comments`, `vote_sentiment`, `get_watchlist`, `change_watchlist` | Token comments, sentiment votes, watchlists |
| **Token Import** | `import_token` | Add a custom token so it appears in details, balances and portfolio |
| **Market Discovery & Analytics** | `get_newest_tokens`, `get_top_tokens`, `get_token_trades`, `get_token_image`, `get_native_prices`, `get_xstocks`, `get_zora_tokens`, `get_wallet_performance`, `get_nof1_analytics`, `generate_pnl` | New and top tokens, trades, prices, xStocks, Zora coins, wallet performance, NoF1 analytics, PnL generation |
| **Livestream** | `get_currently_live`, `get_live_status`, `get_bigbuys` | Solana livestream tokens and big-buy alerts |
| **HL Outcome Markets** | `hl_outcomes`, `get_hl_outcome_volumes`, `hl_outcome_account`, `hl_create_outcome_order`, `hl_cancel_outcome_order`, `hl_close_outcome_order` | HyperLiquid outcome (event) markets |
| **HL Account & Referral** | `hl_enable_trading`, `hl_swap_collateral`, `hl_tx_list`, `hl_list_user_copy_pnl`, `hl_ref_info`, `hl_ref_claim`, `hl_builder_referral` | One-time HL enablement, HIP-3 collateral swaps, fills/orders, copy-trade PnL, referral rewards |
| **Promotion & Partners** | `trending_list`, `trending_options`, `trending_register`, `trending_booking_status`, `get_retailers` | Paid trending slots and retailer partners |
| **Account** | `oauth_login`, `associate_email` | Google sign-in and linking an email to a wallet |

### MCP Documentation Tools (8 tools)

| Tool | Description |
|------|-------------|
| `search_gdex_docs` | Search documentation by keyword |
| `get_sdk_pattern` | TypeScript code patterns by operation |
| `get_api_info` | API endpoint details (URL, method, params) |
| `explain_workflow` | Step-by-step trading workflows |
| `get_chain_info` | Supported chains and capabilities |
| `get_trading_guide` | Spot, perp, or limit trading guides |
| `get_copy_trade_guide` | Copy trading guides (Solana / HL) |
| `get_component_guide` | React UI component patterns |

---

## 📦 SDK Installation

```bash
npm install github:GemachDAO/gdex-skill
```

> Installs the SDK straight from GitHub (it builds on install). Imports stay
> `from '@gemachdao/gdex-skill'`. Pin a release with
> `npm install github:GemachDAO/gdex-skill#v4.1.1`.

> The install script displays a quick-start banner in your terminal. Optional peer dependencies for wallet signing (only needed for user-specific wallet auth):
> ```bash
> npm install ethers        # EVM wallet auth
> npm install bs58 tweetnacl  # Solana wallet auth
> ```

---

## 🚀 Quick Start

```typescript
import { GdexSkill, GDEX_API_KEY_PRIMARY } from '@gemachdao/gdex-skill';

// 1. Create skill instance
const skill = new GdexSkill();

// 2. Authenticate with pre-configured shared key — no wallet needed
skill.loginWithApiKey(GDEX_API_KEY_PRIMARY);

// 3. Spot buy on Solana
const trade = await skill.buyToken({
  chain: 'solana',
  tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
  amount: '0.1',   // 0.1 SOL
  slippage: 1,     // 1% max slippage
});
console.log('Trade submitted:', trade.jobId, '— status:', trade.status);

// 4. Open a BTC 10× long on HyperLiquid
const pos = await skill.openPerpPosition({
  coin: 'BTC',
  side: 'long',
  sizeUsd: '1000',
  leverage: 10,
  takeProfitPrice: '110000',
  stopLossPrice: '95000',
});

// 5. Read-only endpoints need no auth
const trending = await skill.getTrendingTokens({ chain: 'solana', period: '24h', limit: 5 });
console.log('Trending:', trending.map(t => t.symbol).join(', '));
```

---

## ✅ Verify (offline)

Confirm the SDK is installed and configured correctly — **no network connection or API key required**:

```bash
npm run verify
```

Sample output:

```
1. SDK import
  ✓  SDK imported from ../dist/index.js

2. API keys
  ✓  GDEX_API_KEY_PRIMARY   = 9b4e1c73...
  ✓  GDEX_API_KEY_SECONDARY = 2c8f0a91...
  ✓  GDEX_API_KEYS array    = 2 keys

3. GdexSkill instantiation
  ✓  new GdexSkill() — default config
  ✓  new GdexSkill({ apiUrl, timeout, maxRetries }) — custom config

4. Authentication state (offline)
  ✓  isAuthenticated() = false before login
  ✓  loginWithApiKey(GDEX_API_KEY_PRIMARY) → isAuthenticated() = true
  ✓  logout() → isAuthenticated() = false

...

  All 20 checks passed ✓
  SDK is ready — no network token required.
```

---

## 🔑 Authentication

### Shared API Keys (recommended for agents)

Two shared keys are pre-configured in the package — agents do not need to sign wallet transactions:

```typescript
import {
  GdexSkill,
  GDEX_API_KEY_PRIMARY,
  GDEX_API_KEY_SECONDARY,
  GDEX_API_KEYS,
} from '@gemachdao/gdex-skill';

const skill = new GdexSkill();
skill.loginWithApiKey(GDEX_API_KEY_PRIMARY);  // use primary
// or:
skill.loginWithApiKey(GDEX_API_KEY_SECONDARY); // use secondary
// or cycle through them:
skill.loginWithApiKey(GDEX_API_KEYS[0]);

skill.isAuthenticated(); // → true
skill.logout();          // clear session
```

> **Note:** Read-only endpoints (`getTrendingTokens`, `getTokenDetails`, `getOHLCV`, `getTopTraders`) do not require authentication.

### Wallet-based Auth (advanced)

For user-owned wallets or custom signers (hardware wallets, browser extensions):

```typescript
// EVM wallet (secp256k1)
await skill.authenticate({
  type: 'evm',
  address: '0xYourAddress',
  privateKey: '0xPrivateKey',
});

// Solana wallet (ed25519)
await skill.authenticate({
  type: 'solana',
  address: 'YourSolanaAddress',
  privateKey: 'base58EncodedPrivateKey',
});

// Custom signer (MetaMask / Phantom)
await skill.authenticate({
  type: 'evm',
  address: accounts[0],
  signer: async (message) =>
    window.ethereum.request({ method: 'personal_sign', params: [message, accounts[0]] }),
});
```

### Configuration

```typescript
const skill = new GdexSkill({
  apiUrl:     'https://trade-api.gemach.io/v1', // Backend (default)
  timeout:    30000,                          // Request timeout ms
  maxRetries: 3,                              // Retry attempts on 429/503
  debug:      false,                          // Log every request
});
```

The SDK does **not** read environment variables directly. If you choose to use env vars,
read them in your application (for example via `process.env`) and pass their values into
the `GdexSkill` constructor as shown above.

Recommended environment variables for your own app:

| Env Variable | Description | Default |
|---|---|---|
| `GDEX_API_URL` | Backend base URL to pass as `apiUrl` | `https://trade-api.gemach.io/v1` |
| `GDEX_API_KEY` | API key for AES encryption in managed-custody flow | — |
| `GDEX_TIMEOUT` | Request timeout (ms) to pass as `timeout` | `30000` |
| `GDEX_MAX_RETRIES` | Retry attempts to pass as `maxRetries` | `3` |
| `GDEX_DEBUG` | Enable debug logging to pass as `debug` | `false` |
| `GDEX_CONTROL_WALLET` | Control wallet address (userId) for managed custody | — |
| `GDEX_SESSION_PRIVATE` | Session private key (hex, 0x-prefixed) for managed custody | — |
| `GDEX_MANAGED_CHAIN_ID` | Chain ID for managed trades (622112261=Solana, 42161=Arbitrum for perps) | `622112261` |
| `CONFIRM_LIVE_TRADE` | Set to `YES` to submit real trades | — |

---

## 🔒 Managed-Custody Trading

All trading on GDEX goes through **server-side managed wallets**. Your control wallet (EVM or Solana) is only used to authenticate (sign-in) — actual on-chain execution is handled by GDEX backend trade workers.

### How It Works

1. **Generate a session keypair** — secp256k1 key used to sign trades after auth
2. **Sign-in** — control wallet signs a message, encrypted as `computedData` → POST `/v1/sign_in`
3. **Resolve user** — GET `/v1/user` with encrypted session key to see managed wallets
4. **Trade** — ABI-encode trade data, sign with session key, encrypt as `computedData` → POST `/v1/purchase_v2` or `/v1/sell_v2`
5. **Poll status** — GET `/v1/trade-status/:requestId` until completed/failed

### Encryption

All authenticated payloads use **AES-256-CBC** with a **deterministic** key/IV derived from the API key (no random IV):
- Key = first 32 bytes of `SHA256(apiKey)` hex
- IV = first 16 bytes of `SHA256(SHA256(apiKey))` hex
- Trade/sign-in payloads: `JSON.stringify({ userId, data, signature, apiKey })` → UTF-8 → encrypt → hex
- Session key (`/v1/user`): hex-decoded raw bytes → encrypt (not UTF-8 string)
- The API key is included inside the encrypted JSON payload (not just used for encryption)

> **WARNING:** Do NOT use random IVs or the `iv:ciphertext` format. The backend uses deterministic AES derived from the API key hash chain.

### Signatures

**Spot trade signatures** (purchase/sell) use **raw keccak256 + secp256k1** (no EIP-191 prefix):
- Message: `"<action>-<lowercaseUserId>-<dataHexWithout0x>"`
- Digest: `keccak256(utf8Bytes(message))`
- Output: `r(64 hex) + s(64 hex) + v(2 hex)` = 130 chars, no `0x` prefix
- **v = raw recovery parameter** (`00` or `01`), NOT EIP-155 (`1b`/`1c`)

**HL perp signatures** use the same algorithm but with HL-specific action prefixes (`hl_deposit`, `hl_withdraw`, `hl_create_order`, etc.) and different ABI schemas. See the [HL Managed-Custody Reference](#hl-managed-custody-reference) section.

**Sign-in signatures** use EIP-191 `personal_sign` with the control wallet — this is the ONLY operation that uses EIP-191. All post-sign-in operations use raw keccak256.

### Nonce Generation

Nonces are **client-generated** (not fetched from the server):
```typescript
const nonce = String(Math.floor(Date.now() / 1000) + Math.floor(Math.random() * 1000));
```

### Quick Example

```typescript
import {
  GdexSkill,
  GDEX_API_KEY_PRIMARY,
  generateGdexSessionKeyPair,
  buildGdexSignInMessage,
  buildGdexSignInComputedData,
  buildGdexManagedTradeComputedData,
  buildGdexUserSessionData,
} from '@gemachdao/gdex-skill';

const skill = new GdexSkill();
skill.loginWithApiKey(GDEX_API_KEY_PRIMARY);
const apiKey = GDEX_API_KEY_PRIMARY;

// 1. Session keypair
const { sessionPrivateKey, sessionKey } = generateGdexSessionKeyPair();

// 2. Sign-in (control wallet signs this message)
const userId = '0xYourControlWallet';
const nonce = String(Math.floor(Date.now() / 1000) + Math.floor(Math.random() * 1000));
const message = buildGdexSignInMessage(userId, nonce, sessionKey);
const signature = /* wallet.signMessage(message) */;

const signInPayload = buildGdexSignInComputedData({ apiKey, userId, sessionKey, nonce, signature });
await skill.signInWithComputedData({ computedData: signInPayload.computedData, chainId: 900 });

// 3. Trade
const trade = buildGdexManagedTradeComputedData({
  apiKey, action: 'purchase', userId,
  tokenAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
  amount: '100000',
  nonce: String(Math.floor(Date.now() / 1000) + Math.floor(Math.random() * 1000)),
  sessionPrivateKey,
});
const result = await skill.submitManagedPurchase({
  computedData: trade.computedData, chainId: 900, slippage: 1,
});

// 4. Poll
if (result.requestId) {
  const status = await skill.getManagedTradeStatus(result.requestId);
  console.log(status.status, status.hash);
}
```

### Managed-Custody Helpers

| Function | Purpose |
|---|---|
| `generateGdexSessionKeyPair()` | Generate secp256k1 session keypair |
| `buildGdexSignInMessage(userId, nonce, sessionKey)` | Build the sign-in message for wallet signing |
| `encodeGdexSignInData(sessionKey, nonce, refCode?)` | ABI-encode sign-in data (`['bytes','string','string']`) |
| `buildGdexSignInComputedData({...})` | Build encrypted sign-in payload |
| `buildGdexUserSessionData(sessionKey, apiKey)` | Encrypt session key (raw hex bytes) for `/v1/user` |
| `encodeGdexTradeData(tokenAddr, amount, nonce?)` | ABI-encode trade data (`['string','uint256','string']`) |
| `signGdexTradeMessageWithSessionKey(action, userId, data, privKey)` | Sign trade with session key (v = raw recoveryParam `00`/`01`) |
| `buildGdexManagedTradeComputedData({...})` | Build encrypted trade payload |
| `encodeLimitOrderData(action, params)` | ABI-encode limit order data (buy/sell/update schemas) |
| `signLimitOrderMessage(action, userId, data, privKey)` | Sign limit order with session key |
| `buildLimitOrderComputedData({...})` | Build encrypted limit order payload |
| `encodeCopyTradeData(action, params)` | ABI-encode copy trade data (create: 12 fields, update: 16 fields, chainId is `uint256`) |
| `signCopyTradeMessage(action, userId, data, privKey)` | Sign copy trade with session key |
| `buildCopyTradeComputedData({...})` | Build encrypted copy trade payload |
| `buildEncryptedGdexPayload({...})` | Encrypt JSON `{userId, data, signature}` for `computedData` |
| `encryptGdexComputedData(plaintext, apiKey)` | AES-256-CBC encrypt UTF-8 plaintext |
| `encryptGdexHexData(hexData, apiKey)` | AES-256-CBC encrypt raw hex-decoded bytes |
| `decryptGdexComputedData(cipherHex, apiKey)` | AES-256-CBC decrypt to UTF-8 plaintext |
| `deriveGdexAesMaterial(apiKey)` | Get raw AES key/IV from API key |

### Verify Managed Flow (offline)

```bash
npm run verify:managed
```

This generates all payloads (session keypair, sign-in, user lookup, trade), validates encrypt/decrypt roundtrips, and prints the full execution plan — without making any live API calls.

---

## 📚 API Reference

### Spot Trading

#### `buyToken(params)`

Buy a token on any supported chain.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `chain` | `string \| ChainId` | ✅ | Chain name or numeric ID |
| `tokenAddress` | `string` | ✅ | Token contract address |
| `amount` | `string` | ✅ | Native token input amount |
| `slippage` | `number` | | Max slippage % (default: 1) |
| `dex` | `string` | | Force specific DEX |
| `walletAddress` | `string` | | Override wallet address |
| `priorityFee` | `number` | | Solana priority fee (lamports) |

```typescript
const result = await skill.buyToken({
  chain: 'solana',
  tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
  amount: '0.1',
  slippage: 1,
});
// result.jobId, result.status, result.txHash, result.outputAmount
```

#### `sellToken(params)`

```typescript
// Sell absolute amount
await skill.sellToken({ chain: 8453, tokenAddress: '0x...', amount: '100', slippage: 0.5 });

// Sell 50% of holdings
await skill.sellToken({ chain: 'solana', tokenAddress: '...', amount: '50%' });
```

---

### Perpetual Futures (HyperLiquid)

> **Critical: HyperLiquid deposits/withdrawals/orders use a different crypto flow than spot trades.** See the [HL Managed-Custody Reference](#hl-managed-custody-reference) section below for exact specifications.

#### `openPerpPosition(params)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `coin` | `string` | | Asset symbol (e.g., `'BTC'`, `'ETH'`) |
| `side` | `'long' \| 'short'` | | Direction |
| `sizeUsd` | `string` | | Collateral in USD |
| `leverage` | `number` | `5` | 1–50× |
| `takeProfitPrice` | `string` | | Optional TP price |
| `stopLossPrice` | `string` | | Optional SL price |
| `marginMode` | `'cross' \| 'isolated'` | `'cross'` | Margin mode |

```typescript
const pos = await skill.openPerpPosition({
  coin: 'BTC', side: 'long', sizeUsd: '1000', leverage: 10,
  takeProfitPrice: '110000', stopLossPrice: '95000',
});
```

#### `closePerpPosition(params)`

```typescript
await skill.closePerpPosition({ coin: 'BTC' });                  // close 100%
await skill.closePerpPosition({ coin: 'ETH', closePercent: 50 }); // close 50%
```

#### Other perp methods

```typescript
await skill.setPerpLeverage({ coin: 'BTC', leverage: 20 });

// Update leverage via HL managed-custody (explicit session-key signing)
await skill.hlUpdateLeverage({
  coin: 'BTC',
  leverage: 40,
  isCross: true,    // true = cross margin, false = isolated (default: true)
  apiKey,
  walletAddress,
  sessionPrivateKey,
});

// Deposit USDC to HyperLiquid (human-readable amount, converted internally)
await skill.perpDeposit({ amount: '10' });   // deposit 10 USDC (minimum)
await skill.perpWithdraw({ amount: '5' });    // withdraw 5 USDC

const positions = await skill.getPerpPositions({ walletAddress: '0x...' });
// Each: { coin, side, size, entryPrice, markPrice, leverage, unrealizedPnl, liquidationPrice }
```

> **HL Deposit notes:** Amount is in human-readable USDC (e.g., `'10'` for 10 USDC). The SDK automatically converts to smallest unit (6 decimals). Minimum deposit is 10 USDC. Your managed wallet must have the deposit amount + 1% fee buffer in USDC on Arbitrum. After the on-chain tx confirms, HyperLiquid takes ~10 minutes to credit the deposit.

#### Direct HyperLiquid Execution (via `@gdexsdk/hyper-liquid-trader`)

For direct trading on HyperLiquid L1 without managed custody — use your own private key:

```typescript
// Cross-margin perpetual trade
const result = await skill.hlExecuteCrossPerp(
  process.env.PRIVATE_KEY!,
  { coin: 'BTC', isLong: true, price: '100000', positionSize: '0.001', leverage: 10 },
);

// Isolated-margin perpetual trade
const result2 = await skill.hlExecuteIsolatedPerp(
  process.env.PRIVATE_KEY!,
  { coin: 'ETH', isLong: false, price: '3000', positionSize: '1', leverage: 5 },
);

// Spot trade on HyperLiquid
const result3 = await skill.hlExecuteSpot(
  process.env.PRIVATE_KEY!,
  { coin: 'PURR', isBuy: true, price: '0.50', size: '100' },
);

// Cancel an order directly
await skill.hlDirectCancelOrder(process.env.PRIVATE_KEY!, 'BTC', orderId);

// Get all mid prices
const mids = await skill.getHlAllMids();

// Get trade history
const trades = await skill.getHlTradeHistory('0xYourWallet');

// Get a trader's leverage context (for copy trading)
const leverage = await skill.getHlTraderLeverageContext('0xTraderWallet', 'BTC');

// Create a standalone HyperLiquidTrading instance with custom WS URLs
const trader = await skill.createHlTrader(['wss://custom-ws.example.com']);
```

---

### Limit Orders

> **Endpoints: `limit_buy` / `limit_sell` / `update_order` / `orders` — NOT `orders/create` or `orders/cancel`.**

```typescript
// Limit buy — buy WIF when price drops to $0.50 on Solana
const buyResult = await skill.limitBuy({
  apiKey: GDEX_API_KEY_PRIMARY,
  userId: '0x53D029a671bd1CF61a2fB1F4F6e4bD830BFBb2eD',  // control wallet
  sessionPrivateKey: '<session-key-hex>',
  chainId: 622112261,           // Solana
  tokenAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm',
  amount: '10000000',           // lamports
  triggerPrice: '0.50',
  profitPercent: '50',          // optional: TP at 50% gain
  lossPercent: '25',            // optional: SL at 25% loss
});

// Limit sell — sell WIF when price reaches $999.99 (take-profit)
const sellResult = await skill.limitSell({
  apiKey: GDEX_API_KEY_PRIMARY,
  userId: '0x53D029a671bd1CF61a2fB1F4F6e4bD830BFBb2eD',
  sessionPrivateKey: '<session-key-hex>',
  chainId: 622112261,
  tokenAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm',
  amount: '100000',
  triggerPrice: '999.99',
});

// Delete/cancel an order
await skill.updateOrder({
  apiKey: GDEX_API_KEY_PRIMARY,
  userId: '0x53D029a671bd1CF61a2fB1F4F6e4bD830BFBb2eD',
  sessionPrivateKey: '<session-key-hex>',
  chainId: 622112261,
  orderId: '<64-char-hex-order-id>',
  isDelete: true,
});

// List active orders (uses session-key auth, not full computedData)
const { count, orders } = await skill.getLimitOrders({
  userId: '0x53D029a671bd1CF61a2fB1F4F6e4bD830BFBb2eD',
  data: encryptedSessionKey,  // from buildGdexUserSessionData()
  chainId: 622112261,
});
```

---

### Copy Trading

> **Solana-only for write operations.** Sign-in must use `chainId: 622112261`. The ABI encodes `chainId` as `uint256`.

#### Discovery (no auth)

```typescript
// Top 300 wallets by total PnL (cached 2 min)
const topWallets = await skill.getCopyTradeWallets();
// [{ address, totalPnl, receivedMinusSpent, spent, unrealizedValue, chainId }]

// Top 300 by net received
const customWallets = await skill.getCopyTradeCustomWallets();

// Hot new tokens from top wallets (cached 20s)
const gems = await skill.getCopyTradeGems();

// Supported DEXes for Solana
const dexes = await skill.getCopyTradeDexes(622112261);
// [{ dexName: 'pumpfun', dexNumber: 0, programId: '...' }, ...]
```

#### Read (session-key auth)

```typescript
import { buildGdexUserSessionData } from '@gemachdao/gdex-skill';

const data = buildGdexUserSessionData(sessionKey, apiKey);

// List copy trade configs (cached 20s per user)
const { allCopyTrades, dexes } = await skill.getCopyTradeList({ userId, data });
// allCopyTrades: [{ copyTradeId, copyTradeName, traderWallet, isActive, lossPercent, profitPercent, ... }]

// Transaction history with PnL
const { txes } = await skill.getCopyTradeTxList({ userId, data });
```

#### Create (computedData auth)

```typescript
import {
  GDEX_API_KEY_PRIMARY,
  buildCopyTradeComputedData,
  generateGdexSessionKeyPair,
  buildGdexSignInMessage,
  buildGdexSignInComputedData,
} from '@gemachdao/gdex-skill';

// Sign-in MUST use chainId: 622112261 for copy trade operations
const result = await skill.createCopyTrade({
  apiKey: GDEX_API_KEY_PRIMARY,
  userId: controlWalletAddress,
  sessionPrivateKey,
  chainId: 622112261,                // Solana only
  traderWallet: 'SolanaTraderAddress',
  copyTradeName: 'Alpha Trader',
  buyMode: 1,                        // 1 = fixed SOL, 2 = percentage
  copyBuyAmount: '0.001',            // SOL amount (mode 1) or percentage (mode 2)
  lossPercent: '50',                 // stop-loss at 50%
  profitPercent: '100',              // take-profit at 100%
  copySell: true,                    // also copy sell trades
  isBuyExistingToken: false,         // skip tokens already held
  excludedDexNumbers: [],            // no DEX exclusions
});
// { isSuccess: true, message: 'created new copy trade successfully', allCopyTrades: [...] }
```

#### Delete (computedData auth)

```typescript
// Delete a copy trade (isDelete: true)
const deleteResult = await skill.updateCopyTrade({
  apiKey: GDEX_API_KEY_PRIMARY,
  userId: controlWalletAddress,
  sessionPrivateKey,
  chainId: 622112261,
  copyTradeId: '<64-char-hex-id>',
  traderWallet: 'SolanaTraderAddress',
  copyTradeName: 'Alpha Trader',
  buyMode: 1,
  copyBuyAmount: '0.001',
  lossPercent: '50',
  profitPercent: '100',
  isDelete: true,                    // permanently deletes the copy trade
});
// { isSuccess: true, message: 'Updated' }
```

> **WARNING:** Both `isDelete: true` and `isChangeStatus: true` permanently delete the copy trade.
> There is no toggle/pause functionality on the current backend. Boolean fields use `''` for false
> and `'1'` for true internally; string `'0'` is truthy in JS and will trigger deletion.

#### Copy Trade ABI Reference

| Action | Fields | ABI Types |
|---|---|---|
| `create_copy_trade` | 12 | `['string','string','uint256','string'×9]` |
| `update_copy_trade` | 16 | `['string','string','uint256','string'×13]` |

**Create field order:** `[traderWallet, copyTradeName, chainId, gasPrice, buyMode, copyBuyAmount, isBuyExistingToken, lossPercent, profitPercent, nonce, copySell, excludedDexNumbers]`

**Update field order:** `[traderWallet, copyTradeName, chainId, gasPrice, buyMode, copyBuyAmount, isBuyExistingToken, lossPercent, profitPercent, nonce, copySell, excludedDexNumbers, copyTradeId, isDelete, isChangeStatus, excludedProgramIds]`

> `chainId` at position 2 is `uint256`, all other fields are `string`. Nonce is auto-generated by the SDK.

---

### HL Perp Copy Trading

> **Completely separate from Solana copy trading above.** This copies perpetual futures positions (long/short) on HyperLiquid. Sign-in uses `chainId: 1`. All ABI fields are strings.

#### Discovery (no auth)

```typescript
// Top traders by volume/tradeCount/deposit (cached 15 min)
const topByVolume = await skill.getHlTopTraders('volume');
const topByPnl = await skill.getHlTopTradersByPnl();

// Detailed trader stats (cached 1 hr)
// NOTE: Requires MANAGED wallet address, not control wallet
const stats = await skill.getHlUserStats('0xManagedWalletAddress');
// { userStats: { '24h', '7d', '30d', dailyPnls, volumes, tradesCount, allTime } }

// Market data
const dexes = await skill.getHlPerpDexes();
const assets = await skill.getHlAllAssets();
const tokens = await skill.getHlDepositTokens();

// Account state
const state = await skill.getHlClearinghouseState('0xAddress');
const orders = await skill.getHlOpenOrdersForCopy('0xAddress');
const balance = await skill.getHlUsdcBalanceForCopy('0xAddress');
```

#### Read (session-key auth)

```typescript
import { buildGdexUserSessionData } from '@gemachdao/gdex-skill';

const data = buildGdexUserSessionData(sessionKey, apiKey);

// List HL copy trade configs
const { allCopyTrades } = await skill.getHlCopyTradeList({ userId, data });
// [{ copyTradeId, copyTradeName, copyMode, traderWallet, isActive, oppositeCopy, totalPnl, ... }]

// Fill history (cached 15s, max 100 per page)
const { txes, totalCount } = await skill.getHlCopyTradeTxList({ userId, data, page: '1', limit: '20' });
// [{ coin, px, sz, side, dir, closedPnl, copyTradeName, traderWallet, ... }]
```

#### Create (computedData auth)

```typescript
await skill.createHlCopyTrade({
  apiKey,
  userId: controlWalletAddress,
  sessionPrivateKey,
  traderWallet: '0xTraderEvmAddress',
  copyTradeName: 'BTC Whale',
  copyMode: 1,                    // 1 = fixed USD, 2 = proportion
  fixedAmountCostPerOrder: '50',  // $50 per copied trade
  lossPercent: '25',              // mandatory, > 0 and < 100
  profitPercent: '100',           // mandatory, > 0
  oppositeCopy: false,            // true = copy opposite direction
});
```

#### Update / Delete (computedData auth)

```typescript
// Update parameters
await skill.updateHlCopyTrade({
  apiKey, userId: controlWalletAddress, sessionPrivateKey,
  copyTradeId: 'abc123...',
  traderWallet: '0xTraderEvmAddress',
  copyTradeName: 'Updated Name',
  copyMode: 2, fixedAmountCostPerOrder: '0.5',
  lossPercent: '30', profitPercent: '150',
  oppositeCopy: true,
});

// Delete permanently (use isDelete)
await skill.updateHlCopyTrade({ ...existingParams, isDelete: true });

// WARNING: isChangeStatus also PERMANENTLY DELETES (does NOT toggle)
await skill.updateHlCopyTrade({ ...existingParams, isChangeStatus: true });
```

#### HL Perp Copy Trade ABI Reference

| Action | Fields | ABI Types |
|---|---|---|
| `hl_create` | 8 | `['string' × 8]` |
| `hl_update` | 11 | `['string' × 11]` |

**Create field order:** `[traderWallet, copyTradeName, copyMode, fixedAmountCostPerOrder, lossPercent, profitPercent, nonce, oppositeCopy]`

**Update field order:** `[traderWallet, copyTradeName, copyMode, fixedAmountCostPerOrder, lossPercent, profitPercent, nonce, isDelete, isChangeStatus, copyTradeId, oppositeCopy]`

> All fields are `string`. Boolean fields: `'1'` = true, `''` = false. Nonce auto-generated by SDK.
> **Note:** `copyMode` and `oppositeCopy` in responses contain ABI byte-offsets (e.g., 416), not actual values. Both `isDelete` and `isChangeStatus` permanently delete the trade.

---

### Portfolio

```typescript
// Full cross-chain portfolio
const portfolio = await skill.getPortfolio({ walletAddress: '0x...' });
// { totalValueUsd, balances, perpPositions?, realizedPnl, unrealizedPnl }

// Chain-specific balances
const balances = await skill.getBalances({ walletAddress: '0x...', chain: ChainId.ETHEREUM });

// Paginated trade history
const history = await skill.getTradeHistory({
  walletAddress: '0x...', page: 1, limit: 20,
  startTime: 1700000000, endTime: 1700086400,
});
```

---

### Token Information

> 🔓 No authentication required for these endpoints.

```typescript
// Token details
const token = await skill.getTokenDetails({
  tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
  chain: 'solana',
});
// { symbol, name, priceUsd, priceChange24h, marketCap, fdv, volume24h, liquidity }

// Trending tokens
const trending = await skill.getTrendingTokens({
  chain: 'solana',
  period: '24h',     // '1h' | '6h' | '24h' | '7d'
  limit: 20,
  minLiquidity: 50000,
});

// OHLCV candles
const ohlcv = await skill.getOHLCV({
  tokenAddress: 'So11111111111111111111111111111111111111112',
  chain: 'solana',
  resolution: '60',  // '1'|'5'|'15'|'30'|'60'|'240'|'D'|'W'
  from: Math.floor(Date.now() / 1000) - 86400,
  to: Math.floor(Date.now() / 1000),
});
```

---

### Top Traders

> 🔓 No authentication required.

```typescript
const traders = await skill.getTopTraders({
  chain: 'solana',
  period: '7d',    // '1d' | '7d' | '30d' | 'all'
  limit: 10,
  sortBy: 'pnl',   // 'pnl' | 'winRate' | 'volume' | 'tradeCount'
});
// [{ address, totalPnlUsd, winRate, tradeCount, totalVolumeUsd }]
```

---

### Bridge

```typescript
// Get quote first
const quote = await skill.getBridgeQuote({
  fromChain: 'solana',
  toChain: ChainId.ETHEREUM,
  tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
  amount: '100',
});
console.log('Output:', quote.outputAmount, '— fee (USD):', quote.feeUsd);

// Execute bridge
const result = await skill.bridge({
  fromChain: 'solana',
  toChain: ChainId.ETHEREUM,
  tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
  amount: '100',
  destinationAddress: '0xYourEthAddress',
  slippage: 0.5,
});
```

---

### Wallet Info

```typescript
const info = await skill.getWalletInfo({ walletAddress: '...', chain: 'solana' });
// { address, nativeBalance, nativeSymbol, totalValueUsd, tokenCount }
```

---

### Wallet Generation

> 🔓 No authentication required. Keys are generated **locally** and never transmitted.

When a user doesn't have a wallet yet, generate an EVM **control wallet** for them. Then use the managed-custody sign-in flow to authenticate, and the Gbot backend automatically provisions a full trading wallet — including a Solana address and other chain-specific keys — server-side. No separate Solana wallet generation is needed.

#### `generateEvmWallet()` — your control wallet

Generates a new EVM-compatible wallet (Ethereum, Base, Arbitrum, BSC, etc.) using `ethers.Wallet.createRandom()`.

```typescript
import {
  GdexSkill,
  generateEvmWallet,
  generateGdexSessionKeyPair,
  buildGdexSignInMessage,
  GDEX_API_KEY_PRIMARY,
} from '@gemachdao/gdex-skill';

// Step 1: generate your EVM control wallet (one-time setup)
const wallet = generateEvmWallet();
console.log(wallet.address);  // '0xAbCd...' (checksummed) — safe to share
// ⚠️  Store wallet.privateKey and wallet.mnemonic securely

// Step 2: generate a session keypair for managed-custody trading
const { sessionPrivateKey, sessionKey } = generateGdexSessionKeyPair();

// Step 3: build the sign-in message and sign with your control wallet
const message = buildGdexSignInMessage(wallet.address, String(Date.now()), sessionKey);
// Sign with: new ethers.Wallet(wallet.privateKey).signMessage(message)

// Step 4: submit sign_in computedData (see Managed-Custody Trading section above)
// The backend provisions Solana + all other trading wallets server-side
```

---

## 🌐 Supported Chains

| Chain | ChainId | Native Token | DEXes |
|---|---|---|---|
| **Ethereum** | `1` | ETH | Uniswap V2/V3, Odos |
| **Optimism** | `10` | ETH | Uniswap V3, Odos |
| **BNB Smart Chain** | `56` | BNB | PancakeSwap, Odos |
| **Sonic** | `146` | S | — |
| **Fraxtal** | `252` | frxETH | Uniswap V3 |
| **Nibiru** | `6900` | NIBI | — |
| **Base** | `8453` | ETH | Uniswap V3, Odos, Arcadia |
| **Arbitrum One** | `42161` | ETH | Uniswap V3, Odos |
| **Berachain** | `80094` | BERA | — |
| **Solana** | `622112261` | SOL | Raydium, Raydium V2, Orca |
| **Sui** | `1313131213` | SUI | Cetus, Bluefin |
| **HyperLiquid** | perps only | USDC | Native perp engine |

> **The `ChainId` enum is wider than this table.** It also defines Avalanche (43114), Polygon
> (137), zkSync Era (324), Linea (59144), Blast (81457) and Scroll (534352). The backend's
> `supportedChainIds` does **not** include them, so calls against those ids will not route. Trade
> only the chains listed above.

```typescript
import { ChainId } from '@gemachdao/gdex-skill';

ChainId.ETHEREUM   // 1
ChainId.OPTIMISM   // 10
ChainId.BSC        // 56
ChainId.SONIC      // 146
ChainId.FRAXTAL    // 252
ChainId.NIBIRU     // 6900
ChainId.BASE       // 8453
ChainId.ARBITRUM   // 42161
ChainId.BERACHAIN  // 80094
ChainId.SOLANA     // 622112261
ChainId.SUI        // 1313131213
```

---

## ⚠️ Error Handling

```typescript
import {
  GdexAuthError,       // 401/403 — re-authenticate
  GdexValidationError, // invalid input params
  GdexApiError,        // 4xx/5xx backend errors
  GdexNetworkError,    // connection failures, timeouts
  GdexRateLimitError,  // 429 — check err.retryAfter
} from '@gemachdao/gdex-skill';

try {
  await skill.buyToken({ ... });
} catch (err) {
  if (err instanceof GdexRateLimitError) {
    console.log(`Rate limited — retry after ${err.retryAfter}s`);
    await new Promise(r => setTimeout(r, err.retryAfter * 1000));
    // retry…
  } else if (err instanceof GdexAuthError) {
    skill.loginWithApiKey(GDEX_API_KEY_PRIMARY); // re-auth
  } else if (err instanceof GdexValidationError) {
    console.error(`Bad param "${err.field}": ${err.message}`);
  } else if (err instanceof GdexApiError) {
    console.error(`API ${err.statusCode}: ${err.message}`);
  } else if (err instanceof GdexNetworkError) {
    console.error(`Network (${err.code}): ${err.message}`);
  }
}
```

| Class | When thrown |
|---|---|
| `GdexAuthError` | 401/403, invalid credentials |
| `GdexValidationError` | Invalid address, amount, chain, slippage |
| `GdexApiError` | Non-success HTTP (4xx/5xx) |
| `GdexNetworkError` | Connection refused, ECONNABORTED, timeout |
| `GdexRateLimitError` | HTTP 429 (has `.retryAfter` in seconds) |

---

## 🛠 Utility Functions

```typescript
import {
  getChainName,         // getChainName(8453)        → "Base"
  getNativeToken,       // getNativeToken('solana')   → "SOL"
  formatTokenAmount,    // formatTokenAmount('1000000', 6, 'USDC') → "1 USDC"
  formatUsd,            // formatUsd('1234.5')         → "$1,234.50"
  formatPercentChange,  // formatPercentChange('5.23') → "+5.23%"
  shortenAddress,       // shortenAddress('0x1234...') → "0x1234...5678"
  validateAddress,      // throws GdexValidationError if invalid
  validateAmount,       // throws GdexValidationError if invalid
  validateChain,        // throws GdexValidationError if unsupported
} from '@gemachdao/gdex-skill';
```

---

## 🧪 Testing

All 103 tests run with **mocked HTTP** — no real API key or network connection required:

```bash
npm test              # run all 103 tests
npm run test:coverage # with coverage report
npm run verify        # offline SDK smoke-test (20 checks)
npm run verify:managed # managed-custody payload validation (dry-run)
```

Test suites:
- `tests/client/auth.test.ts` — API key auth, EVM/Solana wallet signing
- `tests/actions/spotTrade.test.ts` — buy/sell, slippage, validation
- `tests/actions/perpTrade.test.ts` — open/close positions, leverage, deposits
- `tests/actions/portfolio.test.ts` — balances, history, wallet info
- `tests/actions/tokenInfo.test.ts` — trending, OHLCV, token details, top traders
- `tests/utils/walletGeneration.test.ts` — EVM control wallet generation (offline)
- `tests/utils/gdexManagedCrypto.test.ts` — managed-custody crypto helpers (AES, signing, ABI encoding)

---

## 🏗 Architecture

```
AI Agent (Claude Code / Cursor / Codex / ...)
   │
   │  npx skills add GemachDAO/gdex-skill
   │  ──────────────────────────────────
   │  SKILL.md → agent skill directory
   │
   ▼
@gemachdao/gdex-skill  (this package)
   │  TypeScript methods with full type safety
   │  @gdexsdk/hyper-liquid-trader for HyperLiquid L1 queries & direct execution
   │  Managed-custody: AES-256-CBC encryption + secp256k1 session signing
   │  computedData payloads for all trade operations
   │  Auto-retry with exponential backoff
   │
   │  Control Wallet (EVM / Solana)
   │  └─ signs once for /v1/sign_in → session keypair
   │
   ▼
Gbot Backend API  (https://trade-api.gemach.io/v1)
   │  Decrypt computedData → verify signature → resolve nonce (gRPC)
   │  NATS JetStream trade queue
   │  Server-side managed wallets (custody)
   │  DEX aggregation engine
   ▼
Blockchains  (Solana · Sui · Ethereum · Base · Arbitrum · …)
```

---

## HL Managed-Custody Reference

HyperLiquid perp operations use a **distinct crypto pipeline** from spot trades. Getting any detail wrong produces a `400 Unauthorized (code 103)` error. This section documents the exact specification.

### HL Deposit Flow

The backend custodially executes the on-chain deposit: it loads the user's server-side private key, constructs an Arbitrum transaction, and sends USDC to the HyperLiquid bridge receiver. The agent only provides an authorization signature.

```
Agent SDK                              Backend
────────                               ───────
1. ABI-encode deposit params     ──►   2. AES-decrypt computedData
   (uint64 chainId, address,           3. ABI-decode data
    uint256 amount, string nonce)       4. Verify chainId == 42161
2. Sign with session key          ──►  5. Verify signature vs stored sessionKey
3. AES-encrypt as computedData    ──►  6. Validate token, balance, min deposit
4. POST /v1/hl/deposit                 7. Execute ERC-20 transfer on Arbitrum
```

### HL ABI Schemas (CRITICAL)

| Action | ABI Types | Fields |
|---|---|---|
| `hl_deposit` | `['uint64', 'address', 'uint256', 'string']` | `[chainId, tokenAddress, amount, nonce]` |
| `hl_withdraw` | `['string', 'string']` | `[amount, nonce]` |
| `hl_create_order` | `['string', 'bool', 'string', 'string', 'bool', 'string', 'string', 'string', 'bool']` | `[coin, isLong, price, size, reduceOnly, nonce, tpPrice, slPrice, isMarket]` |
| `hl_place_order` | `['string', 'bool', 'string', 'string', 'bool', 'string']` | `[coin, isLong, price, size, reduceOnly, nonce]` |
| `hl_close_all` | `['string']` | `[nonce]` |
| `hl_cancel_order` | `['string', 'string', 'string']` | `[nonce, coin, orderId]` |
| `hl_cancel_all_orders` | `['string']` | `[nonce]` |
| `hl_update_leverage` | `['string', 'uint32', 'bool', 'string']` | `[coin, leverage, isCross, nonce]` |

> **WARNING:** The `hl_deposit` chainId uses `uint64`, NOT `uint256`. This is the single most common cause of "Unauthorized" errors. The backend re-encodes with `uint64` for signature verification — if you encode with `uint256`, the hex differs, signature recovery fails, and you get code 103.

### HL Signature Format

All HL write operations sign with the **session private key** (from sign-in), NOT the control wallet key:

```typescript
// Message format (no EIP-191 prefix):
const msg = `${action}-${userId.toLowerCase()}-${dataHex}`;
// e.g.: "hl_deposit-0x53d029a6...-00000000000000000000000000000000000000000000000000000000..."

const digest = keccak256(toUtf8Bytes(msg));
const sig = new SigningKey(sessionPrivateKey).sign(digest);
// Output: r(64hex) + s(64hex) + v(2hex) = 130 chars, no 0x prefix
// v = raw recovery parameter (00 or 01), NOT EIP-155 (1b/1c)
```

### HL Deposit Constraints

| Constraint | Value |
|---|---|
| Chain | Arbitrum only (chainId `42161`) |
| Token | USDC only (`0xaf88d065e77c8cC2239327C5EDb3A432268e5831`) |
| Amount | In smallest unit (6 decimals): 10 USDC = `10000000` |
| Min deposit | 10 USDC |
| Fee buffer | Balance must cover `amount × 1.01` |
| Delivery time | ~10 minutes after Arbitrum tx confirms |
| Bridge receiver | `0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7` |
| userId | Control wallet address (from sign-in), NOT managed wallet |

### HL Error Codes

| Code | Error | Cause |
|---|---|---|
| 101 | Missing params | `computedData` not in request body |
| 102 | Invalid chainId | chainId is not 42161 |
| 102 | Invalid params | Nonce already used, or token not supported |
| 103 | Unauthorized | Signature verification failed — check ABI types (`uint64`!), userId, and that you're signing with the session key registered during sign-in |
| — | Insufficient balance | Managed wallet doesn't have enough USDC + fee on Arbitrum |
| — | Too low amount | Amount < 10 USDC |

### HTTP Headers (Required)

The backend sits behind Cloudflare. Requests MUST include browser-like headers:

```typescript
{
  'Content-Type': 'application/json',
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
  'Accept': 'application/json',
  'Accept-Language': 'en-US,en;q=0.9',
  'Accept-Encoding': 'gzip, deflate, br',
  'Connection': 'keep-alive',
  'Authorization': 'Bearer <apiKey>',  // NOT X-API-Key
}
```

> **WARNING:** Using a non-browser User-Agent (e.g., `axios/1.x` or any string containing bot identifiers) will result in Cloudflare 403 "Access denied". The SDK handles this automatically.

### Complete HL Deposit Example

```typescript
import {
  GdexSkill,
  GDEX_API_KEY_PRIMARY,
  generateGdexSessionKeyPair,
  buildGdexSignInMessage,
  buildGdexSignInComputedData,
  buildHlComputedData,
} from '@gemachdao/gdex-skill';
import { ethers } from 'ethers';

const apiKey = GDEX_API_KEY_PRIMARY;
const skill = new GdexSkill();
skill.loginWithApiKey(apiKey);

// 1. Generate session keypair
const { sessionPrivateKey, sessionKey } = generateGdexSessionKeyPair();

// 2. Sign in (control wallet signs the Terms message)
const controlWallet = new ethers.Wallet('0xYourPrivateKey');
const nonce = String(Date.now());
const message = buildGdexSignInMessage(controlWallet.address, nonce, sessionKey);
const signature = await controlWallet.signMessage(message);

const signInPayload = buildGdexSignInComputedData({
  apiKey, userId: controlWallet.address, sessionKey, nonce,
  signature: signature.replace(/^0x/, ''),
});
await skill.signInWithComputedData({
  computedData: signInPayload.computedData, chainId: 42161,
});

// 3. Deposit USDC (amount in smallest unit, uint64 chainId is handled by SDK)
const computedData = buildHlComputedData({
  action: 'hl_deposit',
  apiKey,
  walletAddress: controlWallet.address,  // userId = control wallet
  sessionPrivateKey,
  actionParams: {
    chainId: 42161,
    tokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
    amount: '10000000',  // 10 USDC in smallest unit (6 decimals)
  },
});

const result = await skill.client.post('/v1/hl/deposit', { computedData });
// { hash: '0x...', isSuccess: true, amount: '10000000', message: 'Deposit successfully...' }
// Wait ~10 minutes for HyperLiquid to credit the deposit
```

---

## Contributing

1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Run `npm test && npm run build && npm run verify`
5. Submit a pull request

## License

MIT © [GemachDAO](https://github.com/GemachDAO)

TDQS

C2.6/5.0

Scored across 117 tools

Disambiguation2/5

117 tools contain many overlapping trading and account tools (e.g., open_perp_position/place_perp_order/execute_cross_perp/execute_isolated_perp; get_account_state/get_hl_clearinghouse_state/get_perp_positions; multiple auth and docs tools). While descriptions differentiate some, the sheer number of similar-purpose tools creates frequent ambiguity for an agent.

Naming Consistency2/5

Uses snake_case but inconsistently mixes prefixes and verb patterns: get_ vs no get_, hl_ vs get_hl_, managed_ vs execute_ vs direct_, build_ vs generate_. Some names are noun-first (trending_list, hl_outcomes) while most are verb_noun, making predictable tool discovery harder.

Tool Count1/5

117 tools is an extreme mismatch for any single MCP server; many tools cover niche or duplicate workflows. This exceeds the 50+ threshold and forces agents to sift through a sprawling surface.

Completeness4/5

The surface is broad, covering spot/perp trading, copy trading, bridge, wallet, auth, docs, social, and outcome markets. Most CRUD/lifecycle paths (create, update, cancel, list) exist, though some operations are only reachable via deletion flags or warning-laden tools.

Maintenance

ActivityMaintained
ResponsivenessUnresponsive