Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
63e238f
feat: add Hyperliquid bridge commands
kome12 Jun 9, 2026
c9f2a8a
feat: add Hyperliquid perp trading commands
kome12 Jun 9, 2026
b5d6946
fix(perp,bridge): validate trade inputs and prevent bridge quote replay
Jun 17, 2026
a6f15af
fix(perp,trade,bridge): validate inputs and reject cross-type/replaye…
Jun 17, 2026
f464251
fix(cli): assorted trading UX/safety fixes (perp meta, expiry, keycha…
Jun 17, 2026
59dc4ec
fix(test): perp is a trading command, not a research alias (ECINT-6803)
kome12 Jun 17, 2026
97e3304
fix(bridge): accept human amounts via --amount-unit, resolving per-ch…
Jun 17, 2026
2d6aa1f
fix(bridge): reject unknown --amount-unit instead of silent base-unit…
kome12 Jun 17, 2026
1b1f853
fix(perp): pre-validate leverage against the asset max (ECINT-6803 low)
kome12 Jun 17, 2026
6701fd2
fix(perp): tighten client-side input validation (ECINT-6803 follow-ups)
kome12 Jun 29, 2026
0a648a4
fix(perp): validate --slippage, --take-profit, --stop-loss client-side
kome12 Jun 29, 2026
5882f5c
fix(perp): ECINT-6823 CLI hardening follow-ups (6824/6826/6827/6828)
kome12 Jun 29, 2026
c027109
feat(perp): add `perp transfer` for Spot<->Perps (usdClassTransfer) (…
kome12 Jun 29, 2026
4b00d44
fix(bridge): treat USDC as $1 for --amount-unit usd
kome12 Jun 29, 2026
e57f222
fix(perp,bridge): validate slippage/precision client-side and report …
kome12 Jun 30, 2026
f649eea
feat(perp): client-side Hyperliquid action builder (hl-action.js)
kome12 Jul 23, 2026
3bd3909
feat(perp): direct-to-Hyperliquid /exchange submit (hl-client.js)
kome12 Jul 23, 2026
190aa2d
feat(perp): direct-to-HL submit with per-trade screening + builder fe…
kome12 Jul 24, 2026
cd7dfb3
fix(bridge): update API endpoints to /api/v1/perp/bridge/* (renamed i…
Jul 26, 2026
35a9f1e
Merge branch 'main' into feat/hl-perp-trading
kome12 Jul 27, 2026
54386c0
fix(bridge): consume the quote on the first broadcast, not the last
kome12 Jul 27, 2026
8ab8bb4
fix(bridge): screen before signing, and consume the quote at each bro…
kome12 Jul 27, 2026
0627a12
fix(bridge): screen the wallet that actually signs, and reject a quot…
kome12 Jul 27, 2026
7185359
fix(bridge): offer only the routes the CLI can complete
kome12 Jul 28, 2026
44805e2
fix(cli): render usage banners as text on a TTY; describe bridge in s…
kome12 Jul 28, 2026
b577954
fix(perp,bridge): bypass the cache on money paths; refuse degenerate …
kome12 Jul 28, 2026
19738dd
fix(evm): sign EIP-1559 transactions and stop discarding quoted fees
kome12 Jul 28, 2026
63646a8
test(evm): use a dummy key literal in the EIP-1559 test
kome12 Jul 28, 2026
57efabf
fix(perp,bridge): bound the builder fee, harden bridge inputs, derive…
kome12 Jul 29, 2026
0a182d3
fix(evm): stop hex-decoding the nonce twice; add a stuck-transaction …
kome12 Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bridge-execute-screening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"nansen-cli": patch
---

`bridge execute` now re-screens the wallet against the compliance blocklist immediately before signing, and fails closed if the check can't be completed — matching what the perp commands already did. Bridge quotes stay valid for an hour and the EVM deposit leg broadcasts straight to a public RPC, so previously nothing re-checked the wallet between the quote and the transaction that moves funds.

It also refuses to execute a quote with a wallet other than the one the quote was created for. Previously the signing wallet was resolved from `--wallet` or the current default independently of the quote, so a changed default (or an explicit `--wallet`) could sign with a different wallet than the one screened.
7 changes: 7 additions & 0 deletions .changeset/bridge-perp-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"nansen-cli": patch
---

`nansen schema` now describes the `bridge` command group — its three subcommands, their options, and the supported routes — so agents driving the CLI off the schema can discover it.

The mutating `perp` subcommands (`order`, `cancel`, `close`, `leverage`, `transfer`, `approve-builder-fee`) now declare `submitsTo: "https://api.hyperliquid.xyz/exchange"` in place of an `endpoint`, which is where they actually send a signed action, alongside an `apiEndpoints` list of the Nansen routes each one reads for compliance screening, market metadata and builder-fee status. Read-only subcommands keep their `endpoint` unchanged.
7 changes: 7 additions & 0 deletions .changeset/bridge-supported-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"nansen-cli": patch
---

`nansen bridge` supports `base -> hyperliquid` for deposits, and `hyperliquid -> base`, `hyperliquid -> ethereum`, `hyperliquid -> arbitrum` for withdrawals. Any other combination is rejected at quote time with the supported routes listed.

The route set is asymmetric because the two directions need different things from the client: a deposit broadcasts an EVM transaction and so needs a locally signable origin chain, while a withdrawal signs a Hyperliquid action and never touches the destination chain.
9 changes: 9 additions & 0 deletions .changeset/error-envelope-unification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"nansen-cli": minor
---

**Output shape change:** every command failure now serializes through the same error envelope — `{success: false, error, code, status, details}`. Previously a `CommandError` printed its structured payload at the top level instead, so errors from `trade`, `limit-order` and the API-key flows (`NOT_A_TTY`, `API_KEY_REQUIRED`, `INVALID_API_KEY`, `VERIFICATION_FAILED`) came back in a different shape from everything else.

Nothing is lost — the previous top-level payload is preserved verbatim under `details` — but anything parsing those errors positionally needs to read `details` instead of the root object. The new `perp` and `bridge` commands raise `CommandError` throughout, so without this they would have been the third distinct error shape in the CLI.

One deliberate exception: a missing-argument usage banner (`MISSING_PARAM`, `MISSING_ARGS`) prints as plain text when stdout is an interactive terminal and no output format was requested, because those messages are multi-line help written to be read and serializing them renders every newline as a literal `\n`. Piped output, and any run with `--pretty`, `--table`, `--format csv` or `--stream`, still gets the envelope — so nothing consuming the CLI programmatically sees a different shape.
11 changes: 11 additions & 0 deletions .changeset/evm-eip1559-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"nansen-cli": patch
---

EVM transactions are now signed as EIP-1559 (type 2) when the quote supplies fee caps, instead of being flattened into a legacy (type 0) transaction with a single gas price.

A legacy transaction pays exactly its `gasPrice`, so once the base fee rises above that value it is not merely slow — it can never be included at that nonce. A type-2 transaction pays base fee plus priority up to its cap, so it tolerates the fee moving between signing and inclusion. This affects `trade execute` and `bridge execute`, which share the signer.

`bridge execute` also stops discarding the fee fields the bridge quote provides. It previously overwrote them with a bare `eth_gasPrice` reading, producing a transaction priced at roughly the current base fee with almost no priority fee — which is what it takes to sit unmined on Base. Quoted fees are now kept, with the priority fee raised to a floor that Base will actually schedule and the cap lifted to cover both that and base-fee movement.

Two related robustness changes: signing now refuses outright when a quote carries no gas information at all, rather than falling back to a 1 wei gas price that produces a permanently unmineable transaction; and the receipt wait is longer, because by the time it runs the transaction is already broadcast, so giving up early reports a failure without undoing anything.
13 changes: 13 additions & 0 deletions .changeset/hl-action-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"nansen-cli": patch
---

Add a client-side Hyperliquid action builder (`src/hl-action.js`), the groundwork for submitting perp trades straight to Hyperliquid instead of round-tripping through the Nansen backend to build them.

- msgpack encoder that reproduces the reference `msgpack.packb` output byte-for-byte (insertion-ordered maps, smallest-width ints, utf-8 strings), so an action's `connectionId` hash matches the known-good path.
- `actionHash` + `l1Eip712` reproduce the phantom-agent EIP-712 payload (Exchange domain, mainnet `source: "a"`) that the existing signer already knows how to sign.
- Order-wire assembly for market/limit orders, cancels, closes and leverage updates, including TP/SL (`normalTpsl`) grouping and the builder-code attachment.
- Price/size rounding (`roundPrice`/`roundSize`) ported with Python-parity banker's rounding, so over-precise values that Hyperliquid would reject are rounded identically to the server path.
- `approveBuilderFee` and `usdClassTransfer` user-signed payload builders.

Pinned against the live prepare endpoints with golden-vector tests that assert both the built action and its `connectionId` match byte-for-byte.
7 changes: 7 additions & 0 deletions .changeset/hl-direct-submit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"nansen-cli": patch
---

Add `src/hl-client.js`, the single direct-to-Hyperliquid submission path: `submitExchange()` POSTs a signed action straight to `api.hyperliquid.xyz/exchange` from the user's machine instead of routing it through the Nansen backend. Reads and market-data stay on the proxy.

It reproduces the backend proxy's failure handling that it replaces — throwing on a top-level `status: "err"` and on a per-action error nested in `response.data.statuses[].error` (a rejected order that Hyperliquid otherwise reports under a top-level `"ok"`) — so a rejected order can never be mistaken for a fill. The submit is deliberately not retried, since each carries a unique nonce and is not idempotent. The HL base URL is overridable via `NANSEN_HL_API_URL` (for testnet / tests).
22 changes: 22 additions & 0 deletions .changeset/perp-bridge-input-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"nansen-cli": patch
---

Harden perp, swap, and bridge command safety:

- `perp order`/`close` now reject an invalid `--side` instead of silently opening the opposite direction, and `perp leverage` rejects an invalid `--margin-type` instead of silently switching to isolated.
- Perp numeric args (`--size`, `--price`, `--leverage`, `--oid`) are validated as positive numbers, with specific error messages instead of a generic usage banner.
- Perp commands now require an EVM wallet, with a clear error instead of querying for an `"undefined"` address.
- `trade quote` validates `--quote-index`, `--slippage`, and `--max-auto-slippage`, rejecting out-of-range values (e.g. a percent-vs-decimal slippage mix-up).
- `bridge quote` now validates `--slippage` client-side (whole basis points in `[0, 10000]`), rejecting non-numeric or out-of-range values with a clear message instead of forwarding them to an opaque backend 422 (matching how `perp` validates `--slippage`).
- `perp order`/`close` warn before signing when `--size` (or `--price` for `order`) is finer than the asset's Hyperliquid precision, which the exchange silently rounds.
- `perp order`/`close` now report the size and price the order actually executes at (post-rounding, slippage-adjusted for market orders) instead of echoing the raw input, so the printed values match the fill.
- `limit-order list` no longer aborts the whole render when one order has a non-integer amount.
- Quote loaders reject a cross-type quote (a bridge quote sent to `trade execute`, or a swap quote sent to `bridge execute`).
- `bridge execute` now refuses a quote that has already been executed, preventing an accidental double-bridge on retry.
- `bridge quote` accepts human amounts via `--amount-unit token|usd` (default stays base units), resolving token decimals per chain so the same `5` isn't 100x off between chains (USDC is 6 decimals on EVM, 8 on Hyperliquid). Hyperliquid USDC is floored to the bridge's 6-decimal precision to avoid a round-up-past-balance rejection.
- `perp meta` supports `--all` and `--filter <text>` so assets past the first 20 (e.g. HYPE) are listable.
- Deprecated top-level aliases now print a deprecation notice on stderr when run, not only in `--help`.
- `limit-order` rejects a zero-duration or past expiry instead of creating an order that expires immediately.
- A password with leading/trailing whitespace is no longer mangled when read back from the OS keychain.
- Nested backend error messages containing an apostrophe are no longer truncated.
12 changes: 12 additions & 0 deletions .changeset/perp-bridge-request-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"nansen-cli": patch
---

`perp` and `bridge` no longer serve any API response from the `--cache` store, and `bridge execute` no longer retries its submission.

- **Compliance screening** is always a live check. Every mutating command re-screens the signing wallet immediately before signing; with `--cache` that verdict could previously come from a cache written up to five minutes earlier, which is exactly the window the check exists to close.
- **`bridge execute`** sets `retry: false`. It proxies to Relay's `/authorize` and Hyperliquid's `/exchange`, neither of which is idempotent, so an automatic re-send on a 500 or 502 could submit the same signed action twice.
- **Bridge status polling** now observes progress under `--cache`. The cache key is endpoint plus body, so every poll for a given request id hit the same key and the loop would re-read one stale verdict for the whole TTL.
- **Perp reads** (`positions`, `orders`, `account`, `meta`) and **bridge quotes** bypass the cache too: they either report live balances to the user or feed a signing decision — `close` sizes its order from the positions read, and asset ids come from `meta`.

`bridge execute` also refuses to sign a step whose EIP-712 type definition is missing or whose `primaryType` matches no entry in it. With an empty field list the digest is still well-formed but commits to none of the action's contents, so it would have produced a valid-looking signature over nothing.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ nansen trade limit-order update --order <orderId> --trigger-price 85

For EVM chains, there's no native limit-order surface — pair an external venue's resting order with a `common-token-transfer` smart alert on the settlement wallet as a best-effort fill signal. See the `nansen-limit-orders` skill for details.

## Perpetuals

Hyperliquid perpetual trading via `nansen perp`. Uses the same wallet and `NANSEN_WALLET_PASSWORD` as DEX trading (requires an EVM wallet). Select the asset with `--coin` (`--symbol` is accepted as an alias).

```bash
nansen perp meta --filter ETH # list assets + max leverage
nansen perp order --coin ETH --side buy --size 0.1 --price 1600 --type limit
nansen perp order --coin BTC --side sell --size 0.001 --price 95000 --type market \
--take-profit 90000 --stop-loss 98000
nansen perp close --coin ETH --size 0.1 --price 1600 --side sell # sell closes a long
nansen perp cancel --coin ETH --oid <orderId>
nansen perp leverage --coin ETH --leverage 5 --margin-type cross # or isolated
nansen perp transfer --direction spot-to-perp --amount 25 # Spot<->Perps (or perp-to-spot)
nansen perp positions
nansen perp account # value, unrealized PnL, margin, spot USDC
```

`--side` is `buy`/`long` or `sell`/`short` to open (`buy`/`sell` to close); `--tif` is `Gtc`/`Ioc`/`Alo`; `--slippage` is a decimal in `[0,1]`; `--leverage` is a whole integer capped at the asset max. Perp orders are irreversible once signed. USDC sent to a wallet via Hyperliquid's **Send** lands in the **Spot** balance (shown as `Spot USDC`); move it to Perps with `perp transfer` before trading. See the `nansen-trading` skill for details.

## Wallet

```bash
Expand Down Expand Up @@ -195,7 +214,8 @@ nansen research smart-money netflow --chain solana --fields token_symbol,net_flo
|---------|-----|
| `command not found` | `npm install -g nansen-cli` |
| `UNAUTHORIZED` after login | `cat ~/.nansen/config.json` or set `NANSEN_API_KEY` |
| Empty perp results | Use `--symbol BTC`, not `--token`. Perps are Hyperliquid-only. |
| Empty perp _research_ results | Use `--symbol BTC`, not `--token`. Perps are Hyperliquid-only. |
| `perp` _trading_ prints the usage banner | Trading needs `--coin BTC` (`--symbol` also works); see the Perpetuals section. |
| `UNSUPPORTED_FILTER` on token holders | Remove `--smart-money` — not all tokens have that data. |
| Huge JSON response | Use `--fields` to select columns. |

Expand Down
50 changes: 49 additions & 1 deletion skills/nansen-trading/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: nansen-trading
description: Execute DEX swaps on Solana or Base, including cross-chain bridges. Use when buying or selling a token, getting a swap quote, or executing a trade.
description: Execute DEX swaps on Solana or Base (including cross-chain bridges) and Hyperliquid perpetual trades. Use when buying or selling a token, getting a swap quote, executing a trade, or opening/closing/managing a perp position.
metadata:
openclaw:
requires:
Expand Down Expand Up @@ -191,6 +191,54 @@ If the user says "$20 worth of X", use `--amount-unit usd` directly — no manua
- A wallet is required even for quotes (the API builds sender-specific transactions).
- ERC-20 swaps may require an approval step — execute handles this automatically.

# Perp Trading

Use `nansen perp` for Hyperliquid perpetual trading. Uses the same wallet and `NANSEN_WALLET_PASSWORD` as DEX trading; requires an **EVM** wallet. **Perp orders are irreversible once signed.**

Subcommands: `order`, `cancel`, `close`, `leverage`, `positions`, `orders`, `account`, `meta`.

The asset is selected with `--coin` (e.g. `BTC`, `ETH`); `--symbol` is accepted as an alias. List tradable assets and their max leverage with `nansen perp meta` (use `--filter <text>` or `--all` to see beyond the first 20).

## Open a position

```bash
# Limit long: 0.1 ETH at $1600
nansen perp order --coin ETH --side buy --size 0.1 --price 1600 --type limit

# Market short with optional take-profit / stop-loss
nansen perp order --coin BTC --side sell --size 0.001 --price 95000 --type market \
--take-profit 90000 --stop-loss 98000
```

- `--side`: `buy`/`long` to open a long, `sell`/`short` to open a short.
- `--size`: position size in base asset units (positive number).
- `--price`: limit price (or mark price for market orders).
- `--type`: `limit` (default) or `market`. `--tif`: `Gtc` (default), `Ioc`, `Alo`.
- `--slippage`: decimal in `[0,1]` for market orders (default `0.03` = 3%).

## Close / cancel

```bash
# Close: sell to close a long, buy to close a short (validated against your open position)
nansen perp close --coin ETH --size 0.1 --price 1600 --side sell

# Cancel a resting order by id
nansen perp cancel --coin ETH --oid 123456
```

## Leverage, transfers & account

```bash
nansen perp leverage --coin ETH --leverage 5 --margin-type cross # or isolated
nansen perp transfer --direction spot-to-perp --amount 25 # or perp-to-spot
nansen perp positions
nansen perp account # account value, unrealized PnL, margin used, withdrawable, spot USDC
```

`--leverage` must be a whole integer and is capped at the asset's maximum (see `perp meta`).

**Spot vs Perps:** perp trading draws from the **Perps** balance, but USDC sent to a wallet via Hyperliquid's **Send** lands in **Spot** (and shows as `Spot USDC` in `perp account`). Move it across with `perp transfer --direction spot-to-perp --amount <usdc>` before trading. (Deposits via the bridge land in Perps directly.)

## Source

- npm: https://www.npmjs.com/package/nansen-cli
Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2404,6 +2404,22 @@ describe('NansenAPI', () => {
await expect(api.smartMoneyNetflow({})).rejects.toThrow('Invalid API key');
});

it('extracts a nested error message containing an apostrophe (L7)', async () => {
if (LIVE_TEST) return;

const errorResponse = {
ok: false,
status: 400,
headers: new Map(),
json: async () => ({ detail: "{'message': 'Order can't be filled', 'code': 'X'}" })
};
errorResponse.headers.get = () => null;

mockFetch.mockResolvedValueOnce(errorResponse);

await expect(api.smartMoneyNetflow({})).rejects.toThrow("Order can't be filled");
});

it('should show login guidance for 401 when no API key', async () => {
if (LIVE_TEST) return;

Expand Down
Loading
Loading