Back to skills
SkillHub ClubShip Full StackFull Stack

xaut-trade

Buy or sell XAUT (Tether Gold) on Ethereum. Supports market orders (Uniswap V3) and limit orders (UniswapX). Wallet modes: Foundry keystore or WDK. Triggers: buy XAUT, XAUT trade, swap USDT for XAUT, sell XAUT, swap XAUT for USDT, limit order, limit buy XAUT, limit sell XAUT, check limit order, cancel limit order, XAUT when, create wallet, setup wallet.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,087
Hot score
99
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-aurehub-xaut-trade

Repository

openclaw/skills

Skill path: skills/aure-duncan/aurehub-xaut-trade

Buy or sell XAUT (Tether Gold) on Ethereum. Supports market orders (Uniswap V3) and limit orders (UniswapX). Wallet modes: Foundry keystore or WDK. Triggers: buy XAUT, XAUT trade, swap USDT for XAUT, sell XAUT, swap XAUT for USDT, limit order, limit buy XAUT, limit sell XAUT, check limit order, cancel limit order, XAUT when, create wallet, setup wallet.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install xaut-trade into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding xaut-trade to shared team environments
  • Use xaut-trade for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: xaut-trade
description: "Buy or sell XAUT (Tether Gold) on Ethereum. Supports market orders (Uniswap V3) and limit orders (UniswapX). Wallet modes: Foundry keystore or WDK. Triggers: buy XAUT, XAUT trade, swap USDT for XAUT, sell XAUT, swap XAUT for USDT, limit order, limit buy XAUT, limit sell XAUT, check limit order, cancel limit order, XAUT when, create wallet, setup wallet."
license: MIT
compatibility: "Requires Node.js >= 18, ~/.aurehub/ config directory, Ethereum RPC (HTTPS), and UniswapX API access. Reads/writes encrypted wallet vault and password files under ~/.aurehub/. Foundry (cast) required only for foundry wallet mode."
metadata:
  author: aurehub
  version: "2.1.1"
---

# xaut-trade

Execute `USDT -> XAUT` buy and `XAUT -> USDT` sell flows via Uniswap V3.

## When to Use

Use when the user wants to buy or sell XAUT (Tether Gold):
- **Buy**: USDT -> XAUT
- **Sell**: XAUT -> USDT

## External Communications

This skill connects to external services (Ethereum RPC, UniswapX API, and optionally xaue.com rankings). On first setup, it may install dependencies via npm. Inform the user before executing any external communication for the first time. See the README for a full list.

## Environment & Security Declaration

### Required config files (under `~/.aurehub/`)

| File | Purpose | Required |
|------|---------|----------|
| `.env` | Environment variables (WALLET_MODE, ETH_RPC_URL, password file paths) | Yes |
| `config.yaml` | Network and limit-order configuration (chain ID, contract addresses, UniswapX API URL) | Yes |
| `.wdk_vault` | Encrypted wallet vault (XSalsa20-Poly1305) | When WALLET_MODE=wdk |
| `.wdk_password` | Vault decryption password (file mode 0600) | When WALLET_MODE=wdk |

### Environment variables

| Variable | Purpose | Required |
|----------|---------|----------|
| `WALLET_MODE` | Wallet type: `wdk` (encrypted vault) or `foundry` (keystore) | Yes |
| `ETH_RPC_URL` | Ethereum JSON-RPC endpoint (HTTPS) | Yes |
| `WDK_PASSWORD_FILE` | Path to WDK vault password file (mode 0600) | When WALLET_MODE=wdk |
| `KEYSTORE_PASSWORD_FILE` | Path to Foundry keystore password file (mode 0600) | When WALLET_MODE=foundry |
| `UNISWAPX_API_KEY` | UniswapX API key for limit orders | When using limit orders |
| `ETH_RPC_URL_FALLBACK` | Optional fallback RPC endpoint | No |

### Network access

- **Ethereum JSON-RPC** (ETH_RPC_URL) — blockchain reads and transaction submission
- **UniswapX API** (HTTPS) — limit order nonce, submission, status, cancellation
- **xaue.com Rankings API** (HTTPS, opt-in) — leaderboard registration

### Shell commands

- `node scripts/*.js` — all trading operations run via Node.js subprocesses
- `cast` (foundry mode only) — keystore signing

### Security safeguards

- Runtime `PRIVATE_KEY` is explicitly rejected; only file-based wallet modes are supported
- Seed phrase export is TTY-gated and requires interactive confirmation
- Vault and password files enforce 0600 permissions
- Decrypted key material is zeroed from memory after use

## Environment Readiness Check (run first on every session)

**Before handling any user intent** (except knowledge queries), run these checks:

1. Does `~/.aurehub/.env` exist: `ls ~/.aurehub/.env`
   Fail -> redirect to the **Setup / Create Wallet Flow** below.
2. Read `WALLET_MODE` from .env: `source ~/.aurehub/.env && echo $WALLET_MODE`
   Fail (missing or empty) -> redirect to the **Setup / Create Wallet Flow** below. Do NOT auto-detect or infer the wallet mode from installed tools (e.g. do not assume Foundry mode just because `cast` is installed). The user must explicitly choose.
3. Does `~/.aurehub/config.yaml` exist: `ls ~/.aurehub/config.yaml`
   Fail -> copy from config.example.yaml (see onboarding Step C1) or redirect to setup.
4. **If `WALLET_MODE=wdk`:**
   - Check `~/.aurehub/.wdk_vault` exists: `ls ~/.aurehub/.wdk_vault`
   - Check `WDK_PASSWORD_FILE` in .env and file readable: `source ~/.aurehub/.env && test -r "$WDK_PASSWORD_FILE" && echo OK || echo FAIL`
   - Check Node.js >= 18: `node -v`
   - WDK mode has zero `cast` dependency
5. **If `WALLET_MODE=foundry`:**
   - Check `cast --version` available
   - Check keystore exists: `source ~/.aurehub/.env && ls ~/.foundry/keystores/$FOUNDRY_ACCOUNT`
     (Optional: `cast wallet list` can verify the account name appears in Foundry's keystore)
   - Check `KEYSTORE_PASSWORD_FILE` readable: `source ~/.aurehub/.env && test -r "$KEYSTORE_PASSWORD_FILE" && echo OK || echo FAIL`
   - Check Node.js >= 18: `node -v` (needed for market module)
6. **Both modes**: verify wallet loads by resolving `SCRIPTS_DIR` (see **Resolving SCRIPTS_DIR** below) and running:
   ```bash
   source ~/.aurehub/.env
   cd "$SCRIPTS_DIR"
   node swap.js address
   ```
   This outputs JSON: `{ "address": "0x..." }`. If it fails, the wallet is not configured correctly.

> **Important -- shell isolation**: Every Bash tool call runs in a new subprocess; variables set in one call do NOT persist to the next. Therefore **every Bash command block that needs env vars must begin with `source ~/.aurehub/.env`** (or `set -a; source ~/.aurehub/.env; set +a` to auto-export all variables).
>
> **WALLET_ADDRESS**: derive it from `node swap.js address` (works for both wallet modes):
> ```bash
> source ~/.aurehub/.env
> cd "$SCRIPTS_DIR"
> WALLET_ADDRESS=$(node swap.js address | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).address")
> ```
> Alternatively, `node swap.js balance` also includes the address in its output.

If **all pass**: source `~/.aurehub/.env`, then proceed to intent detection.

If **any fail**: do not continue with the original intent. Note which checks failed, then present the following to the user (fill in [original intent] with a one-sentence summary of what the user originally asked for):

**First, if `WALLET_MODE` is missing or empty** (check 2 failed), ask the user to choose before showing setup options:

---
Environment not ready ([specific failing items]).

First, choose your wallet mode:

> **[1] WDK (recommended)** — seed-phrase based, encrypted vault, no external tools needed
> **[2] Foundry** — requires Foundry installed, keystore-based

---

Default to WDK if the user just presses enter or says "recommended". Remember the choice for the next step.

**Skip this question if `WALLET_MODE` is already set** (other checks failed but wallet mode is known).

**Then, present the setup method options:**

---
Please choose how to set up:

  **[1] Recommended: let the Agent guide setup step by step**

  Agent-guided mode (default behavior):
  - The Agent runs all safe/non-sensitive checks and commands automatically
  - The Agent pauses only when manual input is required (interactive key import / password entry / wallet funding)
  - After each manual step, the Agent resumes automatically and continues original intent

  **[2] Fallback: run setup.sh manually**

  Before showing this option, silently resolve the setup.sh path (try in order, stop at first match):
  ```bash
  # 1. Saved path from previous run (validate it still exists)
  _saved=$(cat ~/.aurehub/.setup_path 2>/dev/null); [ -f "$_saved" ] && SETUP_PATH="$_saved"
  # 2. Git repo (fallback)
  [ -z "$SETUP_PATH" ] && { GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); [ -n "$GIT_ROOT" ] && [ -f "$GIT_ROOT/skills/xaut-trade/scripts/setup.sh" ] && SETUP_PATH="$GIT_ROOT/skills/xaut-trade/scripts/setup.sh"; }
  # 3. Bounded home search fallback
  [ -z "$SETUP_PATH" ] && SETUP_PATH=$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)
  echo "$SETUP_PATH"
  ```
  Then show the user only the resolved absolute path:
  ```bash
  bash /resolved/absolute/path/to/setup.sh
  ```

Once setup is done in option 2, continue original request ([original intent]).

---

Wait for the user's reply:
- User chooses **1** -> load [references/onboarding.md](references/onboarding.md) and follow the agent-guided steps, passing the already-chosen wallet mode (skip Step 0 if wallet mode was selected above)
- User chooses **2** or completes setup.sh and reports back -> re-run all environment checks; if all pass, continue original intent; if any still fail, report the specific item and show the options again

Proceed to intent detection.

**Resolving SCRIPTS_DIR** (used throughout this skill for running Node.js scripts):

Resolve `SCRIPTS_DIR` in this order:
- `dirname "$(cat ~/.aurehub/.setup_path 2>/dev/null)"` (if file exists)
- git fallback: `$(git rev-parse --show-toplevel 2>/dev/null)/skills/xaut-trade/scripts` (if valid)
- bounded home-search fallback: `dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)"`

All `node swap.js` commands assume CWD is `$SCRIPTS_DIR`.

**Extra checks for limit orders** (only when the intent is limit buy / sell / query / cancel):

7. Are limit order dependencies installed: `ls "$SCRIPTS_DIR/node_modules"`
   Fail -> run `cd "$SCRIPTS_DIR" && npm install`, then continue
8. Is `UNISWAPX_API_KEY` configured: `[ -n "$UNISWAPX_API_KEY" ] && [ "$UNISWAPX_API_KEY" != "your_api_key_here" ]`
   Fail -> **hard-stop**, output:
   > Limit orders require a UniswapX API Key.
   > How to get one (about 5 minutes, free):
   > 1. Visit https://developers.uniswap.org/dashboard
   > 2. Sign in with Google / GitHub
   > 3. Generate a Token (choose Free tier)
   > 4. Add the key to ~/.aurehub/.env: `UNISWAPX_API_KEY=your_key`
   > 5. Re-submit your request

## Config & Local Files

- Global config directory: `~/.aurehub/` (persists across sessions, not inside the skill directory)
- `.env` path: `~/.aurehub/.env`
- `config.yaml` path: `~/.aurehub/config.yaml`
- Contract addresses and defaults come from `skills/xaut-trade/config.example.yaml`; copy to `~/.aurehub/config.yaml` during onboarding
- Human operator runbook: [references/live-trading-runbook.md](references/live-trading-runbook.md)

## Interaction & Execution Principles (semi-automated)

1. Run pre-flight checks first, then quote.
2. Show a complete command preview before any on-chain write.
3. Trade execution confirmation follows USD thresholds:
   - `< risk.confirm_trade_usd`: show full preview, then execute without blocking confirmation
   - `>= risk.confirm_trade_usd` and `< risk.large_trade_usd`: single confirmation
   - `>= risk.large_trade_usd` or estimated slippage exceeds `risk.max_slippage_bps_warn`: double confirmation
4. Approval confirmation follows `risk.approve_confirmation_mode` (`always` / `first_only` / `never`, where `never` is high-risk) with a mandatory safety override:
   - If approve amount `> risk.approve_force_confirm_multiple * AMOUNT_IN`, require explicit approval confirmation.

## Mandatory Safety Gates

- When amount exceeds `risk.confirm_trade_usd`, require explicit execution confirmation
- When amount exceeds `risk.large_trade_usd`, require double confirmation
- When slippage exceeds the threshold (e.g. `risk.max_slippage_bps_warn`), warn and require double confirmation
- When approval amount is oversized (`> risk.approve_force_confirm_multiple * AMOUNT_IN`), force approval confirmation regardless of mode
- When ETH gas balance is insufficient, hard-stop and prompt to top up
- When the network or pair is unsupported, hard-stop
- When the pair is not in the whitelist (currently: USDT_XAUT / XAUT_USDT), hard-stop and reply "Only USDT/XAUT pairs are supported; [user's token] is not supported"

## RPC Fallback

After sourcing `~/.aurehub/.env`, parse `ETH_RPC_URL_FALLBACK` as a comma-separated list of fallback RPC URLs.

RPC failover is handled automatically by the FallbackProvider inside swap.js for **read operations** (balance, quote, allowance). When `ETH_RPC_URL` fails (429/502/503/timeout), the provider transparently retries with each URL in `ETH_RPC_URL_FALLBACK` in order, and promotes the successful URL as the new primary. **Write operations** (swap, approve, cancel-nonce) use the current primary URL at the time the signer is created; if a read operation has already promoted a fallback, the write will use that promoted URL. No agent action is needed for RPC switching.

If all RPCs fail, swap.js will exit with an error containing network-related messages. In that case, hard-stop with:
> RPC unavailable. All configured nodes failed (primary + fallbacks).
> To fix: add a paid RPC (Alchemy/Infura) at the front of `ETH_RPC_URL_FALLBACK` in `~/.aurehub/.env`

Do NOT treat non-network errors (insufficient balance, contract revert, invalid parameters, nonce mismatch) as RPC failures. Report these directly to the user.

## Intent Detection

Determine the operation from the user's message:

- **Buy**: contains "buy", "purchase", "swap USDT for", etc. -> run buy flow
- **Sell**: contains "sell", "swap XAUT for", etc. -> run sell flow
- **Insufficient info**: ask for direction and amount -- do not execute directly
- **Limit buy**: contains "limit order", "when price drops to", "when price reaches", and direction is buy -> run limit buy flow
- **Limit sell**: contains "limit sell", "sell when price reaches", "XAUT rises to X sell", etc. -> run limit sell flow
- **Query limit order**: contains "check order", "order status" -> run query flow
- **Cancel limit order**: contains "cancel order", "cancel limit" -> run cancel flow
- **Setup / Create wallet**: contains "setup", "create wallet", "initialize", "init wallet" -> skip environment readiness check, go to Setup / Create Wallet Flow below.
- **XAUT knowledge query**: contains "troy ounce", "grams", "conversion", "what is XAUT" -> answer directly, no on-chain operations or environment checks needed

## Setup / Create Wallet Flow

When the user explicitly requests setup or wallet creation:

### Step 1: Ask wallet mode

Present the choice:

> Which wallet mode would you like?
>
> **[1] WDK (recommended)** — seed-phrase based, encrypted vault, no external tools needed
> **[2] Foundry** — requires Foundry installed, keystore-based

Default to WDK if user just presses enter or says "recommended".

### Step 2: Check if wallet already exists for selected mode

**If user chose WDK:**
```bash
ls ~/.aurehub/.wdk_vault 2>/dev/null && echo "EXISTS" || echo "NOT_FOUND"
```
If EXISTS → inform user and stop:
> "WDK wallet already exists. No action needed. To use it, run a trade command (e.g. 'buy 100 USDT of XAUT')."

If the current `WALLET_MODE` in .env is different (e.g. `foundry`), update it to `wdk` and inform:
> "WDK wallet already exists. Switched wallet mode to WDK."

**If user chose Foundry:**
```bash
source ~/.aurehub/.env 2>/dev/null
ls ~/.foundry/keystores/${FOUNDRY_ACCOUNT:-aurehub-wallet} 2>/dev/null && echo "EXISTS" || echo "NOT_FOUND"
```
If EXISTS → inform user and stop:
> "Foundry keystore already exists. No action needed."

If the current `WALLET_MODE` in .env is different, update it to `foundry` and inform:
> "Foundry keystore already exists. Switched wallet mode to Foundry."

### Step 3: Create wallet (only if NOT_FOUND)

If the wallet does not exist for the selected mode, proceed with wallet creation:
- Load [references/onboarding.md](references/onboarding.md) and follow the setup steps for the selected mode
- After completion, update `WALLET_MODE` in `~/.aurehub/.env`

### Step 4: Security reminder (WDK mode only)

After WDK wallet creation succeeds, **always** display this security notice:

> **IMPORTANT: Back up your seed phrase**
>
> Your wallet is protected by an encrypted vault, but if the vault file or password is lost, **your funds cannot be recovered**.
>
> Export your 12-word seed phrase now and store it safely (paper or hardware backup — never in cloud storage or chat).
>
> Run this command in a **private** terminal:
> ```
> node <scripts_dir>/lib/export-seed.js
> ```
>
> Write down the 12 words and keep them offline. **Never share your seed phrase with anyone.**

Do NOT skip this step. Do NOT display the seed phrase in chat — only provide the export command for the user to run in their own terminal.

---

## Buy Flow (USDT -> XAUT)

### Step 1: Pre-flight Checks

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
BALANCE_JSON=$(node swap.js balance)
echo "$BALANCE_JSON"
```

The output is JSON: `{ "address": "0x...", "ETH": "0.05", "USDT": "1000.0", "XAUT": "0.5" }`

Parse and check:
- ETH balance: if below `risk.min_eth_for_gas`, hard-stop
- USDT balance: if insufficient for the buy amount, hard-stop and report the shortfall

### Step 2: Quote & Risk Warnings

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
QUOTE_JSON=$(node swap.js quote --side buy --amount <USDT_AMOUNT>)
echo "$QUOTE_JSON"
```

The output is JSON: `{ "side": "buy", "amountIn": "...", "amountOut": "...", "amountOutRaw": "...", "sqrtPriceX96": "...", "gasEstimate": "..." }`

Parse the JSON to extract:
- `amountOut`: estimated XAUT to receive (human-readable)
- `gasEstimate`: estimated gas cost
- Derive `minAmountOut` yourself: `amountOut * (1 - slippageBps / 10000)` using `risk.default_slippage_bps` from config.yaml
- Derive reference rate: `amountIn / amountOut` (both tokens have 6 decimals)

Display:
- Wallet address (from balance or address output)
- Input amount (human-readable)
- Estimated XAUT received
- Reference rate: `1 XAUT ~ X USDT`
- Slippage setting and `minAmountOut`
- Risk indicators (large trade / slippage / gas)

Determine confirmation level by USD notional and risk:
- `< risk.confirm_trade_usd`: show full preview, then execute without blocking confirmation
- `>= risk.confirm_trade_usd` and `< risk.large_trade_usd`: single confirmation
- `>= risk.large_trade_usd` or estimated slippage exceeds `risk.max_slippage_bps_warn`: double confirmation

### Step 3: Buy Execution

Follow [references/buy.md](references/buy.md):

**Allowance check:**

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
ALLOWANCE_JSON=$(node swap.js allowance --token USDT)
echo "$ALLOWANCE_JSON"
```

Output: `{ "address": "0x...", "token": "USDT", "allowance": "...", "spender": "0x..." }`

If allowance < amount needed, approve first.

**Approve (if needed):**

USDT requires reset-to-zero before approving (non-standard). The swap.js approve command handles this automatically:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
APPROVE_JSON=$(node swap.js approve --token USDT --amount <AMOUNT>)
echo "$APPROVE_JSON"
```

Output: `{ "address": "0x...", "token": "USDT", "amount": "...", "spender": "0x...", "txHash": "0x..." }`

**Swap execution:**

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
SWAP_JSON=$(node swap.js swap --side buy --amount <USDT_AMOUNT> --min-out <MIN_XAUT>)
echo "$SWAP_JSON"
```

Output: `{ "address": "0x...", "side": "buy", "amountIn": "...", "minAmountOut": "...", "txHash": "0x...", "status": "success", "gasUsed": "..." }`

- Before executing, remind the user: "About to execute an on-chain write"
- Execute with the confirmation level required by thresholds/policy
- Return tx hash and Etherscan link: `https://etherscan.io/tx/<txHash>`

**Swap error recovery (CRITICAL — see [references/buy.md](references/buy.md) Section 3a):**

If the swap command returns an error or `"status": "unconfirmed"`: **do NOT retry**. First check `node swap.js balance` and compare USDT balance against the pre-swap value. If USDT decreased, the swap succeeded — proceed to verification. Only retry if balance is unchanged.

**Result verification:**

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js balance
```

Return:
- tx hash
- post-trade XAUT balance
- on failure, return retry suggestions

## Sell Flow (XAUT -> USDT)

### Step 1: Pre-flight Checks

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
BALANCE_JSON=$(node swap.js balance)
echo "$BALANCE_JSON"
```

Parse and check:
- ETH balance: if below `risk.min_eth_for_gas`, hard-stop
- **XAUT balance check (required)**: hard-stop if insufficient for the sell amount

**Precision check**: if the input has more than 6 decimal places (e.g. `0.0000001`), hard-stop:
> XAUT supports a maximum of 6 decimal places. The minimum tradeable unit is 0.000001 XAUT. Please adjust the input amount.

### Step 2: Quote & Risk Warnings

Follow [references/sell.md](references/sell.md):

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
QUOTE_JSON=$(node swap.js quote --side sell --amount <XAUT_AMOUNT>)
echo "$QUOTE_JSON"
```

Output JSON: `{ "side": "sell", "amountIn": "...", "amountOut": "...", "amountOutRaw": "...", "sqrtPriceX96": "...", "gasEstimate": "..." }`

Parse and display:
- Wallet address (from balance or address output)
- Input amount (user-provided form)
- Estimated USDT received (`amountOut`)
- Reference rate: `1 XAUT ~ X USDT`
- Slippage setting and `minAmountOut`
- Risk indicators (large trade / slippage / gas)

**Large-trade check**: convert `amountOut` (USDT) to USD value; if it exceeds `risk.large_trade_usd`, require double confirmation.

### Step 3: Sell Execution

Follow [references/sell.md](references/sell.md):

**Allowance check:**

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
ALLOWANCE_JSON=$(node swap.js allowance --token XAUT)
echo "$ALLOWANCE_JSON"
```

Output: `{ "address": "0x...", "token": "XAUT", "allowance": "...", "spender": "0x..." }`

If allowance < amount needed, approve first.

**Approve (if needed):**

XAUT is standard ERC-20 -- **no prior reset needed**, approve directly:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
APPROVE_JSON=$(node swap.js approve --token XAUT --amount <AMOUNT>)
echo "$APPROVE_JSON"
```

Output: `{ "address": "0x...", "token": "XAUT", "amount": "...", "spender": "0x...", "txHash": "0x..." }`

**Swap execution:**

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
SWAP_JSON=$(node swap.js swap --side sell --amount <XAUT_AMOUNT> --min-out <MIN_USDT>)
echo "$SWAP_JSON"
```

Output: `{ "address": "0x...", "side": "sell", "amountIn": "...", "minAmountOut": "...", "txHash": "0x...", "status": "success", "gasUsed": "..." }`

- Before executing, remind the user: "About to execute an on-chain write"
- Execute with the confirmation level required by thresholds/policy
- Return tx hash and Etherscan link

**Swap error recovery (CRITICAL — see [references/sell.md](references/sell.md) Section 7a):**

If the swap command returns an error or `"status": "unconfirmed"`: **do NOT retry**. First check `node swap.js balance` and compare XAUT balance against the pre-swap value. If XAUT decreased, the swap succeeded — proceed to verification. Only retry if balance is unchanged.

**Result verification:**

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js balance
```

Return:
- tx hash
- post-trade USDT balance
- on failure, return retry suggestions (reduce sell amount / increase slippage tolerance / check nonce and gas)

## Post-Trade Registration

After **any** on-chain trade completes successfully (buy swap, sell swap, or limit order placed):

1. `source ~/.aurehub/.env`
2. Derive WALLET_ADDRESS:
   ```bash
   cd "$SCRIPTS_DIR"
   WALLET_ADDRESS=$(node swap.js address | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).address")
   ```
3. `REGISTERED=$(cat ~/.aurehub/.registered 2>/dev/null)`
4. If `"$REGISTERED"` starts with `"$WALLET_ADDRESS:"` -> already registered, silent skip
5. If `RANKINGS_OPT_IN` != `"true"`:
   - Check marker: `PROMPTED=$(cat ~/.aurehub/.rankings_prompted 2>/dev/null)`
   - If marker starts with `"$WALLET_ADDRESS:"` -> skip prompt
   - Otherwise ask once: "Join XAUT activity rankings now? (yes/no)"
     - If user says `no`: `echo "$WALLET_ADDRESS:declined" > ~/.aurehub/.rankings_prompted`; stop
     - If user says `yes`:
       - If `NICKNAME` is empty: ask user for nickname
       - Persist opt-in in `~/.aurehub/.env` (`RANKINGS_OPT_IN=true`, `NICKNAME=<value>`)
       - Re-source env to update in-memory variables: `source ~/.aurehub/.env`
6. If `RANKINGS_OPT_IN` == `"true"`:
   - If `NICKNAME` is empty: ask "You're opted in to XAUT activity rankings — what nickname would you like to appear as?", then persist to `~/.aurehub/.env` and re-source: `source ~/.aurehub/.env`
   - Register:
   ```bash
   NICKNAME_ESC=$(printf '%s' "$NICKNAME" | sed 's/\\/\\\\/g; s/"/\\"/g')
   REGISTER_RESP=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
     https://xaue.com/api/rankings/participants \
     -H 'Content-Type: application/json' \
     -d "{\"wallet_address\":\"$WALLET_ADDRESS\",\"nickname\":\"$NICKNAME_ESC\",\"source\":\"agent\"}")
   ```
   - HTTP 200 or 201: `echo "$WALLET_ADDRESS:$NICKNAME" > ~/.aurehub/.registered`; inform: "Registered with nickname: $NICKNAME"
   - Any other status: silent continue, do not write marker file

Only prompt once per wallet when rankings are not enabled yet.

## Limit Buy Flow (USDT -> XAUT via UniswapX)

Follow [references/limit-order-buy-place.md](references/limit-order-buy-place.md).

## Limit Sell Flow (XAUT -> USDT via UniswapX)

Follow [references/limit-order-sell-place.md](references/limit-order-sell-place.md).

## Limit Order Query Flow

Follow [references/limit-order-status.md](references/limit-order-status.md).

## Limit Order Cancel Flow

Follow [references/limit-order-cancel.md](references/limit-order-cancel.md).

## Output Format

Output must include:

- `Wallet`: wallet address (always show early in preview)
- `Stage`: `Preview` or `Ready to Execute`
- `Input`: token, amount, chain
- `Quote`: estimated XAUT amount, slippage setting, `minAmountOut`
- `Reference rate`: `1 XAUT ~ X USDT` (for comparison with spot price; shown for both buy and sell)
- `Risk warnings`: large trade / slippage / gas
- `Command`: the `node swap.js` command to be executed
- `Result`: tx hash, post-trade balance (after execution)

## Error Handling

- Missing prerequisite variable: prompt to add the variable to `.env` and stop
- RPC network error (429/502/timeout): trigger RPC fallback sequence (see RPC Fallback section)
- Insufficient balance: report minimum top-up amount and stop
- User has not confirmed: stay in Preview -- do not execute
- Transaction failed: return failure reason and retry suggestions (reduce amount / increase slippage tolerance / check nonce and gas)
- **Swap error or `"status": "unconfirmed"`**: **NEVER retry without first checking balance.** RPC errors can occur even when the transaction was successfully mined. Always compare current balance against pre-swap balance before deciding to retry. See buy.md Section 3a / sell.md Section 7a.

## XAUT Knowledge Base

- 1 XAUT = 1 troy ounce = 31.1035 grams
- Minimum precision: 0.000001 XAUT (on-chain minimum unit: 1, i.e. 10^-6)
- Conversion: X grams / 31.1035 = XAUT amount
- Examples: 1g ~ 0.032151 XAUT; 10g ~ 0.32151 XAUT
- Contract address (Ethereum mainnet): 0x68749665FF8D2d112Fa859AA293F07A622782F38

Answer knowledge queries directly using the data above -- no on-chain commands needed.

## First-Turn Contract (for testing)

1. When information is sufficient: give a structured preview first, then ask for execution confirmation.
2. When information is insufficient: clarify key details (token, amount, environment variables) -- do not claim a trade has been executed.

## About

This skill is developed by Duncan.Aure (Duncan), an AI Agent created by Aurelion, the world's first NASDAQ-listed Tether Gold (XAU₮) treasury company. Duncan executes on-chain financial actions through modular AI Agent Skills. Enables automated XAU₮ trading, cross-protocol DeFi execution, and programmable digital gold allocation.

## Stay Connected

For updates, new skills, and ecosystem developments:

- **X**: [Aure_duncan](https://x.com/Aure_duncan)
- **Telegram**: [@aure_duncanbot](https://t.me/aure_duncanbot)
- **Aurelion**: [aurelion.com](https://www.aurelion.com/)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/onboarding.md

```markdown
# Environment Initialization (Onboarding)

Run this on first use or when the environment is incomplete. Return to the original user intent after completion.

---

## Automated Setup (recommended)

Run the setup script — it handles all steps automatically and clearly marks the steps that require manual action:

```bash
_saved=$(cat ~/.aurehub/.setup_path 2>/dev/null); [ -f "$_saved" ] && SETUP_PATH="$_saved"
[ -z "$SETUP_PATH" ] && { GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); [ -n "$GIT_ROOT" ] && [ -f "$GIT_ROOT/skills/xaut-trade/scripts/setup.sh" ] && SETUP_PATH="$GIT_ROOT/skills/xaut-trade/scripts/setup.sh"; }
[ -z "$SETUP_PATH" ] && SETUP_PATH=$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)
[ -n "$SETUP_PATH" ] && [ -f "$SETUP_PATH" ] && bash "$SETUP_PATH"
```

If the script exits with an error, follow the manual steps below for the failed step only.

---

## Manual Steps (fallback)

### Step 0: Choose Wallet Mode

**Skip this step if wallet mode was already chosen** (e.g. selected during the environment readiness prompt before entering this flow). Use the previously chosen mode and proceed to the appropriate branch.

Otherwise, two wallet modes are available. Choose one:

**WDK (recommended)** — lightweight, no external tools:
- Encrypted vault stored at `~/.aurehub/.wdk_vault`
- Requires only Node.js >= 18
- See [wallet-modes.md](wallet-modes.md) for details

**Foundry** — uses Foundry keystore:
- Requires Foundry (`cast`) to be installed
- Standard Web3 keystore at `~/.foundry/keystores/`
- See [wallet-modes.md](wallet-modes.md) for details

Ask the user which mode they prefer. Default to WDK if they have no preference.

---

### WDK Branch (if user chose WDK)

#### Step W1: Check Node.js

```bash
node -v
# Must be >= 18. If not found or < 18: https://nodejs.org
```

#### Step W2: Prepare Password File

Check if `~/.aurehub/.wdk_password` exists and is non-empty:

```bash
mkdir -p ~/.aurehub
[ -s ~/.aurehub/.wdk_password ] && echo "ready" || echo "missing or empty"
```

If missing or empty, instruct the user to run in their terminal (password will not appear in chat):

```
Please run the following in your terminal (password input will be hidden):

bash -c 'read -rsp "WDK password (min 12 chars): " p </dev/tty; echo; printf "%s" "$p" > ~/.aurehub/.wdk_password; chmod 600 ~/.aurehub/.wdk_password; echo "✓ Password saved to ~/.aurehub/.wdk_password"'

Copy the entire line above, paste into your terminal, and press Enter.
Tell me when done.
```

Wait for user confirmation, then verify:

```bash
[ -s ~/.aurehub/.wdk_password ] && echo "ready" || echo "still empty"
```

If still empty, repeat the prompt.

#### Step W3: Create WDK Wallet

First check if a WDK vault already exists:

```bash
[ -f ~/.aurehub/.wdk_vault ] && echo "EXISTS" || echo "NOT_FOUND"
```

If `EXISTS`: the wallet is already created. Skip to Step W4. Inform the user:
> "WDK wallet already exists at ~/.aurehub/.wdk_vault. Skipping creation."

If `NOT_FOUND`: resolve scripts directory and create the wallet:

```bash
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SCRIPTS_DIR=$(dirname "$SETUP_PATH")
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -d "$GIT_ROOT/skills/xaut-trade/scripts" ]; then
  SCRIPTS_DIR="$GIT_ROOT/skills/xaut-trade/scripts"
else
  SCRIPTS_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")
fi
cd "$SCRIPTS_DIR" && npm install
node "$SCRIPTS_DIR/lib/create-wallet.js" --password-file ~/.aurehub/.wdk_password
```

This creates `~/.aurehub/.wdk_vault` with the encrypted seed.

#### Step W3b: Security reminder — back up your seed phrase

**IMPORTANT**: After wallet creation, always present this security notice to the user:

> **Back up your seed phrase now.**
>
> Your wallet is protected by an encrypted vault. If the vault file (`~/.aurehub/.wdk_vault`) or password file (`~/.aurehub/.wdk_password`) is lost or corrupted, **your funds cannot be recovered** without the seed phrase.
>
> Run this command in a **private** terminal to display your 12-word mnemonic:
>
> ```bash
> node <scripts_dir>/lib/export-seed.js
> ```
>
> - Write the 12 words on paper and store offline (safe, lockbox).
> - **Never** save the seed phrase in cloud storage, screenshots, or chat.
> - **Never** share it with anyone — no legitimate service will ask for it.

Do NOT print the seed phrase in the chat. Only provide the export command.

#### Step W4: Write .env for WDK

If `~/.aurehub/.env` does not exist, create it:

```bash
cat > ~/.aurehub/.env << 'EOF'
WALLET_MODE=wdk
ETH_RPC_URL=https://eth.llamarpc.com
ETH_RPC_URL_FALLBACK=https://eth.merkle.io,https://rpc.flashbots.net/fast,https://eth.drpc.org,https://ethereum.publicnode.com
WDK_PASSWORD_FILE=~/.aurehub/.wdk_password
EOF
chmod 600 ~/.aurehub/.env
```

If `~/.aurehub/.env` already exists (e.g. switching from Foundry), only update the wallet-related fields — do NOT overwrite the entire file (to preserve UNISWAPX_API_KEY, RANKINGS, etc.):

```bash
# Update or add each key, preserving other settings
for kv in "WALLET_MODE=wdk" "WDK_PASSWORD_FILE=~/.aurehub/.wdk_password"; do
  key="${kv%%=*}"
  grep -q "^${key}=" ~/.aurehub/.env && sed -i.bak "s|^${key}=.*|${kv}|" ~/.aurehub/.env || echo "$kv" >> ~/.aurehub/.env
done
rm -f ~/.aurehub/.env.bak
grep -q "^ETH_RPC_URL=" ~/.aurehub/.env || echo "ETH_RPC_URL=https://eth.llamarpc.com" >> ~/.aurehub/.env
```

> If the user has a paid RPC (e.g. Alchemy/Infura), replace `ETH_RPC_URL` or prepend it to `ETH_RPC_URL_FALLBACK` for automatic failover.

Now skip to **Step C1: Write config.yaml** below.

---

### Foundry Branch (if user chose Foundry)

#### Step F1: Install Foundry (if `cast` is unavailable)

```bash
curl -L https://foundry.paradigm.xyz | bash && \
  export PATH="$HOME/.foundry/bin:$PATH" && \
  foundryup
cast --version   # Expected output: cast Version: x.y.z
```

> After installation, open a new terminal or run `source ~/.zshrc` (zsh) / `source ~/.bashrc` (bash) so `cast` is available in future sessions.

Skip this step if `cast --version` succeeds.

#### Step F2: Prepare Password File

Check if `~/.aurehub/.wallet.password` exists and is non-empty:

```bash
mkdir -p ~/.aurehub
[ -s ~/.aurehub/.wallet.password ] && echo "ready" || echo "missing or empty"
```

If missing or empty, instruct the user to run in their terminal (password will not appear in chat):

```
Please run the following in your terminal (password input will be hidden):

bash -c 'read -rsp "Keystore password: " p </dev/tty; echo; printf "%s" "$p" > ~/.aurehub/.wallet.password; chmod 600 ~/.aurehub/.wallet.password; echo "✓ Password saved to ~/.aurehub/.wallet.password"'

Copy the entire line above, paste into your terminal, and press Enter.
Tell me when done.
```

Wait for user confirmation, then verify:

```bash
[ -s ~/.aurehub/.wallet.password ] && echo "ready" || echo "still empty"
```

If still empty, repeat the prompt.

#### Step F3: Wallet Setup

**Auto-detect**: if the keystore account already exists, skip this step.

```bash
# Use defaults here because ~/.aurehub/.env may not be created yet in manual flow.
FOUNDRY_ACCOUNT=${FOUNDRY_ACCOUNT:-aurehub-wallet}
KEYSTORE_PASSWORD_FILE=${KEYSTORE_PASSWORD_FILE:-~/.aurehub/.wallet.password}
cast wallet list 2>/dev/null | grep -qF "$FOUNDRY_ACCOUNT" && echo "exists" || echo "missing"
```

If missing, choose one method:

Import an existing private key into keystore:

```bash
cast wallet import "$FOUNDRY_ACCOUNT" --interactive
```

Or create a new wallet directly in keystore:

```bash
mkdir -p ~/.foundry/keystores
cast wallet new ~/.foundry/keystores "$FOUNDRY_ACCOUNT" \
  --password-file "$KEYSTORE_PASSWORD_FILE"
```

> Default values: `FOUNDRY_ACCOUNT=aurehub-wallet`, `KEYSTORE_PASSWORD_FILE=~/.aurehub/.wallet.password`

#### Step F4: Write .env for Foundry

If `~/.aurehub/.env` does not exist, create it:

```bash
cat > ~/.aurehub/.env << 'EOF'
WALLET_MODE=foundry
ETH_RPC_URL=https://eth.llamarpc.com
ETH_RPC_URL_FALLBACK=https://eth.merkle.io,https://rpc.flashbots.net/fast,https://eth.drpc.org,https://ethereum.publicnode.com
FOUNDRY_ACCOUNT=aurehub-wallet
KEYSTORE_PASSWORD_FILE=~/.aurehub/.wallet.password
EOF
chmod 600 ~/.aurehub/.env
```

If `~/.aurehub/.env` already exists (e.g. switching from WDK), only update the wallet-related fields:

```bash
for kv in "WALLET_MODE=foundry" "FOUNDRY_ACCOUNT=aurehub-wallet" "KEYSTORE_PASSWORD_FILE=~/.aurehub/.wallet.password"; do
  key="${kv%%=*}"
  grep -q "^${key}=" ~/.aurehub/.env && sed -i.bak "s|^${key}=.*|${kv}|" ~/.aurehub/.env || echo "$kv" >> ~/.aurehub/.env
done
rm -f ~/.aurehub/.env.bak
grep -q "^ETH_RPC_URL=" ~/.aurehub/.env || echo "ETH_RPC_URL=https://eth.llamarpc.com" >> ~/.aurehub/.env
```

> If the user has a paid RPC (e.g. Alchemy/Infura), replace `ETH_RPC_URL` or prepend it to `ETH_RPC_URL_FALLBACK` for automatic failover.

Now continue to **Step C1: Write config.yaml** below.

---

### Common Steps (both modes converge here)

#### Step C1: Write config.yaml

Copy contract config (defaults are ready to use — no user edits needed):

```bash
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SKILL_DIR=$(cd "$(dirname "$SETUP_PATH")/.." && pwd)
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$GIT_ROOT/skills/xaut-trade/config.example.yaml" ]; then
  SKILL_DIR="$GIT_ROOT/skills/xaut-trade"
else
  SKILL_DIR=$(cd "$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")/.." && pwd)
fi
cp "$SKILL_DIR/config.example.yaml" ~/.aurehub/config.yaml
```

`WALLET_MODE` is already set in `~/.aurehub/.env` (written in Step W4 or F4). No config.yaml change needed.

#### Step C2: Install Node.js dependencies

```bash
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SCRIPTS_DIR=$(dirname "$SETUP_PATH")
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -d "$GIT_ROOT/skills/xaut-trade/scripts" ]; then
  SCRIPTS_DIR="$GIT_ROOT/skills/xaut-trade/scripts"
else
  SCRIPTS_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")
fi
cd "$SCRIPTS_DIR" && npm install

# Save scripts path for future sessions
printf '%s/setup.sh\n' "$SCRIPTS_DIR" > ~/.aurehub/.setup_path
```

#### Step C3: Verify

```bash
source ~/.aurehub/.env
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SCRIPTS_DIR=$(dirname "$SETUP_PATH")
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -d "$GIT_ROOT/skills/xaut-trade/scripts" ]; then
  SCRIPTS_DIR="$GIT_ROOT/skills/xaut-trade/scripts"
else
  SCRIPTS_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")
fi
cd "$SCRIPTS_DIR" && node swap.js address
```

Expected output: `{ "address": "0x..." }`

If successful, inform the user:

```
Environment initialized. Wallet address: <address from output>
Make sure the wallet holds a small amount of ETH (>= 0.005) for gas.
```

---

## Runtime Dependencies (required for market and limit orders)

### 1. Install Node.js and scripts dependencies (>= 18)

```bash
node --version   # If version < 18 or command not found: https://nodejs.org
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SCRIPTS_DIR=$(dirname "$SETUP_PATH")
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -d "$GIT_ROOT/skills/xaut-trade/scripts" ]; then
  SCRIPTS_DIR="$GIT_ROOT/skills/xaut-trade/scripts"
else
  SCRIPTS_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")
fi
cd "$SCRIPTS_DIR" && npm install
```

### 2. Get a UniswapX API Key (required for limit orders only)

Limit orders require a UniswapX API Key to submit and query orders.

How to obtain (about 5 minutes, free):
1. Visit https://developers.uniswap.org/dashboard
2. Sign in with Google / GitHub
3. Generate a Token (choose Free tier)

Add the key to `~/.aurehub/.env`:

```bash
echo 'UNISWAPX_API_KEY=your_key_here' >> ~/.aurehub/.env
```

Node.js and npm dependencies are required for both market and limit orders.
Only `UNISWAPX_API_KEY` is limit-order specific.

---

## Activity Rankings (optional)

To join the XAUT trade activity rankings, add the following to `~/.aurehub/.env`:

```bash
echo 'RANKINGS_OPT_IN=true' >> ~/.aurehub/.env
echo 'NICKNAME=YourName' >> ~/.aurehub/.env
```

This shares your wallet address and nickname with https://xaue.com after your first trade. You can disable it anytime by setting `RANKINGS_OPT_IN=false`.

If you do not add these lines, no data is sent — rankings are opt-in only.

```

### references/live-trading-runbook.md

```markdown
# Live Trading Runbook (Chat-first, Mainnet)

Use this runbook when you want a real on-chain trade flow with minimal manual work.

Core principle:
- The Agent drives the workflow end-to-end.
- You only intervene at mandatory checkpoints.

---

## 1) What You Need to Do vs What the Agent Does

Agent (automatic):
- Environment checks (wallet mode, `.env`, password file, RPC reachability via `node swap.js address`)
- Balance check via `node swap.js balance`
- Quote via `node swap.js quote --side <buy|sell> --amount <N>`
- Risk preview and command preparation
- Approve via `node swap.js approve --token <TOKEN> --amount <N>`
- Swap via `node swap.js swap --side <buy|sell> --amount <N> --min-out <M>`
- Post-trade result summary via `node swap.js balance`

User (manual checkpoints only):
1. Sensitive wallet input (interactive key import / password input during setup)
2. Wallet funding (ETH for gas, USDT/XAUT as needed)
3. Confirmation when required by thresholds/policy

---

## 2) Start the Live Flow

In chat, send your intent directly:

```text
buy 10 USDT worth of XAUT
```

or

```text
sell 0.001 XAUT to USDT
```

The Agent will:
1. Run readiness checks
2. If environment is incomplete, switch to agent-guided setup
3. Return a Preview (quote, risk warnings, full command)
4. Apply confirmation policy:
   - small trades: light/optional confirmation
   - medium trades: single confirmation
   - large/high-risk trades: double confirmation

---

## 3) Mandatory Manual Checkpoints

### Checkpoint A: Wallet Sensitive Input

If wallet is missing or locked, the Agent will pause and ask you to complete interactive wallet steps.

Typical examples:
- Setting a password for WDK or Foundry wallet
- Running `node lib/create-wallet.js` (WDK mode)
- `cast wallet import <account> --interactive` (Foundry mode)

After you finish, tell the Agent to continue.

### Checkpoint B: Wallet Funding

Before live execution, ensure your wallet has:
- ETH for gas (recommended `>= 0.005 ETH`)
- USDT for buy flow
- XAUT for sell flow

### Checkpoint C: Final Execution Confirmation

When confirmation is required by policy, explicitly confirm:

```text
confirm approve
confirm swap
```

---

## 4) Expected Conversation Shape

1. You: trade intent
2. Agent: checks + preview
3. You: handle mandatory manual checkpoint(s) if prompted
4. Agent: updated preview / ready state
5. You: `confirm approve` / `confirm swap` when prompted
6. Agent: tx hash + post-trade balances/result

---

## 5) Optional: Limit Orders

For limit orders, add `UNISWAPX_API_KEY` to `~/.aurehub/.env` first.

Then ask in chat to:
- place limit buy/sell
- check order status
- cancel order

---

## 6) Common Failure Cases

1. RPC/network instability (`429/502/timeout`)
- Add a paid node to `ETH_RPC_URL` or put it first in `ETH_RPC_URL_FALLBACK`.

2. Password file mismatch
- Recreate the password file with correct password (`chmod 600`).

3. Insufficient balances
- Top up ETH/USDT/XAUT, then retry.

4. Runtime `PRIVATE_KEY` detected
- Remove `PRIVATE_KEY` from `.env`; runtime uses wallet mode (WDK or Foundry).

---

## 7) Fallback (Manual Script Path)

If agent-guided setup is blocked by local terminal constraints, run setup manually:

```bash
_saved=$(cat ~/.aurehub/.setup_path 2>/dev/null); [ -f "$_saved" ] && SETUP_PATH="$_saved"
[ -z "$SETUP_PATH" ] && { GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); [ -n "$GIT_ROOT" ] && [ -f "$GIT_ROOT/skills/xaut-trade/scripts/setup.sh" ] && SETUP_PATH="$GIT_ROOT/skills/xaut-trade/scripts/setup.sh"; }
[ -z "$SETUP_PATH" ] && SETUP_PATH=$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)
if [ -n "$SETUP_PATH" ] && [ -f "$SETUP_PATH" ]; then
  bash "$SETUP_PATH"
else
  echo "setup.sh not found. Run:"
  echo '  find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1'
  exit 1
fi
```

Then return to chat and continue your original trade intent.

```

### references/buy.md

```markdown
# Buy Execution (USDT -> XAUT)

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with `source ~/.aurehub/.env` and `cd "$SCRIPTS_DIR"`.

## 0. Pre-execution Declaration

- Current stage must be `Ready to Execute`
- Quote and explicit user confirmation must already be complete
- Full command must be displayed before execution

## 1. Allowance Check

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js allowance --token USDT
```

Output:

```json
{
  "address": "0x...",
  "token": "USDT",
  "allowance": "0.0",
  "spender": "0x..."
}
```

If allowance < intended `AMOUNT_IN`, approve first.

## 2. Approve (USDT)

USDT requires a reset-to-zero before approving (handled internally by swap.js when `token_rules.USDT.requires_reset_approve: true` in config):

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
APPROVE_RESULT=$(node swap.js approve --token USDT --amount <AMOUNT_IN>)
echo "$APPROVE_RESULT"
```

Output:

```json
{
  "address": "0x...",
  "token": "USDT",
  "amount": "<AMOUNT_IN>",
  "spender": "0x...",
  "txHash": "0x..."
}
```

The reset-to-zero step is handled automatically — no separate call needed.

Report: `Approve tx: https://etherscan.io/tx/<txHash>`

## 3. Swap Execution

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
SWAP_RESULT=$(node swap.js swap --side buy --amount <AMOUNT_IN> --min-out <MIN_AMOUNT_OUT>)
echo "$SWAP_RESULT"
```

Output:

```json
{
  "address": "0x...",
  "side": "buy",
  "amountIn": "<AMOUNT_IN>",
  "minAmountOut": "<MIN_AMOUNT_OUT>",
  "txHash": "0x...",
  "status": "success",
  "gasUsed": "180000"
}
```

Report: `Swap tx: https://etherscan.io/tx/<txHash>`

> On failure (`"status": "failed"`), report retry suggestions (reduce trade amount / increase slippage tolerance / check nonce and gas).

## 3a. Swap Error Recovery

**CRITICAL**: If the swap command returns an error, exits with a non-zero code, or returns `"status": "unconfirmed"`:

1. **Do NOT retry immediately.** The transaction may have been broadcast and mined despite the RPC error.
2. Check the current balance:
   ```bash
   source ~/.aurehub/.env
   cd "$SCRIPTS_DIR"
   node swap.js balance
   ```
3. Compare the USDT balance against the pre-swap balance (from pre-flight Step 1):
   - **If USDT balance decreased by the trade amount** → the swap **succeeded**. Proceed to Result Verification (Step 4). Do NOT re-approve or re-swap.
   - **If USDT balance is unchanged** → the swap did **not** execute. Safe to retry from Step 1 (Allowance Check).
4. If a `txHash` was returned (including in the `"unconfirmed"` response), verify on Etherscan: `https://etherscan.io/tx/<txHash>`

> **Why this matters**: RPC nodes can return errors (e.g. 400, 504, "Unknown block") even when the transaction was successfully broadcast and mined. Retrying without checking balance will execute a **duplicate trade**, costing the user double.

## 4. Result Verification

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js balance
```

Return:
- tx hash
- post-trade XAUT balance (from balance output)
- on failure, return retry suggestions

## 5. Mandatory Rules

- Before every on-chain write (approve, swap), remind the user: "About to execute an on-chain write"
- Trade execution confirmation follows:
  - `< risk.confirm_trade_usd`: show full preview, then execute without blocking confirmation
  - `>= risk.confirm_trade_usd` and `< risk.large_trade_usd`: single confirmation
  - `>= risk.large_trade_usd` or estimated slippage exceeds `risk.max_slippage_bps_warn`: double confirmation
- Approval confirmation follows `risk.approve_confirmation_mode` with force override:
  - If approve amount `> risk.approve_force_confirm_multiple * AMOUNT_IN`, require explicit approval confirmation
- Accepted confirmation phrases: "confirm approve", "confirm swap"

```

### references/sell.md

```markdown
# Sell Execution (XAUT -> USDT)

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with `source ~/.aurehub/.env` and `cd "$SCRIPTS_DIR"`.

## 0. Pre-execution Declaration

- Current stage must be `Ready to Execute`
- Quote and explicit user confirmation must already be complete
- Full command must be displayed before execution

## 1. Input Validation

User provides XAUT amount (e.g. `0.01`).

**Precision check**: if the input has more than 6 decimal places (e.g. `0.0000001`), hard-stop:

> XAUT supports a maximum of 6 decimal places. The minimum tradeable unit is 0.000001 XAUT. Please adjust the input amount.

## 2. Quote

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
RESULT=$(node swap.js quote --side sell --amount <XAUT_AMOUNT>)
echo "$RESULT"
```

Output:

```json
{
  "side": "sell",
  "amountIn": "<XAUT_AMOUNT>",
  "amountOut": "30.5",
  "amountOutRaw": "30500000",
  "sqrtPriceX96": "...",
  "gasEstimate": "150000"
}
```

Extract values:

```bash
AMOUNT_OUT=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).amountOut")
AMOUNT_OUT_RAW=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).amountOutRaw")
```

Calculate `minAmountOut` using `risk.default_slippage_bps` from config.yaml:

```bash
DEFAULT_SLIPPAGE_BPS=$(node -e "const c=require('js-yaml').load(require('fs').readFileSync(require('os').homedir()+'/.aurehub/config.yaml','utf8')); console.log((c.risk||{}).default_slippage_bps||50)")
MIN_AMOUNT_OUT=$(node -p "Math.trunc($AMOUNT_OUT_RAW * (10000 - $DEFAULT_SLIPPAGE_BPS) / 10000)")
```

Reference rate (for Preview display; both tokens have 6 decimals so divide directly):

```
Reference rate = amountOut / amountIn  (USDT/XAUT, human-readable)
```

## 3. Preview Output

Must include at minimum:

- Input amount (user-provided form)
- Estimated USDT received (`amountOut`, human-readable)
- Reference rate: `1 XAUT ~ X USDT`
- Slippage setting and `minAmountOut`
- Risk indicators (large trade / slippage / gas)

**Large-trade check**: convert `amountOut` (USDT) to USD value; if it exceeds `risk.large_trade_usd`, require double confirmation.

## 4. Confirmation Gate

Trade execution confirmation follows the threshold-based policy (see Section 9 — Mandatory Rules):

- `< risk.confirm_trade_usd`: show full preview, then execute without blocking confirmation
- `>= risk.confirm_trade_usd` and `< risk.large_trade_usd`: single confirmation
- `>= risk.large_trade_usd` or estimated slippage exceeds `risk.max_slippage_bps_warn`: double confirmation

Accepted confirmation phrases: "confirm approve", "confirm swap"

## 5. Allowance Check

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js allowance --token XAUT
```

Output:

```json
{
  "address": "0x...",
  "token": "XAUT",
  "allowance": "0.0",
  "spender": "0x..."
}
```

If allowance < `AMOUNT_IN`, approve first.

## 6. Approve (XAUT is standard ERC-20)

**XAUT does not require a prior reset** — approve directly:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
APPROVE_RESULT=$(node swap.js approve --token XAUT --amount <XAUT_AMOUNT>)
echo "$APPROVE_RESULT"
```

Output:

```json
{
  "address": "0x...",
  "token": "XAUT",
  "amount": "<XAUT_AMOUNT>",
  "spender": "0x...",
  "txHash": "0x..."
}
```

Report: `Approve tx: https://etherscan.io/tx/<txHash>`

> Note: Unlike USDT, XAUT does not require `approve(0)` to reset before approving.

## 7. Swap Execution

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
SWAP_RESULT=$(node swap.js swap --side sell --amount <XAUT_AMOUNT> --min-out <MIN_AMOUNT_OUT>)
echo "$SWAP_RESULT"
```

Output:

```json
{
  "address": "0x...",
  "side": "sell",
  "amountIn": "<XAUT_AMOUNT>",
  "minAmountOut": "<MIN_AMOUNT_OUT>",
  "txHash": "0x...",
  "status": "success",
  "gasUsed": "180000"
}
```

Report: `Swap tx: https://etherscan.io/tx/<txHash>`

> On failure (`"status": "failed"`), report retry suggestions.

## 7a. Swap Error Recovery

**CRITICAL**: If the swap command returns an error, exits with a non-zero code, or returns `"status": "unconfirmed"`:

1. **Do NOT retry immediately.** The transaction may have been broadcast and mined despite the RPC error.
2. Check the current balance:
   ```bash
   source ~/.aurehub/.env
   cd "$SCRIPTS_DIR"
   node swap.js balance
   ```
3. Compare the XAUT balance against the pre-swap balance (from pre-flight):
   - **If XAUT balance decreased by the trade amount** → the swap **succeeded**. Proceed to Result Verification (Step 8). Do NOT re-approve or re-swap.
   - **If XAUT balance is unchanged** → the swap did **not** execute. Safe to retry from Step 5 (Allowance Check).
4. If a `txHash` was returned (including in the `"unconfirmed"` response), verify on Etherscan: `https://etherscan.io/tx/<txHash>`

> **Why this matters**: RPC nodes can return errors (e.g. 400, 504, "Unknown block") even when the transaction was successfully broadcast and mined. Retrying without checking balance will execute a **duplicate trade**, costing the user double.

## 8. Result Verification

Post-swap balance:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js balance
```

Return:
- tx hash
- Post-trade balances (XAUT, USDT, ETH)
- on failure, return retry suggestions (reduce sell amount / increase slippage tolerance / check nonce and gas)

## 9. Mandatory Rules

- Before every on-chain write (approve, swap), remind the user: "About to execute an on-chain write"
- Trade execution confirmation follows:
  - `< risk.confirm_trade_usd`: show full preview, then execute without blocking confirmation
  - `>= risk.confirm_trade_usd` and `< risk.large_trade_usd`: single confirmation
  - `>= risk.large_trade_usd` or estimated slippage exceeds `risk.max_slippage_bps_warn`: double confirmation
- Approval confirmation follows `risk.approve_confirmation_mode` with force override:
  - If approve amount `> risk.approve_force_confirm_multiple * AMOUNT_IN`, require explicit approval confirmation
- Hard-stop if input precision exceeds 6 decimal places

```

### references/limit-order-buy-place.md

```markdown
# Limit Order Placement (USDT → XAUT via UniswapX)

## 0. Pre-execution Declaration

- Current stage must be `Ready to Execute`
- Parameters must be confirmed and user must have explicitly confirmed
- Full command must be displayed before execution

## 1. Pre-flight Checks

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
```

> `--api-url` and `--chain-id` are omitted from `limit-order.js` calls — the script reads defaults from `limit_order.uniswapx_api` and `networks.ethereum_mainnet.chain_id` in `~/.aurehub/config.yaml`.

```bash
node --version     # If not found, hard-stop and prompt to install https://nodejs.org (Node is required for all script commands)
node swap.js balance   # ETH balance check + tokenIn (USDT) balance check
```

## 2. Parameter Confirmation (Preview)

Display at minimum:
- Pair: USDT → XAUT
- Limit price: `1 XAUT = X USDT` (i.e. amountIn / minAmountOut, human-readable)
- Amount: `amountIn` USDT → at least `minAmountOut` XAUT
- Expiry: `expiry` seconds / deadline in local time
- UniswapX Filler risk notice: XAUT is a low-liquidity token; if no Filler fills the order, it expires automatically after the deadline with no loss of funds

## 3. Large-Trade Double Confirmation

If amountIn (USDT converted to USD) > `risk.large_trade_usd`, double confirmation is required.

## 4. Approve Permit2 (if allowance is insufficient)

Check USDT allowance for Permit2:

```bash
node swap.js allowance --token USDT --spender 0x000000000022D473030F116dDEE9F6B43aC78BA3
```

If insufficient, approve (USDT requires reset-to-zero; swap.js handles this automatically via token_rules):

```bash
node swap.js approve --token USDT --amount <AMOUNT_IN> --spender 0x000000000022D473030F116dDEE9F6B43aC78BA3
```

## 5. Place Order

> **Important — raw units**: Unlike `swap.js` (which accepts human-readable amounts), `limit-order.js` requires **raw integer amounts in smallest token units**.
> Both USDT and XAUT have 6 decimals, so multiply by `10^6`:
> - 1 USDT → `1000000`
> - 0.0005 XAUT → `500`
> - Formula: `raw = human_amount * 1000000` (drop any fractional remainder)

Resolve contract addresses and wallet before placing:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
USDT=$(node -e "const c=require('js-yaml').load(require('fs').readFileSync(require('os').homedir()+'/.aurehub/config.yaml','utf8')); console.log(c.tokens.USDT.address)")
XAUT=$(node -e "const c=require('js-yaml').load(require('fs').readFileSync(require('os').homedir()+'/.aurehub/config.yaml','utf8')); console.log(c.tokens.XAUT.address)")
WALLET_ADDRESS=$(node swap.js address | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).address")
# Convert human-readable amounts to raw integers (6 decimals): raw = human_amount * 1000000
# AMOUNT_IN: user's USDT spend amount; MIN_AMOUNT_OUT: minimum XAUT to receive (from limit price)
AMOUNT_IN=$(node -e "console.log(Math.trunc(parseFloat('<USDT_AMOUNT>') * 1e6))")
MIN_AMOUNT_OUT=$(node -e "console.log(Math.trunc(parseFloat('<MIN_XAUT_AMOUNT>') * 1e6))")
```

```bash
# EXPIRY_SECONDS: use the user-specified expiry, or fall back to 86400 (1 day).
RESULT=$(node limit-order.js place \
  --token-in       "$USDT" \
  --token-out      "$XAUT" \
  --amount-in      "$AMOUNT_IN" \
  --min-amount-out "$MIN_AMOUNT_OUT" \
  --expiry         "$EXPIRY_SECONDS" \
  --wallet         "$WALLET_ADDRESS")
```

Parse result:

```bash
ORDER_HASH=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).orderHash")
DEADLINE=$(echo "$RESULT"   | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).deadline")
NONCE=$(echo "$RESULT"      | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).nonce")
```

## 6. Output

Return to user:
- `orderHash`: for querying / cancelling the order
- `deadline`: order expiry in local time
- Note: order details (including nonce) have been auto-saved to `~/.aurehub/orders/`. Cancellation can be done via `--order-hash` without needing to record the nonce manually.
- Reminder: order has been submitted to UniswapX; the computer does not need to stay online — the Filler network fills automatically when the price is reached

## 7. Error Handling

| Error | Action |
|-------|--------|
| `node` not found | Hard-stop, prompt to install Node.js >= 18 (required for all script commands) |
| UniswapX API returns 4xx | Hard-stop, note XAUT may not be in the supported list, suggest market order |
| Limit price deviates > 50% from current market | Warn + double confirmation (prevent price typos) |
| Approve failed | Return failure reason, suggest retry |

```

### references/limit-order-sell-place.md

```markdown
# Limit Sell Order Placement (XAUT → USDT via UniswapX)

## 0. Pre-execution Declaration

- Current stage must be `Ready to Execute`
- Parameters must be confirmed and user must have explicitly confirmed
- Full command must be displayed before execution

## 1. Pre-flight Checks

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
```

> `--api-url` and `--chain-id` are omitted from `limit-order.js` calls — the script reads defaults from `limit_order.uniswapx_api` and `networks.ethereum_mainnet.chain_id` in `~/.aurehub/config.yaml`.

```bash
node --version     # If not found, hard-stop and prompt to install https://nodejs.org (Node is required for all script commands)
node swap.js balance   # ETH balance check + XAUT balance check (hard-stop if insufficient)
```

## 2. Parameter Confirmation (Preview)

Display at minimum:
- Pair: XAUT → USDT
- Limit price: `1 XAUT = X USDT` (i.e. minAmountOut / amountIn, human-readable)
- Amount: sell `amountIn` XAUT → receive at least `minAmountOut` USDT
- Expiry: `expiry` seconds / deadline in local time
- UniswapX Filler risk notice: XAUT is a low-liquidity token; if no Filler fills the order, it expires automatically after the deadline with no loss of funds

## 3. Large-Trade Double Confirmation

If `minAmountOut` (USDT) > `risk.large_trade_usd`, double confirmation is required.

## 4. Approve Permit2 (if allowance is insufficient)

XAUT is a standard ERC-20 — **approve directly, no reset needed**:

```bash
node swap.js allowance --token XAUT --spender 0x000000000022D473030F116dDEE9F6B43aC78BA3
```

If insufficient, approve directly:

```bash
node swap.js approve --token XAUT --amount <AMOUNT_IN> --spender 0x000000000022D473030F116dDEE9F6B43aC78BA3
```

## 5. Place Order

> **Important — raw units**: Unlike `swap.js` (which accepts human-readable amounts), `limit-order.js` requires **raw integer amounts in smallest token units**.
> Both XAUT and USDT have 6 decimals, so multiply by `10^6`:
> - 0.001 XAUT → `1000`
> - 5000 USDT → `5000000000`
> - Formula: `raw = human_amount * 1000000` (drop any fractional remainder)

Resolve contract addresses and wallet before placing:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
XAUT=$(node -e "const c=require('js-yaml').load(require('fs').readFileSync(require('os').homedir()+'/.aurehub/config.yaml','utf8')); console.log(c.tokens.XAUT.address)")
USDT=$(node -e "const c=require('js-yaml').load(require('fs').readFileSync(require('os').homedir()+'/.aurehub/config.yaml','utf8')); console.log(c.tokens.USDT.address)")
WALLET_ADDRESS=$(node swap.js address | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).address")
# Convert human-readable amounts to raw integers (6 decimals): raw = human_amount * 1000000
# AMOUNT_IN: user's XAUT sell amount; MIN_AMOUNT_OUT: minimum USDT to receive (from limit price)
AMOUNT_IN=$(node -e "console.log(Math.trunc(parseFloat('<XAUT_AMOUNT>') * 1e6))")
MIN_AMOUNT_OUT=$(node -e "console.log(Math.trunc(parseFloat('<MIN_USDT_AMOUNT>') * 1e6))")
```

```bash
# EXPIRY_SECONDS: use the user-specified expiry, or fall back to 86400 (1 day).
RESULT=$(node limit-order.js place \
  --token-in       "$XAUT" \
  --token-out      "$USDT" \
  --amount-in      "$AMOUNT_IN" \
  --min-amount-out "$MIN_AMOUNT_OUT" \
  --expiry         "$EXPIRY_SECONDS" \
  --wallet         "$WALLET_ADDRESS")
```

Parse result:

```bash
ORDER_HASH=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).orderHash")
DEADLINE=$(echo "$RESULT"   | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).deadline")
NONCE=$(echo "$RESULT"      | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).nonce")
```

## 6. Output

Return to user:
- `orderHash`: for querying / cancelling the order
- `deadline`: order expiry in local time
- Note: order details (including nonce) have been auto-saved to `~/.aurehub/orders/`. Cancellation can be done via `--order-hash` without needing to record the nonce manually.
- Reminder: order has been submitted to UniswapX; the computer does not need to stay online — the Filler network fills automatically when the price is reached

## 7. Error Handling

| Error | Action |
|-------|--------|
| `node` not found | Hard-stop, prompt to install Node.js >= 18 (required for all script commands) |
| XAUT precision > 6 decimals | Script-level hard-stop (exit 1), report minimum precision of 0.000001 |
| USDT minAmountOut precision > 6 decimals | Script-level hard-stop (exit 1), report maximum precision |
| XAUT balance insufficient | Hard-stop, report shortfall |
| Limit price deviates > 50% from current market | Warn + double confirmation (prevent price typos) |
| UniswapX API returns 4xx | Hard-stop, note XAUT may not be in the supported list, suggest market order |
| Approve failed | Return failure reason, suggest retry |

```

### references/limit-order-status.md

```markdown
# Limit Order Query

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
```

> `--api-url` and `--chain-id` are omitted from examples below — the script reads defaults from `limit_order.uniswapx_api` and `networks.ethereum_mainnet.chain_id` in `~/.aurehub/config.yaml`.

## 1. Query a Single Order (by orderHash)

```bash
RESULT=$(node limit-order.js status \
  --order-hash "$ORDER_HASH")

STATUS=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).status")
```

## 2. List All Open Orders (by wallet address)

```bash
WALLET_ADDRESS=$(node swap.js address | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).address")
RESULT=$(node limit-order.js list \
  --wallet       "$WALLET_ADDRESS" \
  --order-status open)   # Optional: open / filled / expired / cancelled — omit to return all
```

Returns JSON: `{ address, total, orders: [{ orderHash, status, inputToken, inputAmount, outputToken, outputAmount, txHash, createdAt }] }`

## 3. Status Display

| status | Display |
|--------|---------|
| `open` | Order active; remaining time = deadline - current time |
| `filled` | Filled: show txHash and actual settled amounts (settledAmounts) |
| `expired` | Expired; can re-place the order |
| `cancelled` | Cancelled |
| `not_found` | orderHash does not exist or has been purged from the API (may be cleared after expiry) |

## 4. Error Handling

- API unreachable: prompt to check network, suggest retrying later
- `not_found`: suggest the orderHash may be wrong, or the order has expired and been purged

```

### references/limit-order-cancel.md

```markdown
# Limit Order Cancellation

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
```

## 0. Pre-confirmation

Cancelling a limit order is an on-chain operation (gas required). Confirm before cancelling:
- orderHash
- Current order status (recommended: query first to avoid cancelling an already-filled or expired order)

## 1. Fetch Cancellation Parameters

**Preferred: use `--order-hash`** (auto-reads nonce from `~/.aurehub/orders/`):

```bash
CANCEL_PARAMS=$(node limit-order.js cancel \
  --order-hash "$ORDER_HASH")

WORD_POS=$(echo "$CANCEL_PARAMS" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).wordPos")
MASK=$(echo "$CANCEL_PARAMS"     | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).mask")
```

Supports prefix matching (e.g. `0x9079c4f2` matches the full hash). Falls back to `--nonce` if no local order file is found.

**Fallback: use `--nonce` directly** (if local order file is missing):

```bash
CANCEL_PARAMS=$(node limit-order.js cancel \
  --nonce "$NONCE")
```

## 2. Execute Cancellation

Display the command and wait for user confirmation:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
CANCEL_JSON=$(node swap.js cancel-nonce --word-pos "$WORD_POS" --mask "$MASK")
STATUS=$(echo "$CANCEL_JSON" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).status")
TX_HASH=$(echo "$CANCEL_JSON" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).txHash")
echo "Cancel tx: https://etherscan.io/tx/$TX_HASH"
if [ "$STATUS" != "success" ]; then echo "WARNING: on-chain cancellation failed (status=$STATUS)"; fi
```

## 3. Output

- tx hash
- Note: No assets were locked — Permit2 uses signature-based authorization, not asset custody. Cancellation revokes the signature on-chain; no token return operation is needed.

## 4. Special Cases

| Case | Action |
|------|--------|
| Order already filled | No cancellation needed; inform the user |
| Order already expired | Nonce has auto-invalidated; no on-chain cancellation needed |
| Cancel succeeds but Filler is still processing | Very low probability; the Filler transaction will revert once the nonce is invalidated on-chain |

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# xaut-trade

Our skills are developed by Duncan.Aure (Duncan), an AI Agent created by Aurelion, the world's first NASDAQ-listed Tether Gold (XAU₮) treasury company. Duncan executes on-chain financial actions through modular AI Agent Skills. Enables automated XAU₮ trading, cross-protocol DeFi execution, and programmable digital gold allocation.

Buy and sell XAUT (Tether Gold) on Ethereum mainnet via AI Agent, using Uniswap V3 + Node.js (ethers.js) under the hood.

## Supported Pairs

| Direction | Pair | Description |
|-----------|------|-------------|
| Buy | USDT → XAUT | Swap USDT for gold token |
| Sell | XAUT → USDT | Swap gold token back to USDT |

## Setup

### Automated (recommended)

Run the setup script — it handles wallet creation, config file generation, and optional Foundry installation interactively:

```bash
_saved=$(cat ~/.aurehub/.setup_path 2>/dev/null); [ -f "$_saved" ] && SETUP_PATH="$_saved"
[ -z "$SETUP_PATH" ] && { GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); [ -n "$GIT_ROOT" ] && [ -f "$GIT_ROOT/skills/xaut-trade/scripts/setup.sh" ] && SETUP_PATH="$GIT_ROOT/skills/xaut-trade/scripts/setup.sh"; }
[ -z "$SETUP_PATH" ] && SETUP_PATH=$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)
if [ -n "$SETUP_PATH" ] && [ -f "$SETUP_PATH" ]; then
  bash "$SETUP_PATH"
else
  echo "setup.sh not found. Run:"
  echo '  find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1'
  exit 1
fi
```

If the command above cannot find setup.sh (first-time install with a non-standard agent), locate it manually:

```bash
find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1
```

The script walks you through each step, clearly marks actions that require manual intervention, and explains the reason for each one.

After the script completes, follow the manual steps it prints at the end (fund wallet, get API key if needed).

For a chat-first real-mainnet walkthrough (Agent-driven, minimal manual steps), see:
- `references/live-trading-runbook.md`

### Manual (fallback)

If you prefer to configure everything yourself, or if the script fails at a specific step:

First choose a wallet mode: **WDK (recommended)** or **Foundry**.

#### Option A: WDK mode (recommended)

**1. Create password file**

```bash
mkdir -p ~/.aurehub
bash -c 'read -rsp "WDK password (min 12 chars): " p </dev/tty; echo; printf "%s" "$p" > ~/.aurehub/.wdk_password; chmod 600 ~/.aurehub/.wdk_password; echo "Password saved."'
```

**2. Create WDK wallet** (requires Node.js >= 18)

```bash
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SCRIPTS_DIR=$(dirname "$SETUP_PATH")
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -d "$GIT_ROOT/skills/xaut-trade/scripts" ]; then
  SCRIPTS_DIR="$GIT_ROOT/skills/xaut-trade/scripts"
else
  SCRIPTS_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")
fi
cd "$SCRIPTS_DIR" && npm install
node "$SCRIPTS_DIR/lib/create-wallet.js" --password-file ~/.aurehub/.wdk_password
```

**3. Create .env**

```bash
cat > ~/.aurehub/.env << EOF
WALLET_MODE=wdk
ETH_RPC_URL=https://eth.llamarpc.com
ETH_RPC_URL_FALLBACK=https://eth.merkle.io,https://rpc.flashbots.net/fast,https://eth.drpc.org,https://ethereum.publicnode.com
WDK_PASSWORD_FILE=~/.aurehub/.wdk_password
# UNISWAPX_API_KEY=your_key_here   # required for limit orders only
EOF
chmod 600 ~/.aurehub/.env
```

#### Option B: Foundry mode

**1. Install Foundry**

```bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
source ~/.zshrc   # or ~/.bashrc
```

**2. Create password file and configure wallet**

```bash
mkdir -p ~/.aurehub
bash -c 'read -rsp "Keystore password: " p </dev/tty; echo; printf "%s" "$p" > ~/.aurehub/.wallet.password; chmod 600 ~/.aurehub/.wallet.password; echo "Password saved."'
```

Then choose one initialization method:

```bash
# Import existing private key (interactive)
cast wallet import aurehub-wallet --interactive

# Or create a new wallet
mkdir -p ~/.foundry/keystores
cast wallet new ~/.foundry/keystores aurehub-wallet \
  --password-file ~/.aurehub/.wallet.password
```

**3. Create .env**

```bash
cat > ~/.aurehub/.env << EOF
WALLET_MODE=foundry
ETH_RPC_URL=https://eth.llamarpc.com
ETH_RPC_URL_FALLBACK=https://eth.merkle.io,https://rpc.flashbots.net/fast,https://eth.drpc.org,https://ethereum.publicnode.com
FOUNDRY_ACCOUNT=aurehub-wallet
KEYSTORE_PASSWORD_FILE=~/.aurehub/.wallet.password
# UNISWAPX_API_KEY=your_key_here   # required for limit orders only
EOF
chmod 600 ~/.aurehub/.env
```

#### Common steps (both modes)

**4. Copy config and install dependencies**

```bash
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SKILL_DIR=$(cd "$(dirname "$SETUP_PATH")/.." && pwd)
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$GIT_ROOT/skills/xaut-trade/config.example.yaml" ]; then
  SKILL_DIR="$GIT_ROOT/skills/xaut-trade"
else
  SKILL_DIR=$(cd "$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")/.." && pwd)
fi
cp "$SKILL_DIR/config.example.yaml" ~/.aurehub/config.yaml
```

**5. Install runtime dependencies (required for market and limit orders)**

```bash
node --version   # requires >= 18; install from https://nodejs.org if missing
SETUP_PATH=$(cat ~/.aurehub/.setup_path 2>/dev/null)
if [ -f "$SETUP_PATH" ]; then
  SCRIPTS_DIR=$(dirname "$SETUP_PATH")
elif GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [ -d "$GIT_ROOT/skills/xaut-trade/scripts" ]; then
  SCRIPTS_DIR="$GIT_ROOT/skills/xaut-trade/scripts"
else
  SCRIPTS_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*/xaut-trade/scripts/setup.sh" 2>/dev/null | head -1)")
fi
cd "$SCRIPTS_DIR" && npm install
```

**6. Get a UniswapX API Key (limit orders only)**

How to obtain (about 5 minutes, free):
1. Visit [developers.uniswap.org/dashboard](https://developers.uniswap.org/dashboard)
2. Sign in with Google or GitHub
3. Generate a Token (Free tier)

```bash
echo 'UNISWAPX_API_KEY=your_key_here' >> ~/.aurehub/.env
```

Market orders do not require an API Key.

**7. Fund the wallet**

- A small amount of ETH (≥ 0.005) for gas
- USDT (for buying XAUT)
- XAUT (for selling)

## Usage

Just talk to the Agent in natural language:

### Buy

```
buy XAUT with 100 USDT
buy 200 USDT worth of XAUT
```

### Sell

```
sell 0.01 XAUT
swap 0.05 XAUT for USDT
sell 0.1 XAUT
```

### Limit Buy

```
buy 0.01 XAUT when price drops to 3000 USDT
limit order: buy 0.01 XAUT when price reaches 3000 USDT
limit buy XAUT at 3000, amount 0.01, valid 3 days
```

### Limit Sell

```
sell 0.01 XAUT when price rises to 4000 USDT
limit sell 0.01 XAUT at target price 3800 USDT, valid 3 days
sell 0.01 XAUT when price reaches 4000
```

### Check Limit Order

```
check my limit order status, orderHash is 0x...
```

### Cancel Limit Order

```
cancel limit order, orderHash is 0x...
```

### Check Balance

```
check my XAUT balance
```

## Trade Flow

For both buy and sell, the Agent follows this semi-automated flow:

```
Pre-flight checks → On-chain quote → Preview display → [Threshold-based confirmation] → Approve (mode-based confirmation) → Swap → Result verification
```

Before on-chain writes, the Agent always displays full commands. Confirmation level is policy-driven:
- Trade confirmation uses USD thresholds (`confirm_trade_usd`, `large_trade_usd`):
  - `< confirm_trade_usd`: preview shown, no blocking confirmation required
  - `>= confirm_trade_usd` and `< large_trade_usd`: single confirmation required
  - `>= large_trade_usd` or estimated slippage exceeds `max_slippage_bps_warn`: double confirmation required
- Approval confirmation uses `approve_confirmation_mode` with oversize safety override

## Risk Controls

| Rule | Default Threshold | Behavior |
|------|-------------------|----------|
| Trade confirm threshold | `confirm_trade_usd = $10` | Above threshold requires confirmation |
| Large trade | >= $1,000 USD | Double confirmation required |
| High slippage | > 50 bps (0.5%) | Warning + double confirmation |
| Oversized approval | `approve > 10x amount_in` | Force approval confirmation |
| Insufficient gas | ETH < 0.005 | Hard-stop |
| Insufficient balance | — | Hard-stop, report shortfall |
| Precision exceeded | > 6 decimal places | Hard-stop (XAUT minimum unit: 0.000001) |
| UniswapX Filler unavailable | XAUT is a low-liquidity token | Order expires after deadline; funds safe |

Thresholds can be customized in the `risk` section of `config.yaml`.

## Configuration

### .env (required)

| Variable | Description | Example |
|----------|-------------|---------|
| `WALLET_MODE` | Wallet backend: `wdk` (recommended) or `foundry` | `wdk` |
| `ETH_RPC_URL` | Ethereum RPC URL | `https://eth.llamarpc.com` |
| `ETH_RPC_URL_FALLBACK` | Comma-separated fallback RPCs tried in order on network error (429/502/timeout) | `https://eth.merkle.io,...` |
| `WDK_PASSWORD_FILE` | Path to WDK vault password file (**WDK mode**) | `~/.aurehub/.wdk_password` |
| `WDK_VAULT_FILE` | Override default vault path (**WDK mode**, optional) | `~/.aurehub/.wdk_vault` |
| `FOUNDRY_ACCOUNT` | Foundry keystore account name (**Foundry mode**) | `aurehub-wallet` |
| `KEYSTORE_PASSWORD_FILE` | Path to keystore password file (**Foundry mode**) | `~/.aurehub/.wallet.password` |
| `UNISWAPX_API_KEY` | UniswapX API Key (**limit orders only**, not needed for market orders) | Get at: developers.uniswap.org/dashboard |
| `RANKINGS_OPT_IN` | Join activity rankings — opt-in only (default: `false`) | `true` or `false` |
| `NICKNAME` | Display name for activity rankings (required if `RANKINGS_OPT_IN=true`) | `Alice` |

### config.yaml (optional)

Key adjustable parameters:

```yaml
risk:
  default_slippage_bps: 50      # Default slippage protection 0.5%
  max_slippage_bps_warn: 50     # Slippage warning threshold
  confirm_trade_usd: 10         # Single-confirm threshold (USD)
  large_trade_usd: 1000         # Large trade threshold (USD)
  approve_confirmation_mode: "first_only" # always | first_only | never (never is high-risk)
  approve_force_confirm_multiple: 10       # Force confirm if approve > 10x amount_in
  min_eth_for_gas: "0.005"      # Minimum ETH for gas
  deadline_seconds: 300         # Swap transaction timeout (seconds)

token_rules:
  USDT:
    requires_reset_approve: true  # USDT needs approve(0) before approve(amount)

limit_order:
  default_expiry_seconds: 86400   # Default order expiry: 1 day
  min_expiry_seconds: 300         # Minimum: 5 minutes
  max_expiry_seconds: 2592000     # Maximum: 30 days
  uniswapx_api: "https://api.uniswap.org/v2"  # Override for local mock testing
```

## Local Testing (Anvil Fork)

> **Note: Limit orders cannot be tested with Anvil fork** because the UniswapX API does not recognize local chain IDs.
> For limit orders, use a very small amount (e.g. 1 USDT → XAUT) on mainnet for end-to-end verification.
> Signature format can be validated against a local mock service via `limit_order.uniswapx_api` in `config.yaml`.

Use Anvil to fork mainnet state locally for zero-cost testing of the full buy/sell flow without spending real assets.

### 1. Start Anvil Fork

```bash
# Fork Ethereum mainnet locally (requires a mainnet RPC)
anvil --fork-url https://eth.llamarpc.com

# Optionally pin a block (for reproducible state)
anvil --fork-url https://eth.llamarpc.com --fork-block-number 19500000
```

Anvil starts with 10 pre-funded accounts, each with 10,000 ETH. Default: `http://127.0.0.1:8545`.

### 2. Point .env to Local

```bash
# .env
WALLET_MODE=foundry
ETH_RPC_URL=http://127.0.0.1:8545
FOUNDRY_ACCOUNT=aurehub-wallet
KEYSTORE_PASSWORD_FILE=~/.aurehub/.wallet.password
```
If you want to use Anvil account #0, import it once into keystore:

```bash
cast wallet import aurehub-wallet --interactive
```

### 3. Fund the Test Account with USDT

Anvil pre-funded accounts only have ETH. Use `cast` to impersonate a whale and transfer tokens:

```bash
# Find a USDT whale (e.g. Binance Hot Wallet)
USDT=0xdAC17F958D2ee523a2206206994597C13D831ec7
WHALE=0xF977814e90dA44bFA03b6295A0616a897441aceC  # Binance hot wallet

# Impersonate whale, transfer 10,000 USDT to test account
cast send $USDT "transfer(address,uint256)" \
  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 10000000000 \
  --from $WHALE \
  --unlocked \
  --rpc-url http://127.0.0.1:8545

# Verify balance
cast call $USDT "balanceOf(address)" \
  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
  --rpc-url http://127.0.0.1:8545
```

### 4. Run Test Trades

Once configured, just use the skill normally:

```
buy XAUT with 100 USDT
```

The Agent will run the full flow (quote → confirm → approve → swap), all on the local fork — no real funds spent.

### 5. Notes

- Anvil fork state is **temporary** and resets on restart (unless using `anvil --state` for persistence)
- Local testing may use `--unlocked` + `--from` for manual token funding, while skill runtime signing remains keystore-only (`--account` + `--password-file`)
- If the fork runs for a long time, on-chain state may diverge from current mainnet; restart the fork to refresh
- Whale addresses may change over time; if the transfer fails, check the latest top holders on [Etherscan](https://etherscan.io/token/0xdAC17F958D2ee523a2206206994597C13D831ec7#balances)

## Security & Privacy

> For the complete environment and security declaration (required config files, environment variables, filesystem access, security safeguards), see the **Environment & Security Declaration** section in [SKILL.md](SKILL.md).

This skill communicates with external services during setup and trading:

| Service | When | Data Sent |
|---------|------|-----------|
| foundry.paradigm.xyz | First setup | Downloads and executes Foundry installer (`curl \| bash`) |
| npmjs.com | Limit order setup | Downloads Node.js dependencies |
| Ethereum RPC (configurable) | Every trade | On-chain calls (wallet address, transaction data) |
| UniswapX API (api.uniswap.org) | Limit orders | Order data, wallet address |
| xaue.com Rankings API | Opt-in only | Wallet address, nickname |

- **Foundry installation** uses `curl | bash`. Review the source at [github.com/foundry-rs/foundry](https://github.com/foundry-rs/foundry) before proceeding. The setup script asks for confirmation before running.
- **Rankings registration** remains opt-in (`RANKINGS_OPT_IN=false` by default). If not enabled during setup, the Agent can prompt once after your first successful trade.
- **All API calls use HTTPS.**

## FAQ

**Q: What if a transaction gets stuck or fails?**
The Agent will provide retry suggestions: reduce amount, increase slippage tolerance, or check nonce and gas.

**Q: Why does USDT approval require two steps?**
USDT's non-standard implementation requires `approve(0)` to reset the allowance before `approve(amount)`. XAUT does not.

**Q: Are other chains supported?**
Only Ethereum mainnet (chain_id: 1) is currently supported. Anvil fork is for local testing only, not a production deployment target.

**Q: I see `Device not configured (os error 6)` in Foundry mode — what do I do?**

This happens on macOS when Foundry's keystore cannot access the system Keychain in a non-interactive environment. Fix:

1. Create a password file and set permissions:
   ```bash
   echo "your_keystore_password" > ~/.aurehub/.wallet.password
   chmod 600 ~/.aurehub/.wallet.password
   ```
2. Set `KEYSTORE_PASSWORD_FILE` to point to this file in `.env`.
3. Re-run the trade flow.

**Q: What is a Skill package? How does it drive the AI to trade gold?**

A Skill package is a set of structured AI instruction files (`SKILL.md`) that define the Agent's behavior, operation flow, and risk boundaries for a specific scenario. The `xaut-trade` Skill tells the Agent how to check prerequisites, call the Uniswap V3 quote contract, execute trades via Node.js scripts, handle USDT's non-standard approval, and more. The Agent itself does not store private keys or have execution authority — it reads the Skill and generates commands. Depending on the configured risk thresholds, the Agent requests the required confirmation(s) before signing and broadcasting.

**Q: Do I need a computer running 24/7?**

- **Market orders (buy/sell)**: No. Market trades are one-shot interactions — you send the instruction → Agent quotes → you confirm → trade completes. No need to stay online.
- **Limit orders**: No. After signing, the order is submitted to the UniswapX network, where third-party Filler nodes automatically fill it when the price is met. Your computer can be off. Note: if no Filler fills the order before the `deadline`, it expires naturally with no loss of funds.

**Q: Does it only work with Claude Code?**

No. The Skill supports two main runners:

- **Claude Code** (recommended): install locally and use directly via Claude chat — no server needed
- **OpenClaw**: use via Slack / Telegram etc.; each user must configure their own wallet credentials independently

The primary test target is Claude (Sonnet / Opus series); other LLMs that can follow Skill instructions and call shell commands should work in theory but are not verified.

**Q: Will you read my API Key or private key from `.env`?**

No. The Skill package runs entirely locally. The only optional external data sharing is the activity rankings feature (opt-in during setup or first-success prompt, sends wallet address and nickname to xaue.com). All trades are executed via local Node.js scripts — no intermediary servers. Private keys are encrypted locally (WDK vault or Foundry keystore); `.env` only stores the account name and other config (wallet address is derived at runtime). Never commit `.env` to version control.

**Q: Will the Agent auto-buy based on price movements?**

No. The Agent does not monitor prices or make autonomous decisions. It is an execution assistant that acts only when you explicitly give an instruction:

- **Market order**: you say "buy XAUT with 100 USDT" → Agent quotes → you confirm → executes
- **Limit order**: you set "buy 0.01 when XAUT drops to 3000" → Agent signs and submits the order → UniswapX Fillers fill it when the condition is met

**Q: Do I need to manually confirm each trade? Can it spend my money without confirmation?**

By default, confirmation is threshold-based: small trades show full preview and can execute without blocking confirmation, medium trades require one confirmation, and large/high-risk trades require double confirmation. Approval confirmations are controlled by `approve_confirmation_mode`, with a mandatory override for oversized approvals. `approve_confirmation_mode=never` is high-risk and intended for advanced users only. The Agent cannot sign without your local keystore/password setup.

**Q: Can I use multiple wallets simultaneously?**

The current Skill is designed for a single wallet per instance. For multi-wallet use, prepare a separate `.env` for each wallet (with distinct `FOUNDRY_ACCOUNT` and `KEYSTORE_PASSWORD_FILE`), and switch config files before each operation. There is no built-in multi-wallet concurrent management.

**Q: Do I need to reinstall after a Skill update?**

Yes. Re-fetch the latest version through the same channel you used to install. Updates will not overwrite your local config (`.env`, `config.yaml`).

## Stay Connected

For our updates, new skills, and ecosystem developments. Check out:

- **X**: [Aure_duncan](https://x.com/Aure_duncan)
- **Telegram**: [@aure_duncanbot](https://t.me/aure_duncanbot)
- **Aurelion**: [aurelion.com](https://www.aurelion.com/)

```

### _meta.json

```json
{
  "owner": "aure-duncan",
  "slug": "aurehub-xaut-trade",
  "displayName": "xaut-trade",
  "latest": {
    "version": "2.1.1",
    "publishedAt": 1773619748279,
    "commit": "https://github.com/openclaw/skills/commit/2aa9e07e1e5494dd0c656743cf917b89159ef361"
  },
  "history": [
    {
      "version": "2.0.1",
      "publishedAt": 1772898671116,
      "commit": "https://github.com/openclaw/skills/commit/8883fedc74a05787e82f9062ed6d580ee1eaa61c"
    },
    {
      "version": "1.0.0",
      "publishedAt": 1772759808731,
      "commit": "https://github.com/openclaw/skills/commit/7a5fc54999d1401bfe15edc31310ccc530b5ccd3"
    }
  ]
}

```

### references/balance.md

```markdown
# Balance & Pre-flight Checks

Complete the following steps in order before any quote or execution.

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced. Each Bash block must begin with:

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
```

## 1. Environment Check

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js address
```

If the command fails, stop and prompt:
- Node.js not installed or < 18: install Node.js first
- Config missing: trigger onboarding
- RPC unavailable: trigger RPC fallback sequence (see RPC Fallback section in SKILL.md)

## 2. Wallet Mode Validation

Check `WALLET_MODE` in `~/.aurehub/.env`:

- **If `WALLET_MODE=wdk`**: verify `WDK_PASSWORD_FILE` is set and readable:
  ```bash
  source ~/.aurehub/.env
  test -r "$WDK_PASSWORD_FILE" && echo "OK" || echo "FAIL"
  ```
  If `FAIL`, hard-stop:
  > Password file not readable: `$WDK_PASSWORD_FILE`
  > Create it with: `bash -c 'read -rsp "WDK password: " p </dev/tty; echo; printf "%s" "$p" > ~/.aurehub/.wdk_password; chmod 600 ~/.aurehub/.wdk_password; echo "Saved."'`

- **If `WALLET_MODE=foundry`**: verify `FOUNDRY_ACCOUNT` and `KEYSTORE_PASSWORD_FILE` are set:
  ```bash
  source ~/.aurehub/.env
  [ -n "$FOUNDRY_ACCOUNT" ] && [ -n "$KEYSTORE_PASSWORD_FILE" ] && echo "OK" || echo "FAIL"
  ```
  If `FAIL`, hard-stop:
  > Missing keystore signing config. Set both `FOUNDRY_ACCOUNT` and `KEYSTORE_PASSWORD_FILE` in `.env`.

  Verify password file is readable:
  ```bash
  source ~/.aurehub/.env
  test -r "$KEYSTORE_PASSWORD_FILE" && echo "OK" || echo "FAIL"
  ```
  If `FAIL`, hard-stop:
  > Password file not readable: `$KEYSTORE_PASSWORD_FILE`

If `PRIVATE_KEY` exists in `.env`, hard-stop immediately:
> `PRIVATE_KEY` runtime mode is no longer supported.
> Remove `PRIVATE_KEY` from `.env` and use either WDK or Foundry wallet mode.

## 3. Derive Wallet Address

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
WALLET_ADDRESS=$(node swap.js address | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).address")
echo "$WALLET_ADDRESS"
```

## 4. Full Balance Check (ETH + USDT + XAUT)

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
node swap.js balance
```

Output is JSON with all balances pre-formatted (human-readable):

```json
{
  "address": "0x...",
  "ETH": "0.05",
  "USDT": "1000.0",
  "XAUT": "0.5"
}
```

- If ETH balance is below `risk.min_eth_for_gas`, hard-stop
- **Buy flow**: if USDT balance is insufficient for the intended trade, hard-stop and report the shortfall
- **Sell flow**: if XAUT balance is insufficient for the intended trade, hard-stop and report the shortfall

```

### references/quote.md

```markdown
# Quote & Slippage Protection

All commands below assume CWD is `$SCRIPTS_DIR` and env is sourced.

## 1. Fetch Quote

Example: buy with 100 USDT

```bash
source ~/.aurehub/.env
cd "$SCRIPTS_DIR"
RESULT=$(node swap.js quote --side buy --amount 100)
echo "$RESULT"
```

Output is JSON:

```json
{
  "side": "buy",
  "amountIn": "100",
  "amountOut": "0.033",
  "amountOutRaw": "33000",
  "sqrtPriceX96": "...",
  "gasEstimate": "150000"
}
```

For sell direction, use `--side sell --amount <XAUT_amount>`.

Extract values for downstream use:

```bash
AMOUNT_OUT=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).amountOut")
AMOUNT_OUT_RAW=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).amountOutRaw")
GAS_ESTIMATE=$(echo "$RESULT" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).gasEstimate")
```

## 2. Calculate minAmountOut

Read `default_slippage_bps` from config.yaml (e.g. 50 bps = 0.5%):

```bash
# Read slippage from config.yaml, default to 50 bps
DEFAULT_SLIPPAGE_BPS=$(node -e "const c=require('js-yaml').load(require('fs').readFileSync(require('os').homedir()+'/.aurehub/config.yaml','utf8')); console.log((c.risk||{}).default_slippage_bps||50)")
# Use node to avoid bash integer overflow on large trades
MIN_AMOUNT_OUT=$(node -p "Math.trunc($AMOUNT_OUT_RAW * (10000 - $DEFAULT_SLIPPAGE_BPS) / 10000)")
```

## 3. Preview Output

Must include at minimum:
- Input amount (human-readable)
- Estimated output received (`amountOut`)
- Slippage setting and `minAmountOut`
- Risk indicators (large trade / slippage / gas)

## 4. Execution Confirmation Gate

Determine confirmation level by USD notional and risk:

- `< risk.confirm_trade_usd`: show full preview, then execute without blocking confirmation
- `>= risk.confirm_trade_usd` and `< risk.large_trade_usd`: single confirmation
- `>= risk.large_trade_usd` or estimated slippage exceeds `risk.max_slippage_bps_warn`: double confirmation

Accepted confirmation phrases:
- "confirm approve"
- "confirm swap"

```

### references/wallet-modes.md

```markdown
# Wallet Modes

## WDK Mode (Recommended)

- **Storage**: Encrypted vault (`~/.aurehub/.wdk_vault`) using PBKDF2-SHA256 + XSalsa20-Poly1305
- **Encryption**: PBKDF2 with 100k iterations, seed never stored as plaintext
- **Dependencies**: Node.js >= 18 only — no external tools required
- **Setup**: Choose password → encrypted vault created automatically
- **Config**: `WALLET_MODE=wdk` + `WDK_PASSWORD_FILE` in `.env`

## Foundry Mode (Advanced)

- **Storage**: Foundry keystore (`~/.foundry/keystores/<account>`) — standard Web3 Secret Storage
- **Encryption**: Scrypt-based (Foundry default)
- **Dependencies**: Foundry (`cast`) must be installed
- **Setup**: Install Foundry → import/create keystore → set password file
- **Config**: `WALLET_MODE=foundry` + `FOUNDRY_ACCOUNT` + `KEYSTORE_PASSWORD_FILE` in `.env`

## Switching Modes

Re-run `setup.sh` and select the other mode. Existing wallet data is not deleted.

## Security Comparison

| Feature | WDK | Foundry |
|---------|-----|---------|
| Seed/key encryption at rest | PBKDF2 + XSalsa20-Poly1305 | Scrypt |
| Password file | `~/.aurehub/.wdk_password` | `~/.aurehub/.wallet.password` |
| External tool required | No | Yes (Foundry) |
| Key derivation | BIP-39/BIP-44 (HD wallet) | Single key per keystore |

```

### scripts/__tests__/helpers.test.js

```javascript
import { describe, it, expect } from 'vitest';
import { computeNonceComponents, resolveExpiry, checkPrecision } from '../helpers.js';

// --- computeNonceComponents ---
// Permit2 invalidateUnorderedNonces(wordPos, mask):
//   wordPos = nonce >> 8n   (which 256-bit word)
//   mask    = 1n << (nonce & 0xFFn)  (which bit within the word)

describe('computeNonceComponents', () => {
  it('nonce 0 → wordPos 0, mask 1', () => {
    const { wordPos, mask } = computeNonceComponents(0n);
    expect(wordPos).toBe(0n);
    expect(mask).toBe(1n);
  });

  it('nonce 1 → wordPos 0, mask 2', () => {
    const { wordPos, mask } = computeNonceComponents(1n);
    expect(wordPos).toBe(0n);
    expect(mask).toBe(2n);
  });

  it('nonce 255 → wordPos 0, mask 2^255', () => {
    const { wordPos, mask } = computeNonceComponents(255n);
    expect(wordPos).toBe(0n);
    expect(mask).toBe(2n ** 255n);
  });

  it('nonce 256 → wordPos 1, mask 1', () => {
    const { wordPos, mask } = computeNonceComponents(256n);
    expect(wordPos).toBe(1n);
    expect(mask).toBe(1n);
  });

  it('nonce 257 → wordPos 1, mask 2', () => {
    const { wordPos, mask } = computeNonceComponents(257n);
    expect(wordPos).toBe(1n);
    expect(mask).toBe(2n);
  });
});

// --- resolveExpiry ---

describe('resolveExpiry', () => {
  const limits = { defaultSeconds: 86400, minSeconds: 300, maxSeconds: 2592000 };

  it('null → default', () => {
    expect(resolveExpiry(null, limits)).toBe(86400);
  });

  it('undefined → default', () => {
    expect(resolveExpiry(undefined, limits)).toBe(86400);
  });

  it('value within range → unchanged', () => {
    expect(resolveExpiry(3600, limits)).toBe(3600);
  });

  it('below min → clamped to min', () => {
    expect(resolveExpiry(60, limits)).toBe(300);
  });

  it('above max → clamped to max', () => {
    expect(resolveExpiry(9999999, limits)).toBe(2592000);
  });

  it('exactly min → allowed', () => {
    expect(resolveExpiry(300, limits)).toBe(300);
  });

  it('exactly max → allowed', () => {
    expect(resolveExpiry(2592000, limits)).toBe(2592000);
  });
});

// --- checkPrecision ---

describe('checkPrecision', () => {
  it('integer → valid', () => {
    expect(checkPrecision('1000000', 6)).toBe(true);
  });

  it('exactly 6 decimals → valid', () => {
    expect(checkPrecision('0.000001', 6)).toBe(true);
  });

  it('7 decimals → invalid', () => {
    expect(checkPrecision('0.0000001', 6)).toBe(false);
  });

  it('no dot → valid', () => {
    expect(checkPrecision('42', 6)).toBe(true);
  });

  it('5 decimals → valid', () => {
    expect(checkPrecision('1.12345', 6)).toBe(true);
  });

  it('boundary: maxDecimals=0 integer → valid', () => {
    expect(checkPrecision('5', 0)).toBe(true);
  });
});

```

### scripts/__tests__/integration.test.js

```javascript
import { describe, it, expect } from 'vitest';

describe('module imports', () => {
  it('imports config', async () => {
    const m = await import('../lib/config.js');
    expect(typeof m.loadConfig).toBe('function');
    expect(typeof m.resolveToken).toBe('function');
  });
  it('imports provider', async () => {
    const m = await import('../lib/provider.js');
    expect(typeof m.createProvider).toBe('function');
    expect(typeof m.FallbackProvider).toBe('function');
  });
  it('imports signer', async () => {
    const m = await import('../lib/signer.js');
    expect(typeof m.createSigner).toBe('function');
  });
  it('imports erc20', async () => {
    const m = await import('../lib/erc20.js');
    expect(typeof m.getBalance).toBe('function');
    expect(typeof m.getAllowance).toBe('function');
    expect(typeof m.approve).toBe('function');
  });
  it('imports uniswap', async () => {
    const m = await import('../lib/uniswap.js');
    expect(typeof m.quote).toBe('function');
    expect(typeof m.buildSwap).toBe('function');
  });
  it('imports swap CLI parser', async () => {
    const m = await import('../swap.js');
    expect(typeof m.parseCliArgs).toBe('function');
  });
});

```

### scripts/__tests__/swap-cli.test.js

```javascript
import { describe, it, expect } from 'vitest';
import { parseCliArgs } from '../swap.js';

describe('parseCliArgs', () => {
  it('parses quote subcommand with --side and --amount', () => {
    const result = parseCliArgs(['quote', '--side', 'buy', '--amount', '100']);
    expect(result.command).toBe('quote');
    expect(result.side).toBe('buy');
    expect(result.amount).toBe('100');
  });

  it('parses approve with --token and --amount', () => {
    const result = parseCliArgs(['approve', '--token', 'USDT', '--amount', '1000']);
    expect(result.command).toBe('approve');
    expect(result.token).toBe('USDT');
    expect(result.amount).toBe('1000');
  });

  it('parses swap with --side, --amount, --min-out', () => {
    const result = parseCliArgs(['swap', '--side', 'sell', '--amount', '0.5', '--min-out', '1500']);
    expect(result.command).toBe('swap');
    expect(result.side).toBe('sell');
    expect(result.amount).toBe('0.5');
    expect(result.minOut).toBe('1500');
  });

  it('parses balance (no args needed)', () => {
    const result = parseCliArgs(['balance']);
    expect(result.command).toBe('balance');
  });

  it('parses allowance with --token', () => {
    const result = parseCliArgs(['allowance', '--token', 'XAUT']);
    expect(result.command).toBe('allowance');
    expect(result.token).toBe('XAUT');
  });

  it('parses allowance with --token and --spender', () => {
    const result = parseCliArgs(['allowance', '--token', 'USDT', '--spender', '0x000000000022D473030F116dDEE9F6B43aC78BA3']);
    expect(result.command).toBe('allowance');
    expect(result.token).toBe('USDT');
    expect(result.spender).toBe('0x000000000022D473030F116dDEE9F6B43aC78BA3');
  });

  it('parses approve with --token, --amount, and --spender', () => {
    const result = parseCliArgs(['approve', '--token', 'USDT', '--amount', '1000', '--spender', '0x000000000022D473030F116dDEE9F6B43aC78BA3']);
    expect(result.command).toBe('approve');
    expect(result.token).toBe('USDT');
    expect(result.amount).toBe('1000');
    expect(result.spender).toBe('0x000000000022D473030F116dDEE9F6B43aC78BA3');
  });

  it('parses address', () => {
    const result = parseCliArgs(['address']);
    expect(result.command).toBe('address');
  });

  it('parses sign with --data-file', () => {
    const result = parseCliArgs(['sign', '--data-file', '/tmp/typed-data.json']);
    expect(result.command).toBe('sign');
    expect(result.dataFile).toBe('/tmp/typed-data.json');
  });

  it('parses cancel-nonce with --word-pos and --mask', () => {
    const result = parseCliArgs(['cancel-nonce', '--word-pos', '42', '--mask', '115792089237316195423570985008687907853269984665640564039457584007913129639935']);
    expect(result.command).toBe('cancel-nonce');
    expect(result.wordPos).toBe('42');
    expect(result.mask).toBe('115792089237316195423570985008687907853269984665640564039457584007913129639935');
  });

  it('errors on unknown subcommand', () => {
    expect(() => parseCliArgs(['unknown-cmd'])).toThrow(/unknown command/i);
  });

  it('normalizes --side for quote/swap', () => {
    expect(parseCliArgs(['quote', '--side', 'BUY', '--amount', '1']).side).toBe('buy');
    expect(parseCliArgs(['swap', '--side', ' Sell ', '--amount', '1', '--min-out', '1']).side).toBe('sell');
  });

  it('errors when --side is not buy/sell for quote/swap', () => {
    expect(() => parseCliArgs(['quote', '--side', 'long', '--amount', '1'])).toThrow(/invalid --side/i);
    expect(() => parseCliArgs(['swap', '--side', 'short', '--amount', '1', '--min-out', '1'])).toThrow(/invalid --side/i);
  });

  it('parses --config-dir override', () => {
    const result = parseCliArgs(['balance', '--config-dir', '/tmp/myconfig']);
    expect(result.command).toBe('balance');
    expect(result.configDir).toBe('/tmp/myconfig');
  });
});

```

### scripts/__tests__/wallet-init.test.js

```javascript
/**
 * wallet-init.test.js — Simulate wallet initialization and mode switching.
 *
 * Tests:
 *   1. WDK vault creation → decrypt → derive same address (round-trip)
 *   2. createSigner() with WDK mode
 *   3. createSigner() with Foundry mode (using ethers-generated keystore)
 *   4. Mode switching: WDK → Foundry → WDK (same config object, different WALLET_MODE)
 *   5. Error paths: missing WALLET_MODE, unknown mode, bad password, missing vault
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { execFileSync } from 'child_process';
import { Wallet } from 'ethers6';

const SCRIPTS_DIR = join(import.meta.dirname, '..');
const CREATE_WALLET = join(SCRIPTS_DIR, 'lib', 'create-wallet.js');

describe('wallet initialization & mode switching', () => {
  let tmpDir;
  let wdkPasswordFile;
  let wdkVaultFile;
  let foundryKeystoreDir;
  let foundryPasswordFile;
  let wdkAddress;
  let foundryAddress;

  const WDK_PASSWORD = 'test-password-long-enough';
  const FOUNDRY_PASSWORD = 'foundry-pw-12chars';
  const FOUNDRY_ACCOUNT = 'test-account';

  beforeAll(async () => {
    tmpDir = mkdtempSync(join(tmpdir(), 'wallet-init-test-'));

    // --- WDK setup ---
    wdkPasswordFile = join(tmpDir, '.wdk_password');
    wdkVaultFile = join(tmpDir, '.wdk_vault');
    writeFileSync(wdkPasswordFile, WDK_PASSWORD, { mode: 0o600 });

    // Create WDK vault via create-wallet.js CLI
    const out = execFileSync('node', [
      CREATE_WALLET,
      '--password-file', wdkPasswordFile,
      '--vault-file', wdkVaultFile,
    ], { encoding: 'utf8', cwd: SCRIPTS_DIR });

    const result = JSON.parse(out.trim());
    wdkAddress = result.address;
    expect(wdkAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);

    // --- Foundry keystore setup (simulate with ethers) ---
    foundryKeystoreDir = join(tmpDir, 'keystores');
    mkdirSync(foundryKeystoreDir, { recursive: true });
    foundryPasswordFile = join(tmpDir, '.foundry_password');
    writeFileSync(foundryPasswordFile, FOUNDRY_PASSWORD, { mode: 0o600 });

    const foundryWallet = Wallet.createRandom();
    foundryAddress = foundryWallet.address;
    const keystoreJson = await foundryWallet.encrypt(FOUNDRY_PASSWORD);
    writeFileSync(join(foundryKeystoreDir, FOUNDRY_ACCOUNT), keystoreJson);
  });

  afterAll(() => {
    if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
  });

  // -----------------------------------------------------------------------
  // 1. WDK round-trip: create → decrypt → same address
  // -----------------------------------------------------------------------

  it('WDK: vault round-trip produces consistent address', async () => {
    const { createSigner } = await import('../lib/signer.js');

    const cfg = {
      env: {
        WALLET_MODE: 'wdk',
        WDK_VAULT_FILE: wdkVaultFile,
        WDK_PASSWORD_FILE: wdkPasswordFile,
      },
    };

    const wallet = await createSigner(cfg, null);
    expect(wallet.address).toBe(wdkAddress);
  });

  // -----------------------------------------------------------------------
  // 2. Foundry mode: keystore decrypt → correct address
  // -----------------------------------------------------------------------

  it('Foundry: keystore decrypt produces correct address', async () => {
    const { createSigner } = await import('../lib/signer.js');

    const cfg = {
      env: {
        WALLET_MODE: 'foundry',
        FOUNDRY_ACCOUNT: FOUNDRY_ACCOUNT,
        KEYSTORE_PASSWORD_FILE: foundryPasswordFile,
      },
    };

    const wallet = await createSigner(cfg, null, { keystoreDir: foundryKeystoreDir });
    expect(wallet.address).toBe(foundryAddress);
  });

  // -----------------------------------------------------------------------
  // 3. Mode switching: WDK → Foundry → WDK
  // -----------------------------------------------------------------------

  it('switches from WDK to Foundry and back', async () => {
    const { createSigner } = await import('../lib/signer.js');

    // Start with WDK
    const wdkCfg = {
      env: {
        WALLET_MODE: 'wdk',
        WDK_VAULT_FILE: wdkVaultFile,
        WDK_PASSWORD_FILE: wdkPasswordFile,
      },
    };
    const w1 = await createSigner(wdkCfg, null);
    expect(w1.address).toBe(wdkAddress);

    // Switch to Foundry (simulates user changing WALLET_MODE in .env)
    const foundryCfg = {
      env: {
        WALLET_MODE: 'foundry',
        FOUNDRY_ACCOUNT: FOUNDRY_ACCOUNT,
        KEYSTORE_PASSWORD_FILE: foundryPasswordFile,
      },
    };
    const w2 = await createSigner(foundryCfg, null, { keystoreDir: foundryKeystoreDir });
    expect(w2.address).toBe(foundryAddress);
    expect(w2.address).not.toBe(w1.address); // different wallets

    // Switch back to WDK
    const w3 = await createSigner(wdkCfg, null);
    expect(w3.address).toBe(wdkAddress);
    expect(w3.address).toBe(w1.address); // same wallet as before
  });

  // -----------------------------------------------------------------------
  // 4. Config loader integration: loadConfig → createSigner
  // -----------------------------------------------------------------------

  it('loadConfig + createSigner integration for WDK', async () => {
    const { loadConfig } = await import('../lib/config.js');
    const { createSigner } = await import('../lib/signer.js');

    // Write a .env in tmpDir
    const envContent = [
      `WALLET_MODE=wdk`,
      `WDK_VAULT_FILE=${wdkVaultFile}`,
      `WDK_PASSWORD_FILE=${wdkPasswordFile}`,
    ].join('\n');
    writeFileSync(join(tmpDir, '.env'), envContent);

    const cfg = loadConfig(tmpDir);
    expect(cfg.env.WALLET_MODE).toBe('wdk');

    const wallet = await createSigner(cfg, null);
    expect(wallet.address).toBe(wdkAddress);
  });

  it('loadConfig + createSigner integration for Foundry', async () => {
    const { loadConfig } = await import('../lib/config.js');
    const { createSigner } = await import('../lib/signer.js');

    const envContent = [
      `WALLET_MODE=foundry`,
      `FOUNDRY_ACCOUNT=${FOUNDRY_ACCOUNT}`,
      `KEYSTORE_PASSWORD_FILE=${foundryPasswordFile}`,
    ].join('\n');
    writeFileSync(join(tmpDir, '.env'), envContent);

    const cfg = loadConfig(tmpDir);
    expect(cfg.env.WALLET_MODE).toBe('foundry');

    const wallet = await createSigner(cfg, null, { keystoreDir: foundryKeystoreDir });
    expect(wallet.address).toBe(foundryAddress);
  });

  // -----------------------------------------------------------------------
  // 5. Error paths
  // -----------------------------------------------------------------------

  describe('error handling', () => {
    it('throws when WALLET_MODE is missing', async () => {
      const { createSigner } = await import('../lib/signer.js');
      await expect(createSigner({ env: {} }, null)).rejects.toThrow(/WALLET_MODE not set/);
    });

    it('throws when WALLET_MODE is empty', async () => {
      const { createSigner } = await import('../lib/signer.js');
      await expect(createSigner({ env: { WALLET_MODE: '' } }, null)).rejects.toThrow(/WALLET_MODE not set/);
    });

    it('throws on unknown wallet mode', async () => {
      const { createSigner } = await import('../lib/signer.js');
      await expect(createSigner({ env: { WALLET_MODE: 'metamask' } }, null)).rejects.toThrow(/Unknown wallet_mode "metamask"/);
    });

    it('throws when WDK vault file is missing', async () => {
      const { createSigner } = await import('../lib/signer.js');
      const cfg = {
        env: {
          WALLET_MODE: 'wdk',
          WDK_VAULT_FILE: join(tmpDir, 'nonexistent_vault'),
          WDK_PASSWORD_FILE: wdkPasswordFile,
        },
      };
      await expect(createSigner(cfg, null)).rejects.toThrow(/vault not found/i);
    });

    it('throws when WDK password is wrong', async () => {
      const { createSigner } = await import('../lib/signer.js');
      const badPwFile = join(tmpDir, '.bad_password');
      writeFileSync(badPwFile, 'wrong-password-here');

      const cfg = {
        env: {
          WALLET_MODE: 'wdk',
          WDK_VAULT_FILE: wdkVaultFile,
          WDK_PASSWORD_FILE: badPwFile,
        },
      };
      await expect(createSigner(cfg, null)).rejects.toThrow(/decryption failed|wrong password/i);
    });

    it('throws when Foundry keystore is missing', async () => {
      const { createSigner } = await import('../lib/signer.js');
      const cfg = {
        env: {
          WALLET_MODE: 'foundry',
          FOUNDRY_ACCOUNT: 'nonexistent-account',
          KEYSTORE_PASSWORD_FILE: foundryPasswordFile,
        },
      };
      await expect(createSigner(cfg, null, { keystoreDir: foundryKeystoreDir })).rejects.toThrow(/keystore not found/i);
    });

    it('throws when Foundry password file is missing', async () => {
      const { createSigner } = await import('../lib/signer.js');
      const cfg = {
        env: {
          WALLET_MODE: 'foundry',
          FOUNDRY_ACCOUNT: FOUNDRY_ACCOUNT,
          KEYSTORE_PASSWORD_FILE: join(tmpDir, 'no-such-file'),
        },
      };
      await expect(createSigner(cfg, null, { keystoreDir: foundryKeystoreDir })).rejects.toThrow(/Cannot read KEYSTORE_PASSWORD_FILE/);
    });

    it('create-wallet.js rejects short password', () => {
      const shortPwFile = join(tmpDir, '.short_pw');
      writeFileSync(shortPwFile, 'short');

      expect(() => {
        execFileSync('node', [
          CREATE_WALLET,
          '--password-file', shortPwFile,
          '--vault-file', join(tmpDir, '.vault_short'),
        ], { encoding: 'utf8', cwd: SCRIPTS_DIR });
      }).toThrow();
    });

    it('create-wallet.js rejects overwrite without --force', () => {
      // wdkVaultFile already exists from beforeAll
      expect(() => {
        execFileSync('node', [
          CREATE_WALLET,
          '--password-file', wdkPasswordFile,
          '--vault-file', wdkVaultFile,
        ], { encoding: 'utf8', cwd: SCRIPTS_DIR });
      }).toThrow();
    });

    it('export-seed.js refuses to run in non-TTY environment', () => {
      const EXPORT_SEED = join(SCRIPTS_DIR, 'lib', 'export-seed.js');
      expect(() => {
        execFileSync('node', [
          EXPORT_SEED,
          '--password-file', wdkPasswordFile,
          '--vault-file', wdkVaultFile,
        ], { encoding: 'utf8', cwd: SCRIPTS_DIR });
      }).toThrow(/interactive terminal/i);
    });

    it('create-wallet.js allows overwrite with --force', () => {
      const out = execFileSync('node', [
        CREATE_WALLET,
        '--password-file', wdkPasswordFile,
        '--vault-file', wdkVaultFile,
        '--force',
      ], { encoding: 'utf8', cwd: SCRIPTS_DIR });

      const result = JSON.parse(out.trim());
      expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/);
      // Address will differ since new entropy is generated
    });
  });
});

```

### scripts/helpers.js

```javascript
/**
 * Compute Permit2 invalidateUnorderedNonces arguments from a UniswapX nonce.
 * @param {bigint} nonce
 * @returns {{ wordPos: bigint, mask: bigint }}
 */
function computeNonceComponents(nonce) {
  const wordPos = nonce >> 8n;
  const mask = 1n << (nonce & 0xFFn);
  return { wordPos, mask };
}

/**
 * Resolve expiry seconds with clamping. Returns default if input is null/undefined.
 * @param {number|null|undefined} inputSeconds
 * @param {{ defaultSeconds: number, minSeconds: number, maxSeconds: number }} limits
 * @returns {number}
 */
function resolveExpiry(inputSeconds, limits) {
  if (inputSeconds == null) return limits.defaultSeconds;
  return Math.max(limits.minSeconds, Math.min(limits.maxSeconds, inputSeconds));
}

/**
 * Check that an amount string does not exceed maxDecimals decimal places.
 * @param {string} amount - must be a decimal string (not scientific notation)
 * @param {number} maxDecimals
 * @returns {boolean}
 */
function checkPrecision(amount, maxDecimals) {
  const s = String(amount);
  const dotIndex = s.indexOf('.');
  if (dotIndex === -1) return true;
  return s.length - dotIndex - 1 <= maxDecimals;
}

export { computeNonceComponents, resolveExpiry, checkPrecision };

```

### scripts/lib/__tests__/config.test.js

```javascript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { loadConfig, resolveToken } from '../config.js';

let testDir;

beforeEach(() => {
  testDir = join(tmpdir(), `config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
  mkdirSync(testDir, { recursive: true });
});

afterEach(() => {
  rmSync(testDir, { recursive: true, force: true });
});

describe('loadConfig', () => {
  it('parses .env file correctly (key=value, ignores comments and blanks)', () => {
    const envContent = [
      '# This is a comment',
      '',
      'PRIVATE_KEY=0xdeadbeef',
      '  ',
      'RPC_URL=https://mainnet.example.com',
      '# another comment',
      'QUOTED_VAL="hello world"',
      "SINGLE_QUOTED='foo bar'",
    ].join('\n');

    writeFileSync(join(testDir, '.env'), envContent);

    const config = loadConfig(testDir);

    expect(config.env.PRIVATE_KEY).toBe('0xdeadbeef');
    expect(config.env.RPC_URL).toBe('https://mainnet.example.com');
    expect(config.env.QUOTED_VAL).toBe('hello world');
    expect(config.env.SINGLE_QUOTED).toBe('foo bar');
    expect(Object.keys(config.env)).not.toContain('');
  });

  it('resolves token symbol to address and decimals via resolveToken', () => {
    const yamlContent = [
      'tokens:',
      '  USDT:',
      '    address: "0xdAC17F958D2ee523a2206206994597C13D831ec7"',
      '    decimals: 6',
      '  XAUT:',
      '    address: "0x68749665FF8D2d112Fa859AA293F07A622782F38"',
      '    decimals: 6',
    ].join('\n');

    writeFileSync(join(testDir, 'config.yaml'), yamlContent);

    const config = loadConfig(testDir);
    const token = resolveToken(config, 'USDT');

    expect(token.address).toBe('0xdAC17F958D2ee523a2206206994597C13D831ec7');
    expect(token.decimals).toBe(6);
  });

  it('throws on unknown token symbol', () => {
    const yamlContent = [
      'tokens:',
      '  USDT:',
      '    address: "0xdAC17F958D2ee523a2206206994597C13D831ec7"',
      '    decimals: 6',
    ].join('\n');

    writeFileSync(join(testDir, 'config.yaml'), yamlContent);

    const config = loadConfig(testDir);

    expect(() => resolveToken(config, 'UNKNOWN')).toThrow(/unknown token/i);
  });

  it('returns undefined wallet_mode when not set in yaml', () => {
    const yamlContent = [
      'tokens:',
      '  USDT:',
      '    address: "0xdAC17F958D2ee523a2206206994597C13D831ec7"',
      '    decimals: 6',
    ].join('\n');

    writeFileSync(join(testDir, 'config.yaml'), yamlContent);

    const config = loadConfig(testDir);

    expect(config.yaml.wallet_mode).toBeUndefined();
  });

  it('silently returns empty objects when files do not exist', () => {
    // testDir exists but has no .env or config.yaml
    const config = loadConfig(testDir);

    expect(config.env).toEqual({});
    expect(config.yaml).toEqual({});
    expect(config.configDir).toBe(testDir);
  });
});

```

### scripts/lib/__tests__/create-wallet.test.js

```javascript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, rmSync, writeFileSync, existsSync, statSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { execFileSync } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

// sodium-native and bip39-mnemonic are CJS; load via createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const CREATE_WALLET_SCRIPT = join(__dirname, '..', 'create-wallet.js');

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function runCreateWallet(args, { expectSuccess = true } = {}) {
  try {
    const output = execFileSync(process.execPath, [CREATE_WALLET_SCRIPT, ...args], {
      encoding: 'utf8',
      env: { ...process.env },
    });
    if (!expectSuccess) {
      throw new Error(`Expected failure but succeeded with output: ${output}`);
    }
    return { stdout: output, exitCode: 0 };
  } catch (err) {
    if (expectSuccess) {
      throw err;
    }
    return {
      stdout: err.stdout || '',
      stderr: err.stderr || '',
      exitCode: err.status || 1,
    };
  }
}

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

let testDir;

beforeEach(() => {
  testDir = join(
    tmpdir(),
    `create-wallet-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
  );
  mkdirSync(testDir, { recursive: true });
});

afterEach(() => {
  rmSync(testDir, { recursive: true, force: true });
});

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('create-wallet.js', () => {
  it('creates vault file and outputs valid Ethereum address', () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    writeFileSync(passwordFile, 'my-strong-password-123');

    const { stdout } = runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
    ]);

    const result = JSON.parse(stdout);
    expect(result).toHaveProperty('address');
    expect(result).toHaveProperty('vaultFile');
    expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/);
    expect(result.vaultFile).toBe(vaultFile);
    expect(existsSync(vaultFile)).toBe(true);
  });

  it('vault file has correct JSON structure', () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    writeFileSync(passwordFile, 'my-strong-password-123');

    runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
    ]);

    const vault = JSON.parse(readFileSync(vaultFile, 'utf8'));
    expect(vault).toHaveProperty('encryptedEntropy');
    expect(vault).toHaveProperty('salt');
    expect(typeof vault.encryptedEntropy).toBe('string');
    expect(typeof vault.salt).toBe('string');
    // salt should be 16 bytes = 32 hex chars
    expect(vault.salt).toHaveLength(32);
  });

  it('errors if vault already exists without --force', () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    writeFileSync(passwordFile, 'my-strong-password-123');
    writeFileSync(vaultFile, '{}'); // pre-existing vault

    const result = runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
    ], { expectSuccess: false });

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toMatch(/already exists/i);
  });

  it('overwrites existing vault with --force', () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    writeFileSync(passwordFile, 'my-strong-password-123');
    writeFileSync(vaultFile, '{}'); // pre-existing vault

    const { stdout } = runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
      '--force',
    ]);

    const result = JSON.parse(stdout);
    expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/);
  });

  it('errors if password is too short (< 12 characters)', () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    writeFileSync(passwordFile, 'short');

    const result = runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
    ], { expectSuccess: false });

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toMatch(/password/i);
  });

  it('vault is decryptable by signer.js (round-trip test)', async () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    const password = 'my-strong-password-123';
    writeFileSync(passwordFile, password);

    const { stdout } = runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
    ]);

    const { address: createdAddress } = JSON.parse(stdout);

    // Now use signer.js to decrypt the vault and verify we get the same address
    const { createSigner } = await import('../signer.js');
    const cfg = {
      env: {
        WALLET_MODE: 'wdk',
        WDK_VAULT_FILE: vaultFile,
        WDK_PASSWORD_FILE: passwordFile,
      },
      yaml: {},
      configDir: testDir,
    };

    const wallet = await createSigner(cfg, null);
    expect(wallet.address.toLowerCase()).toBe(createdAddress.toLowerCase());
  });

  it('sets vault file permissions to 600', () => {
    const passwordFile = join(testDir, '.wdk_password');
    const vaultFile = join(testDir, '.wdk_vault');
    writeFileSync(passwordFile, 'my-strong-password-123');

    runCreateWallet([
      '--password-file', passwordFile,
      '--vault-file', vaultFile,
    ]);

    const stat = statSync(vaultFile);
    // mode & 0o777 should be 0o600
    expect(stat.mode & 0o777).toBe(0o600);
  });
});

```

### scripts/lib/__tests__/erc20.test.js

```javascript
import { describe, it, expect, vi } from 'vitest';
import { getBalance, getAllowance, approve } from '../erc20.js';

function mockProvider(returnValue) {
  return { call: vi.fn().mockResolvedValue(returnValue) };
}

function mockSigner(address = '0x' + '1'.repeat(40)) {
  return {
    getAddress: vi.fn().mockResolvedValue(address),
    sendTransaction: vi.fn().mockResolvedValue({
      hash: '0xabc',
      wait: vi.fn().mockResolvedValue({ status: 1 }),
    }),
    provider: mockProvider('0x' + '0'.repeat(64)),
  };
}

const TOKEN_USDT = { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 };
const TOKEN_XAUT = { address: '0x68749665FF8D2d112Fa859AA293F07A622782F38', decimals: 6 };

describe('getBalance', () => {
  it('calls provider.call and formats result with token decimals', async () => {
    // 1000000000 raw / 10^6 = 1000.0
    const raw = '0x' + (1000000000n).toString(16).padStart(64, '0');
    const provider = mockProvider(raw);

    const result = await getBalance(TOKEN_USDT, '0x' + '2'.repeat(40), provider);

    expect(provider.call).toHaveBeenCalledOnce();
    const callArg = provider.call.mock.calls[0][0];
    expect(callArg.to).toBe(TOKEN_USDT.address);
    expect(typeof callArg.data).toBe('string');
    expect(result).toBe('1000.0');
  });

  it('returns "0.0" when balance is zero', async () => {
    const provider = mockProvider('0x' + '0'.repeat(64));
    const result = await getBalance(TOKEN_XAUT, '0x' + '3'.repeat(40), provider);
    expect(result).toBe('0.0');
  });
});

describe('getAllowance', () => {
  it('returns formatted allowance value', async () => {
    // 500000 raw / 10^6 = 0.5
    const raw = '0x' + (500000n).toString(16).padStart(64, '0');
    const provider = mockProvider(raw);
    const owner = '0x' + '2'.repeat(40);
    const spender = '0x' + '3'.repeat(40);

    const result = await getAllowance(TOKEN_USDT, owner, spender, provider);

    expect(provider.call).toHaveBeenCalledOnce();
    const callArg = provider.call.mock.calls[0][0];
    expect(callArg.to).toBe(TOKEN_USDT.address);
    expect(result).toBe('0.5');
  });
});

describe('approve', () => {
  it('sends one transaction when requiresResetApprove is not set', async () => {
    const signer = mockSigner();
    const spender = '0x' + '3'.repeat(40);

    const result = await approve(TOKEN_USDT, spender, '1000', signer);

    expect(signer.sendTransaction).toHaveBeenCalledOnce();
    const txArg = signer.sendTransaction.mock.calls[0][0];
    expect(txArg.to).toBe(TOKEN_USDT.address);
    expect(typeof txArg.data).toBe('string');
    expect(result).toEqual({ hash: '0xabc' });
  });

  it('sends two transactions (reset + approve) when requiresResetApprove is true', async () => {
    const signer = mockSigner();
    const spender = '0x' + '3'.repeat(40);

    const result = await approve(TOKEN_USDT, spender, '1000', signer, { requiresResetApprove: true });

    expect(signer.sendTransaction).toHaveBeenCalledTimes(2);

    // First call: approve(spender, 0)
    const resetTxArg = signer.sendTransaction.mock.calls[0][0];
    expect(resetTxArg.to).toBe(TOKEN_USDT.address);

    // Second call: approve(spender, amount)
    const approveTxArg = signer.sendTransaction.mock.calls[1][0];
    expect(approveTxArg.to).toBe(TOKEN_USDT.address);

    expect(result).toEqual({ hash: '0xabc' });
  });
});

```

### scripts/lib/__tests__/provider.test.js

```javascript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { FallbackProvider, createProvider } from '../provider.js';

describe('FallbackProvider', () => {
  it('uses primary URL by default', async () => {
    const provider = new FallbackProvider('https://primary.example.com');
    provider._rawSend = vi.fn().mockResolvedValue('0x1');

    const result = await provider.send('eth_blockNumber', []);

    expect(result).toBe('0x1');
    expect(provider._rawSend).toHaveBeenCalledWith(
      'https://primary.example.com',
      'eth_blockNumber',
      []
    );
  });

  it('throws when no primary URL is provided', () => {
    expect(() => new FallbackProvider()).toThrow(/primary/i);
    expect(() => new FallbackProvider('')).toThrow(/primary/i);
  });

  it('falls back on retriable error codes (429, 502, 503)', async () => {
    const provider = new FallbackProvider('https://primary.example.com', [
      'https://fallback1.example.com',
    ]);

    let callCount = 0;
    provider._rawSend = vi.fn().mockImplementation(async (url) => {
      callCount++;
      if (url === 'https://primary.example.com') {
        const err = new Error('Too Many Requests');
        err.status = 429;
        throw err;
      }
      return '0x2';
    });

    const result = await provider.send('eth_blockNumber', []);

    expect(result).toBe('0x2');
    expect(provider._rawSend).toHaveBeenCalledTimes(2);
    expect(provider._rawSend).toHaveBeenNthCalledWith(
      1,
      'https://primary.example.com',
      'eth_blockNumber',
      []
    );
    expect(provider._rawSend).toHaveBeenNthCalledWith(
      2,
      'https://fallback1.example.com',
      'eth_blockNumber',
      []
    );
  });

  it('is session-sticky: after fallback success, primary URL updates', async () => {
    const provider = new FallbackProvider('https://primary.example.com', [
      'https://fallback1.example.com',
    ]);

    provider._rawSend = vi.fn().mockImplementation(async (url) => {
      if (url === 'https://primary.example.com') {
        const err = new Error('Service Unavailable');
        err.status = 503;
        throw err;
      }
      return '0x3';
    });

    // First call triggers fallback
    await provider.send('eth_blockNumber', []);

    // Reset mock to track next call
    provider._rawSend.mockClear();
    provider._rawSend.mockResolvedValue('0x4');

    // Second call should use the new primary (fallback1)
    await provider.send('eth_blockNumber', []);

    expect(provider._rawSend).toHaveBeenCalledWith(
      'https://fallback1.example.com',
      'eth_blockNumber',
      []
    );
  });

  it('throws after all URLs are exhausted', async () => {
    const provider = new FallbackProvider('https://primary.example.com', [
      'https://fallback1.example.com',
      'https://fallback2.example.com',
    ]);

    provider._rawSend = vi.fn().mockImplementation(async (url) => {
      const err = new Error('Bad Gateway');
      err.status = 502;
      throw err;
    });

    await expect(provider.send('eth_blockNumber', [])).rejects.toThrow();
  });

  it('call() routes through _sendWithFallback with eth_call', async () => {
    const provider = new FallbackProvider('https://primary.example.com');
    provider._rawSend = vi.fn().mockResolvedValue('0xresult');

    const tx = { to: '0xabc', data: '0x1234' };
    const result = await provider.call(tx);

    expect(result).toBe('0xresult');
    expect(provider._rawSend).toHaveBeenCalledWith(
      'https://primary.example.com',
      'eth_call',
      [tx, 'latest']
    );
  });

  it('getBalance() routes through _sendWithFallback with eth_getBalance', async () => {
    const provider = new FallbackProvider('https://primary.example.com');
    provider._rawSend = vi.fn().mockResolvedValue('0xde0b6b3a7640000');

    const address = '0xabcdef1234567890abcdef1234567890abcdef12';
    const result = await provider.getBalance(address);

    expect(result).toBe('0xde0b6b3a7640000');
    expect(provider._rawSend).toHaveBeenCalledWith(
      'https://primary.example.com',
      'eth_getBalance',
      [address, 'latest']
    );
  });

  it('getBlockNumber() routes through _sendWithFallback', async () => {
    const provider = new FallbackProvider('https://primary.example.com');
    provider._rawSend = vi.fn().mockResolvedValue('0x10d4f');

    const result = await provider.getBlockNumber();

    expect(result).toBe('0x10d4f');
    expect(provider._rawSend).toHaveBeenCalledWith(
      'https://primary.example.com',
      'eth_blockNumber',
      []
    );
  });

  it('falls back on timeout and connection-refused errors', async () => {
    const provider = new FallbackProvider('https://primary.example.com', [
      'https://fallback1.example.com',
    ]);

    provider._rawSend = vi.fn().mockImplementation(async (url) => {
      if (url === 'https://primary.example.com') {
        const err = new Error('Connection refused');
        err.code = 'ECONNREFUSED';
        throw err;
      }
      return '0x5';
    });

    const result = await provider.send('eth_blockNumber', []);
    expect(result).toBe('0x5');
  });

  it('times out individual RPC requests and retries fallback', async () => {
    const provider = new FallbackProvider(
      'https://primary.example.com',
      ['https://fallback1.example.com'],
      5
    );

    provider._rawSend = vi.fn().mockImplementation(async (url) => {
      if (url === 'https://primary.example.com') {
        const err = new Error('RPC request timeout after 5ms');
        err.code = 'ETIMEDOUT';
        throw err;
      }
      return '0x6';
    });

    const result = await provider.send('eth_blockNumber', []);
    expect(result).toBe('0x6');
    expect(provider._rawSend).toHaveBeenCalledTimes(2);
  });

  it('does not fall back on non-retriable errors', async () => {
    const provider = new FallbackProvider('https://primary.example.com', [
      'https://fallback1.example.com',
    ]);

    provider._rawSend = vi.fn().mockImplementation(async () => {
      const err = new Error('Invalid params');
      err.code = -32602;
      throw err;
    });

    await expect(provider.send('eth_call', [{}])).rejects.toThrow('Invalid params');
    // Should only have tried primary, not fallback
    expect(provider._rawSend).toHaveBeenCalledTimes(1);
  });

  it('getEthersProvider() returns underlying JsonRpcProvider', () => {
    const provider = new FallbackProvider('https://primary.example.com');
    const ethersProvider = provider.getEthersProvider();
    // Should be an object with expected ethers Provider interface
    expect(ethersProvider).toBeDefined();
    expect(typeof ethersProvider.getBlockNumber).toBe('function');
  });
});

describe('createProvider', () => {
  it('creates provider from env with primary and fallbacks', () => {
    const env = {
      ETH_RPC_URL: 'https://primary.example.com',
      ETH_RPC_URL_FALLBACK: 'https://fb1.example.com,https://fb2.example.com',
    };

    const provider = createProvider(env);
    expect(provider).toBeInstanceOf(FallbackProvider);
  });

  it('creates provider with only primary when no fallback env set', () => {
    const env = { ETH_RPC_URL: 'https://primary.example.com' };
    const provider = createProvider(env);
    expect(provider).toBeInstanceOf(FallbackProvider);
  });

  it('throws when ETH_RPC_URL is missing from env', () => {
    const originalHome = process.env.HOME;
    process.env.HOME = mkdtempSync(join(tmpdir(), 'xaut-provider-test-'));
    try {
      expect(() => createProvider({})).toThrow(/ETH_RPC_URL/i);
    } finally {
      process.env.HOME = originalHome;
    }
  });
});

```

### scripts/lib/__tests__/signer.test.js

```javascript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { pbkdf2Sync } from 'crypto';
import { BaseWallet, HDNodeWallet, Wallet } from 'ethers6';

import { createSigner } from '../signer.js';

// sodium-native and bip39-mnemonic are CJS; load via createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const sodium = require('sodium-native');
const b4a = require('b4a');
const bip39 = require('bip39-mnemonic');

// ---------------------------------------------------------------------------
// Helpers to create test fixtures
// ---------------------------------------------------------------------------

/**
 * Encrypt a buffer using the same algorithm as WdkSecretManager.
 * (Replicates WdkSecretManager#encrypt — see signer.js for details.)
 */
function wdkEncrypt(buffer, key) {
  const NONCEBYTES = sodium.crypto_secretbox_NONCEBYTES;
  const MACBYTES = sodium.crypto_secretbox_MACBYTES;
  const buffLength = buffer.byteLength;

  const nonce = b4a.alloc(NONCEBYTES);
  sodium.randombytes_buf(nonce);

  const payload = b4a.alloc(1 + NONCEBYTES + 1 + buffLength + MACBYTES);
  payload[0] = 0; // version
  payload.set(nonce, 1);

  const cipher = payload.subarray(1 + nonce.byteLength);
  const plain = cipher.subarray(0, cipher.byteLength - MACBYTES);
  plain[0] = buffLength;
  plain.set(buffer, 1);

  sodium.crypto_secretbox_easy(cipher, plain, nonce, key);

  return payload;
}

/**
 * Create a minimal WDK vault JSON string for a given password + mnemonic.
 */
function createWdkVault(password, mnemonic) {
  const salt = b4a.alloc(16);
  sodium.randombytes_buf(salt);

  const key = pbkdf2Sync(password, salt, 100_000, 32, 'sha256');

  // Convert mnemonic back to entropy
  const entropy = Buffer.from(bip39.mnemonicToEntropy(mnemonic), 'hex');
  const encryptedEntropy = wdkEncrypt(entropy, key);

  return JSON.stringify({
    encryptedEntropy: encryptedEntropy.toString('hex'),
    salt: salt.toString('hex'),
  });
}

// ---------------------------------------------------------------------------
// Test fixtures
// ---------------------------------------------------------------------------

let testDir;

beforeEach(() => {
  testDir = join(
    tmpdir(),
    `signer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
  );
  mkdirSync(testDir, { recursive: true });
});

afterEach(() => {
  rmSync(testDir, { recursive: true, force: true });
});

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('createSigner', () => {
  it('throws when WALLET_MODE is not set in .env', async () => {
    const cfg = { env: {}, yaml: {}, configDir: testDir };

    await expect(createSigner(cfg, null)).rejects.toThrow(
      /WALLET_MODE not set in \.env/i,
    );
  });

  it('throws for unknown wallet_mode', async () => {
    const cfg = { env: { WALLET_MODE: 'ledger' }, yaml: {}, configDir: testDir };

    await expect(createSigner(cfg, null)).rejects.toThrow(
      /unknown wallet_mode/i,
    );
  });

  describe('foundry mode', () => {
    it('creates a wallet from a Foundry keystore', async () => {
      // Create a random wallet and encrypt its keystore
      const originalWallet = Wallet.createRandom();
      const password = 'test-password-foundry';
      const keystoreJson = await originalWallet.encrypt(password);

      // Write keystore to temp dir using account name
      const accountName = 'test-account';
      const keystoreDir = join(testDir, 'foundry-keystores');
      mkdirSync(keystoreDir, { recursive: true });
      writeFileSync(join(keystoreDir, accountName), keystoreJson);

      // Write password file
      const passwordFile = join(testDir, '.keystore-password');
      writeFileSync(passwordFile, password);

      const cfg = {
        env: {
          WALLET_MODE: 'foundry',
          FOUNDRY_ACCOUNT: accountName,
          KEYSTORE_PASSWORD_FILE: passwordFile,
        },
        yaml: {},
        configDir: testDir,
      };

      const wallet = await createSigner(cfg, null, { keystoreDir });

      // ethers v6 returns HDNodeWallet (a BaseWallet subclass) from both
      // fromEncryptedJson and fromPhrase
      expect(wallet).toBeInstanceOf(BaseWallet);
      expect(wallet.address.toLowerCase()).toBe(
        originalWallet.address.toLowerCase(),
      );
    });

    it('throws when FOUNDRY_ACCOUNT is missing', async () => {
      const cfg = {
        env: { WALLET_MODE: 'foundry' },
        yaml: {},
        configDir: testDir,
      };

      await expect(createSigner(cfg, null)).rejects.toThrow(
        /FOUNDRY_ACCOUNT not set/i,
      );
    });

    it('throws when keystore file does not exist', async () => {
      const passwordFile = join(testDir, '.pass');
      writeFileSync(passwordFile, 'password');

      const cfg = {
        env: {
          WALLET_MODE: 'foundry',
          FOUNDRY_ACCOUNT: 'nonexistent-account',
          KEYSTORE_PASSWORD_FILE: passwordFile,
        },
        yaml: {},
        configDir: testDir,
      };

      await expect(
        createSigner(cfg, null, { keystoreDir: testDir }),
      ).rejects.toThrow(/keystore not found/i);
    });
  });

  describe('wdk mode', () => {
    it('creates a wallet from a WDK encrypted vault', async () => {
      // Create a deterministic mnemonic via a random wallet
      const originalWallet = Wallet.createRandom();
      const mnemonic = originalWallet.mnemonic.phrase;

      const password = 'test-password-wdk';
      const vaultJson = createWdkVault(password, mnemonic);

      // Write vault and password files
      const vaultFile = join(testDir, '.wdk_vault');
      const passwordFile = join(testDir, '.wdk_password');
      writeFileSync(vaultFile, vaultJson);
      writeFileSync(passwordFile, password);

      const cfg = {
        env: {
          WALLET_MODE: 'wdk',
          WDK_VAULT_FILE: vaultFile,
          WDK_PASSWORD_FILE: passwordFile,
        },
        yaml: {},
        configDir: testDir,
      };

      const wallet = await createSigner(cfg, null);

      expect(wallet).toBeInstanceOf(BaseWallet);
      expect(wallet.address.toLowerCase()).toBe(
        originalWallet.address.toLowerCase(),
      );
    });

    it('throws when vault file does not exist', async () => {
      const passwordFile = join(testDir, '.wdk_password');
      writeFileSync(passwordFile, 'password');

      const cfg = {
        env: {
          WALLET_MODE: 'wdk',
          WDK_VAULT_FILE: join(testDir, 'nonexistent.vault'),
          WDK_PASSWORD_FILE: passwordFile,
        },
        yaml: {},
        configDir: testDir,
      };

      await expect(createSigner(cfg, null)).rejects.toThrow(
        /wdk vault not found/i,
      );
    });

    it('throws when decryption fails (wrong password)', async () => {
      const originalWallet = Wallet.createRandom();
      const mnemonic = originalWallet.mnemonic.phrase;

      // Encrypt with one password, decrypt with another
      const vaultJson = createWdkVault('correct-password', mnemonic);

      const vaultFile = join(testDir, '.wdk_vault');
      const passwordFile = join(testDir, '.wdk_password');
      writeFileSync(vaultFile, vaultJson);
      writeFileSync(passwordFile, 'wrong-password');

      const cfg = {
        env: {
          WALLET_MODE: 'wdk',
          WDK_VAULT_FILE: vaultFile,
          WDK_PASSWORD_FILE: passwordFile,
        },
        yaml: {},
        configDir: testDir,
      };

      await expect(createSigner(cfg, null)).rejects.toThrow(
        /decryption failed/i,
      );
    });
  });
});

```

### scripts/lib/__tests__/uniswap.test.js

```javascript
import { describe, it, expect, vi } from 'vitest';
import { AbiCoder } from 'ethers6';
import { quote, buildSwap } from '../uniswap.js';

// QuoterV2 returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)
function encodeQuoterResponse({ amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate }) {
  const coder = AbiCoder.defaultAbiCoder();
  return coder.encode(
    ['uint256', 'uint160', 'uint32', 'uint256'],
    [amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate],
  );
}

const TOKEN_WETH = { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18 };
const TOKEN_XAUT = { address: '0x68749665FF8D2d112Fa859AA293F07A622782F38', decimals: 6 };

const CONTRACTS = {
  quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
  router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
};

describe('quote', () => {
  it('returns amountOut, amountOutRaw, sqrtPriceX96, and gasEstimate from QuoterV2', async () => {
    // 1 XAUT (6 decimals) = 1_000_000 raw
    const rawAmountOut = 1_000_000n;
    const rawSqrtPrice = 79228162514264337593543950336n;
    const rawGasEstimate = 150_000n;

    const encodedResponse = encodeQuoterResponse({
      amountOut: rawAmountOut,
      sqrtPriceX96After: rawSqrtPrice,
      initializedTicksCrossed: 1,
      gasEstimate: rawGasEstimate,
    });

    const provider = { call: vi.fn().mockResolvedValue(encodedResponse) };

    const result = await quote({
      tokenIn: TOKEN_WETH,
      tokenOut: TOKEN_XAUT,
      amountIn: '1',
      fee: 3000,
      contracts: CONTRACTS,
      provider,
    });

    // provider.call should have been called once with quoter address
    expect(provider.call).toHaveBeenCalledOnce();
    const callArg = provider.call.mock.calls[0][0];
    expect(callArg.to).toBe(CONTRACTS.quoter);
    expect(typeof callArg.data).toBe('string');
    expect(callArg.data.startsWith('0x')).toBe(true);

    // Result shape
    expect(result.amountOutRaw).toBe(rawAmountOut);
    expect(result.amountOut).toBe('1.0'); // 1_000_000 / 10^6
    expect(result.sqrtPriceX96).toBe(rawSqrtPrice);
    expect(result.gasEstimate).toBe(rawGasEstimate);
  });
});

describe('buildSwap', () => {
  it('returns tx params with to, data, and value fields', () => {
    const recipient = '0x' + '1'.repeat(40);
    const deadline = Math.floor(Date.now() / 1000) + 600;

    const result = buildSwap({
      tokenIn: TOKEN_WETH,
      tokenOut: TOKEN_XAUT,
      amountIn: '1',
      minAmountOut: '0.99',
      fee: 3000,
      recipient,
      deadline,
      contracts: CONTRACTS,
    });

    expect(result.to).toBe(CONTRACTS.router);
    expect(typeof result.data).toBe('string');
    expect(result.data.startsWith('0x')).toBe(true);
    expect(result.data.length).toBeGreaterThan(10);
    // value should be a bigint (0 for ERC-20 → ERC-20 swaps)
    expect(typeof result.value).toBe('bigint');
  });

  it('sets value to amountIn in wei for ETH input', () => {
    const TOKEN_ETH = { address: '0x0000000000000000000000000000000000000000', decimals: 18 };
    const recipient = '0x' + '1'.repeat(40);
    const deadline = Math.floor(Date.now() / 1000) + 600;

    const result = buildSwap({
      tokenIn: TOKEN_ETH,
      tokenOut: TOKEN_XAUT,
      amountIn: '1',
      minAmountOut: '0.99',
      fee: 3000,
      recipient,
      deadline,
      contracts: CONTRACTS,
    });

    expect(result.to).toBe(CONTRACTS.router);
    expect(result.value).toBe(1_000_000_000_000_000_000n); // 1 ETH in wei
  });
});

```

### scripts/lib/config.js

```javascript
import { readFileSync } from 'fs';
import { join } from 'path';
import yaml from 'js-yaml';
import { getAddress } from 'ethers6';

/**
 * Parse a .env file content into a plain object.
 * - Lines starting with # (after optional whitespace) are ignored.
 * - Blank / whitespace-only lines are ignored.
 * - Values may be surrounded by single or double quotes, which are stripped.
 */
function parseEnv(content) {
  const result = {};
  for (const raw of content.split('\n')) {
    const line = raw.trim();
    if (!line || line.startsWith('#')) continue;

    const eqIndex = line.indexOf('=');
    if (eqIndex === -1) continue;

    const key = line.slice(0, eqIndex).trim();
    let value = line.slice(eqIndex + 1).trim();

    // Strip surrounding quotes (single or double)
    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1);
    }

    if (key) {
      result[key] = value;
    }
  }
  return result;
}

/**
 * Load configuration from configDir.
 *
 * Reads:
 *   <configDir>/.env       — environment variables (key=value pairs)
 *   <configDir>/config.yaml — structured YAML config
 *
 * Both files are optional; missing files are silently treated as empty.
 *
 * @param {string} configDir  Path to the config directory (defaults to ~/.aurehub)
 * @returns {{ env: object, yaml: object, configDir: string }}
 */
export function loadConfig(configDir) {
  // Default to ~/.aurehub when no directory is supplied
  const dir = configDir ?? join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.aurehub');

  let env = {};
  try {
    const raw = readFileSync(join(dir, '.env'), 'utf8');
    env = parseEnv(raw);
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }

  let yamlConfig = {};
  try {
    const raw = readFileSync(join(dir, 'config.yaml'), 'utf8');
    yamlConfig = yaml.load(raw, { schema: yaml.JSON_SCHEMA }) ?? {};
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }

  return { env, yaml: yamlConfig, configDir: dir };
}

/**
 * Resolve a token symbol to its address and decimals from config.yaml's `tokens` section.
 *
 * @param {{ yaml: object }} config  Config object returned by loadConfig
 * @param {string} symbol            Token symbol, e.g. "USDT"
 * @returns {{ address: string, decimals: number }}
 * @throws {Error} If the symbol is not found in the tokens section
 */
// Canonical mainnet addresses for tamper detection
const CANONICAL_TOKENS = {
  XAUT: '0x68749665FF8D2d112Fa859AA293F07A622782F38',
  USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
};
const CANONICAL_CONTRACTS = {
  router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
  quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
  permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3',
};

export function resolveToken(config, symbol) {
  const tokens = config?.yaml?.tokens ?? {};
  const token = tokens[symbol];
  if (!token) {
    throw new Error(`Unknown token symbol: "${symbol}"`);
  }
  // Validate address against canonical value if known
  const canonical = CANONICAL_TOKENS[symbol];
  if (canonical && token.address.toLowerCase() !== canonical.toLowerCase()) {
    throw new Error(`Token ${symbol} address mismatch: config has ${token.address}, expected ${canonical}. Check config.yaml for tampering.`);
  }
  // Validate decimals
  if (typeof token.decimals !== 'number' || !Number.isInteger(token.decimals) || token.decimals < 0 || token.decimals > 18) {
    throw new Error(`Token ${symbol} has invalid decimals: ${token.decimals}`);
  }
  // Normalize to EIP-55 checksum to tolerate old config files with non-standard casing.
  return { address: getAddress(token.address.toLowerCase()), decimals: token.decimals };
}

export function validateContracts(config) {
  const contracts = config?.yaml?.contracts ?? {};
  for (const [name, canonical] of Object.entries(CANONICAL_CONTRACTS)) {
    if (contracts[name] && contracts[name].toLowerCase() !== canonical.toLowerCase()) {
      throw new Error(`Contract ${name} address mismatch: config has ${contracts[name]}, expected ${canonical}. Check config.yaml for tampering.`);
    }
    // Normalize to EIP-55 checksum to tolerate old config files with non-standard casing.
    if (contracts[name]) {
      contracts[name] = getAddress(contracts[name].toLowerCase());
    }
  }
}

```

### scripts/lib/create-wallet.js

```javascript
#!/usr/bin/env node
/**
 * create-wallet.js — CLI script to create an encrypted WDK vault file.
 *
 * Usage:
 *   node lib/create-wallet.js --password-file <path> [--vault-file <path>] [--force]
 *
 * Outputs JSON to stdout: { address, vaultFile }
 *
 * The vault format is compatible with signer.js / WdkSecretManager:
 *   { encryptedEntropy: <hex>, salt: <hex> }
 *
 * encryptedEntropy hex encodes: [version=0][nonce(24b)][secretbox_easy(plain, nonce, key)]
 *   where plain = [length_byte, ...entropy_bytes]
 */

import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';

/** Expand leading ~ to the user's home directory. */
function expandTilde(p) {
  if (typeof p === 'string' && p.startsWith('~/')) {
    return join(homedir(), p.slice(2));
  }
  return p;
}
import { randomBytes, pbkdf2Sync } from 'crypto';
import { Wallet } from 'ethers6';

// sodium-native and bip39-mnemonic are CJS; load via createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const sodium = require('sodium-native');
const b4a = require('b4a');
const bip39 = require('bip39-mnemonic');

// ---------------------------------------------------------------------------
// CLI argument parsing
// ---------------------------------------------------------------------------

function parseArgs(argv) {
  const args = argv.slice(2);
  const opts = {
    passwordFile: null,
    vaultFile: join(homedir(), '.aurehub', '.wdk_vault'),
    force: false,
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--password-file') {
      opts.passwordFile = expandTilde(args[++i]);
    } else if (arg === '--vault-file') {
      opts.vaultFile = expandTilde(args[++i]);
    } else if (arg === '--force') {
      opts.force = true;
    } else {
      fatal(`Unknown argument: ${arg}`);
    }
  }

  if (!opts.passwordFile) {
    fatal('--password-file is required');
  }

  return opts;
}

// ---------------------------------------------------------------------------
// Error helpers
// ---------------------------------------------------------------------------

function fatal(msg) {
  process.stderr.write(`Error: ${msg}\n`);
  process.exit(1);
}

// ---------------------------------------------------------------------------
// WDK vault encryption
//
// Matches WdkSecretManager#encrypt() exactly so signer.js can decrypt.
//
// Encryption layout (encryptedEntropy hex):
//   byte 0        : version = 0
//   bytes 1..24   : nonce (crypto_secretbox_NONCEBYTES = 24)
//   bytes 25..end : ciphertext from crypto_secretbox_easy
//                     plain = [length_byte(1), ...entropy]
// ---------------------------------------------------------------------------

function wdkEncrypt(entropy, key) {
  const NONCEBYTES = sodium.crypto_secretbox_NONCEBYTES; // 24
  const MACBYTES = sodium.crypto_secretbox_MACBYTES;     // 16
  const buffLength = entropy.byteLength;

  const nonce = b4a.alloc(NONCEBYTES);
  sodium.randombytes_buf(nonce);

  // Total payload: 1 (version) + 24 (nonce) + 1 (length) + buffLength + 16 (mac)
  const payload = b4a.alloc(1 + NONCEBYTES + 1 + buffLength + MACBYTES);
  payload[0] = 0; // version
  payload.set(nonce, 1);

  // cipher occupies the rest: 1 + buffLength + MACBYTES bytes
  const cipher = payload.subarray(1 + NONCEBYTES);
  // plain is cipher minus MAC trailer
  const plain = cipher.subarray(0, cipher.byteLength - MACBYTES);
  plain[0] = buffLength;
  plain.set(entropy, 1);

  sodium.crypto_secretbox_easy(cipher, plain, nonce, key);

  return payload;
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
  const opts = parseArgs(process.argv);

  // 1. Read password
  let password;
  try {
    password = readFileSync(opts.passwordFile, 'utf8').trim();
  } catch (err) {
    fatal(`Cannot read password file "${opts.passwordFile}": ${err.message}`);
  }

  // 2. Validate password length
  if (password.length < 12) {
    fatal('Password must be at least 12 characters long');
  }

  // 3. Check vault file existence
  if (existsSync(opts.vaultFile) && !opts.force) {
    fatal(`Vault file already exists at "${opts.vaultFile}". Use --force to overwrite.`);
  }

  // 4. Generate 16 bytes random entropy (BIP-39 seed)
  const entropy = randomBytes(16);

  // 5. Generate 16-byte random salt for PBKDF2 key derivation (matches WDK source)
  const salt = randomBytes(16);

  // 6. Derive 32-byte key via PBKDF2-SHA256
  const key = pbkdf2Sync(password, salt, 100_000, 32, 'sha256');

  // 7. Encrypt entropy using XSalsa20-Poly1305 (WDK format)
  const encryptedEntropy = wdkEncrypt(entropy, key);
  key.fill(0);

  // 8. Assemble vault JSON
  const vault = {
    encryptedEntropy: encryptedEntropy.toString('hex'),
    salt: salt.toString('hex'),
  };

  // 9. Ensure parent directory exists
  const vaultDir = dirname(opts.vaultFile);
  mkdirSync(vaultDir, { recursive: true });

  // 10. Write vault file
  writeFileSync(opts.vaultFile, JSON.stringify(vault, null, 2), { encoding: 'utf8' });

  // 11. Set permissions to 600
  chmodSync(opts.vaultFile, 0o600);

  // 12. Derive wallet address: entropy → mnemonic → Wallet.fromPhrase
  let wallet;
  try {
    const mnemonic = bip39.entropyToMnemonic(entropy);
    wallet = Wallet.fromPhrase(mnemonic);
  } finally {
    sodium.sodium_memzero(entropy);
  }

  // 13. Output result as JSON to stdout
  process.stdout.write(JSON.stringify({ address: wallet.address, vaultFile: opts.vaultFile }) + '\n');
}

main().catch(err => {
  process.stderr.write(`Error: ${err.message}\n`);
  process.exit(1);
});

```

### scripts/lib/erc20.js

```javascript
import { Interface, formatUnits, parseUnits } from 'ethers6';

const ERC20_ABI = [
  'function balanceOf(address) view returns (uint256)',
  'function allowance(address,address) view returns (uint256)',
  'function approve(address,uint256) returns (bool)',
];

const iface = new Interface(ERC20_ABI);

/**
 * Get the token balance of an address.
 *
 * @param {{ address: string, decimals: number }} token
 * @param {string} address
 * @param {object} provider  ethers provider (or compatible mock with .call())
 * @returns {Promise<string>}  Human-readable balance formatted with token decimals
 */
export async function getBalance(token, address, provider) {
  const data = iface.encodeFunctionData('balanceOf', [address]);
  const raw = await provider.call({ to: token.address, data });
  const [value] = iface.decodeFunctionResult('balanceOf', raw);
  return formatUnits(value, token.decimals);
}

/**
 * Get the ERC-20 allowance granted by owner to spender.
 *
 * @param {{ address: string, decimals: number }} token
 * @param {string} owner
 * @param {string} spender
 * @param {object} provider
 * @returns {Promise<string>}  Human-readable allowance formatted with token decimals
 */
export async function getAllowance(token, owner, spender, provider) {
  const data = iface.encodeFunctionData('allowance', [owner, spender]);
  const raw = await provider.call({ to: token.address, data });
  const [value] = iface.decodeFunctionResult('allowance', raw);
  return formatUnits(value, token.decimals);
}

/**
 * Approve a spender to transfer tokens on behalf of the signer.
 *
 * For tokens like USDT that revert when changing a non-zero allowance directly,
 * pass `opts.requiresResetApprove = true` to first reset to 0 before setting the
 * new value.
 *
 * @param {{ address: string, decimals: number }} token
 * @param {string} spender
 * @param {string} amount  Human-readable amount (e.g. "1000")
 * @param {object} signer  ethers signer (or compatible mock)
 * @param {{ requiresResetApprove?: boolean, fallbackProvider?: object }} opts
 * @returns {Promise<{ hash: string }>}
 */
export async function approve(token, spender, amount, signer, opts = {}) {
  const rawAmount = parseUnits(amount, token.decimals);
  const timeoutMs = (opts.timeoutSeconds ?? 300) * 1000;
  const waitForReceipt = async (sentTx) => {
    // Use fallbackProvider.waitForTransaction when available for RPC resilience
    if (opts.fallbackProvider?.waitForTransaction) {
      return Promise.race([
        opts.fallbackProvider.waitForTransaction(sentTx.hash, 1, timeoutMs),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error(
            `Approve tx not confirmed within ${timeoutMs / 1000}s (txHash: ${sentTx.hash}). It may still be pending.`
          )), timeoutMs)
        ),
      ]);
    }
    return Promise.race([
      sentTx.wait(),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error(
          `Approve tx not confirmed within ${timeoutMs / 1000}s (txHash: ${sentTx.hash}). It may still be pending.`
        )), timeoutMs)
      ),
    ]);
  };

  if (opts.requiresResetApprove) {
    const resetData = iface.encodeFunctionData('approve', [spender, 0n]);
    const resetTx = await signer.sendTransaction({ to: token.address, data: resetData });
    const resetReceipt = await waitForReceipt(resetTx);
    if (!resetReceipt || resetReceipt.status !== 1) {
      throw new Error(`Allowance reset failed (txHash: ${resetTx.hash}). Approval not sent.`);
    }
  }

  const data = iface.encodeFunctionData('approve', [spender, rawAmount]);
  const tx = await signer.sendTransaction({ to: token.address, data });
  const receipt = await waitForReceipt(tx);
  if (!receipt || receipt.status !== 1) {
    throw new Error(`Approval failed (txHash: ${tx.hash}). Check token contract and allowance.`);
  }

  return { hash: tx.hash };
}

```

### scripts/lib/export-seed.js

```javascript
#!/usr/bin/env node
/**
 * export-seed.js — Decrypt a WDK vault and print the BIP-39 mnemonic.
 *
 * Usage:
 *   node lib/export-seed.js [--password-file <path>] [--vault-file <path>]
 *
 * Defaults:
 *   --password-file  ~/.aurehub/.wdk_password
 *   --vault-file     ~/.aurehub/.wdk_vault
 *
 * Security:
 *   - Requires an interactive terminal (TTY). Refuses to run when stdout is
 *     piped or captured (e.g. by an AI agent) to prevent seed leakage.
 *   - Prompts for confirmation before revealing the mnemonic.
 *   - Displays the seed briefly, then offers to clear the screen.
 */

import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { pbkdf2Sync } from 'crypto';
import { createInterface } from 'readline';

import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const sodium = require('sodium-native');
const b4a = require('b4a');
const bip39 = require('bip39-mnemonic');

function expandTilde(p) {
  if (typeof p === 'string' && p.startsWith('~/')) {
    return join(homedir(), p.slice(2));
  }
  return p;
}

function fatal(msg) {
  process.stderr.write(`Error: ${msg}\n`);
  process.exit(1);
}

function parseArgs(argv) {
  const args = argv.slice(2);
  const opts = {
    passwordFile: join(homedir(), '.aurehub', '.wdk_password'),
    vaultFile: join(homedir(), '.aurehub', '.wdk_vault'),
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--password-file') {
      opts.passwordFile = expandTilde(args[++i]);
    } else if (arg === '--vault-file') {
      opts.vaultFile = expandTilde(args[++i]);
    } else {
      fatal(`Unknown argument: ${arg}`);
    }
  }

  return opts;
}

/**
 * Prompt the user on the TTY and return their answer.
 * Prompts are written to stderr so stdout stays clean.
 */
function askTty(question) {
  return new Promise((resolve) => {
    const rl = createInterface({
      input: process.stdin,
      output: process.stderr,
      terminal: true,
    });
    rl.question(question, (answer) => {
      rl.close();
      resolve(answer.trim());
    });
  });
}

function wdkDecrypt(payload, key) {
  const NONCEBYTES = sodium.crypto_secretbox_NONCEBYTES;
  const MACBYTES = sodium.crypto_secretbox_MACBYTES;

  if (payload[0] !== 0) throw new Error('Unsupported encryption version');

  const nonce = payload.subarray(1, 1 + NONCEBYTES);
  const cipher = payload.subarray(1 + NONCEBYTES);
  const plain = b4a.alloc(cipher.byteLength - MACBYTES);

  if (!sodium.crypto_secretbox_open_easy(plain, cipher, nonce, key)) {
    throw new Error('Decryption failed — wrong password or corrupted vault');
  }

  const bytes = plain[0];
  const result = b4a.alloc(bytes);
  result.set(plain.subarray(1, 1 + bytes));
  sodium.sodium_memzero(plain);
  return result;
}

async function main() {
  const opts = parseArgs(process.argv);

  // ── TTY gate: refuse to reveal seed in non-interactive contexts ──────────
  if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stderr.isTTY) {
    fatal(
      'This command must be run in an interactive terminal.\n' +
      'It cannot be executed by scripts, agents, or piped commands\n' +
      'to prevent your seed phrase from leaking into logs or chat history.\n\n' +
      'Open a terminal window and run the command directly.'
    );
  }

  // ── Decrypt vault ────────────────────────────────────────────────────────
  let password;
  try {
    password = readFileSync(opts.passwordFile, 'utf8').trim();
  } catch (err) {
    fatal(`Cannot read password file "${opts.passwordFile}": ${err.message}`);
  }

  let vaultJson;
  try {
    vaultJson = readFileSync(opts.vaultFile, 'utf8');
  } catch (err) {
    fatal(`Cannot read vault file "${opts.vaultFile}": ${err.message}`);
  }

  const vault = JSON.parse(vaultJson);
  if (!vault.encryptedEntropy || !vault.salt) {
    fatal('Vault is missing required fields: encryptedEntropy, salt');
  }

  const salt = Buffer.from(vault.salt, 'hex');
  const encryptedEntropy = Buffer.from(vault.encryptedEntropy, 'hex');
  const key = pbkdf2Sync(password, salt, 100_000, 32, 'sha256');
  const entropy = wdkDecrypt(encryptedEntropy, key);
  key.fill(0);
  const mnemonic = bip39.entropyToMnemonic(entropy);
  sodium.sodium_memzero(entropy);

  // ── Interactive confirmation ─────────────────────────────────────────────
  process.stderr.write('\n');
  process.stderr.write('  ┌─────────────────────────────────────────────────────┐\n');
  process.stderr.write('  │  WARNING: Your seed phrase is about to be displayed │\n');
  process.stderr.write('  │                                                     │\n');
  process.stderr.write('  │  • Make sure no one can see your screen             │\n');
  process.stderr.write('  │  • Do not screenshot or copy to clipboard           │\n');
  process.stderr.write('  │  • Write the words on paper and store offline       │\n');
  process.stderr.write('  └─────────────────────────────────────────────────────┘\n');
  process.stderr.write('\n');

  const confirm = await askTty('  Type "yes" to reveal your seed phrase: ');
  if (confirm.toLowerCase() !== 'yes') {
    process.stderr.write('  Cancelled.\n');
    process.exit(0);
  }

  // ── Display seed phrase ──────────────────────────────────────────────────
  process.stderr.write('\n  Your 12-word seed phrase:\n\n');
  process.stderr.write(`  ${mnemonic}\n`);
  process.stderr.write('\n');

  // ── Post-display: offer to clear screen ──────────────────────────────────
  const clear = await askTty('  Press Enter to clear the screen (or type "keep" to leave it): ');
  if (clear.toLowerCase() !== 'keep') {
    // ANSI escape: clear screen + move cursor to top
    process.stdout.write('\x1b[2J\x1b[H');
    process.stderr.write('  Screen cleared. Your seed phrase is no longer visible.\n');
  }
}

main().catch(err => {
  process.stderr.write(`Error: ${err.message}\n`);
  process.exit(1);
});

```

### scripts/lib/provider.js

```javascript
import { JsonRpcProvider } from 'ethers6';
import { loadConfig } from './config.js';

/**
 * Error codes and patterns that indicate a transient failure worth retrying
 * on another RPC endpoint.
 */
const RETRIABLE_HTTP_STATUSES = new Set([429, 502, 503]);
const RETRIABLE_NODE_CODES = new Set(['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNRESET']);
const RETRIABLE_MSG_PATTERNS = [
  /rate.?limit/i,
  /too many requests/i,
  /timeout/i,
  /service unavailable/i,
  /bad gateway/i,
  /connection refused/i,
];

function isRetriable(err) {
  if (err.status && RETRIABLE_HTTP_STATUSES.has(err.status)) return true;
  if (err.code && RETRIABLE_NODE_CODES.has(err.code)) return true;
  if (err.message) {
    for (const pattern of RETRIABLE_MSG_PATTERNS) {
      if (pattern.test(err.message)) return true;
    }
  }
  return false;
}

export class FallbackProvider {
  /**
   * @param {string} primaryUrl  - Primary RPC URL (required)
   * @param {string[]} fallbackUrls - Ordered list of fallback URLs
   * @param {number} requestTimeoutMs - Per-request timeout for JSON-RPC calls
   */
  constructor(primaryUrl, fallbackUrls = [], requestTimeoutMs = 12_000) {
    if (!primaryUrl) {
      throw new Error('FallbackProvider requires a primary RPC URL');
    }
    this._primaryUrl = primaryUrl;
    this._fallbackUrls = fallbackUrls;
    this._requestTimeoutMs = requestTimeoutMs;
    // Underlying ethers provider always points at the current primary
    this._ethersProvider = new JsonRpcProvider(primaryUrl);
  }

  /**
   * Send a single JSON-RPC request to `url`. Override in tests to avoid
   * real network calls.
   *
   * @param {string} url
   * @param {string} method
   * @param {any[]} params
   * @returns {Promise<any>}
   */
  async _rawSend(url, method, params) {
    const body = JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method,
      params,
    });

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this._requestTimeoutMs);
    let response;
    try {
      response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body,
        signal: controller.signal,
      });
    } catch (err) {
      if (err?.name === 'AbortError') {
        const timeoutErr = new Error(`RPC request timeout after ${this._requestTimeoutMs}ms`);
        timeoutErr.code = 'ETIMEDOUT';
        throw timeoutErr;
      }
      throw err;
    } finally {
      clearTimeout(timer);
    }

    if (!response.ok) {
      const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
      err.status = response.status;
      throw err;
    }

    const json = await response.json();
    if (json.error) {
      const err = new Error(json.error.message ?? 'RPC error');
      err.code = json.error.code;
      throw err;
    }

    return json.result;
  }

  /**
   * Try the primary URL first; on retriable errors try each fallback in order.
   * Session-sticky: the first URL that succeeds becomes the new primary.
   *
   * @param {string} method
   * @param {any[]} params
   * @returns {Promise<any>}
   */
  async _sendWithFallback(method, params) {
    const urls = [this._primaryUrl, ...this._fallbackUrls];
    const errors = [];

    for (let i = 0; i < urls.length; i++) {
      const url = urls[i];
      try {
        const result = await this._rawSend(url, method, params);

        // Session-sticky: promote successful fallback to primary
        if (i > 0) {
          const oldPrimary = this._primaryUrl;
          this._primaryUrl = url;
          this._fallbackUrls = [
            oldPrimary,
            ...urls.slice(1, i),
            ...urls.slice(i + 1),
          ];
          this._ethersProvider = new JsonRpcProvider(this._primaryUrl);
        }

        return result;
      } catch (err) {
        if (!isRetriable(err) || i === urls.length - 1) {
          // Non-retriable: throw immediately without trying fallbacks
          if (!isRetriable(err)) throw err;
        }
        errors.push({ url, error: err });
      }
    }

    // All URLs failed — redact API keys from URLs before logging
    const redact = (u) => { try { const o = new URL(u); return `${o.protocol}//${o.host}${o.pathname.replace(/\/[^/]{20,}$/, '/***')}`; } catch { return '[invalid url]'; } };
    const summary = errors
      .map(({ url, error }) => `${redact(url)}: ${error.message}`)
      .join('; ');
    throw new Error(`All RPC endpoints failed — ${summary}`);
  }

  /**
   * Generic JSON-RPC send.
   */
  async send(method, params) {
    return this._sendWithFallback(method, params);
  }

  /**
   * eth_call — routes through fallback path.
   */
  async call(tx) {
    return this._sendWithFallback('eth_call', [tx, 'latest']);
  }

  /**
   * eth_blockNumber — routes through fallback path.
   */
  async getBlockNumber() {
    return this._sendWithFallback('eth_blockNumber', []);
  }

  /**
   * eth_getBalance — routes through fallback path.
   */
  async getBalance(address) {
    return this._sendWithFallback('eth_getBalance', [address, 'latest']);
  }

  /**
   * Return the underlying ethers JsonRpcProvider (e.g. for Wallet.connect()).
   * Always reflects the current primary URL (updated after fallback switches).
   */
  getEthersProvider() {
    return this._ethersProvider;
  }

  /**
   * Wait for a transaction receipt with fallback support.
   * When the current provider's waitForTransaction fails with a retriable
   * error, try each fallback URL's provider instead.
   *
   * @param {string} txHash
   * @param {number} [confirmations=1]
   * @param {number} [timeoutMs=300000]
   * @returns {Promise<import('ethers6').TransactionReceipt|null>}
   */
  async waitForTransaction(txHash, confirmations = 1, timeoutMs = 300000) {
    const urls = [this._primaryUrl, ...this._fallbackUrls];

    // Race all URLs in parallel — whichever returns the receipt first wins.
    // This prevents a slow/lagging primary RPC from blocking confirmation
    // detection when faster fallback nodes already have the receipt indexed.
    const providers = urls.map(url => {
      const p = new JsonRpcProvider(url);
      p.pollingInterval = 1000;
      return p;
    });

    // Do NOT pass timeoutMs to individual providers — ethers6 creates an internal
    // setTimeout for the timeout that destroy() cannot cancel, which would hold
    // the event loop for the full duration. Manage the overall timeout ourselves.
    const racePromises = providers.map((provider, i) =>
      provider.waitForTransaction(txHash, confirmations)
        .then(receipt => ({ receipt, index: i }))
    );

    let result;
    let timer;
    try {
      result = await Promise.race([
        Promise.any(racePromises),
        new Promise((_, reject) => {
          timer = setTimeout(() => reject(new Error(
            `waitForTransaction timeout after ${timeoutMs / 1000}s`
          )), timeoutMs);
        }),
      ]);
    } finally {
      clearTimeout(timer);
      // Destroy all providers to stop their polling loops and free the event loop.
      providers.forEach(p => p.destroy());
    }

    const { receipt, index } = result;

    // Promote the winning URL to primary if it was a fallback
    if (index > 0) {
      const oldPrimary = this._primaryUrl;
      this._primaryUrl = urls[index];
      this._fallbackUrls = [
        oldPrimary,
        ...urls.slice(1, index),
        ...urls.slice(index + 1),
      ];
      this._ethersProvider = new JsonRpcProvider(this._primaryUrl);
    }

    return receipt;
  }
}

/**
 * Build a FallbackProvider from an env object.
 *
 * @param {Record<string, string>} env
 * @returns {FallbackProvider}
 */
export function createProvider(env) {
  let effectiveEnv = env;
  if (!effectiveEnv?.ETH_RPC_URL) {
    // Fallback: load from ~/.aurehub/.env when env vars are not exported
    try {
      const cfg = loadConfig();
      effectiveEnv = { ...cfg.env, ...effectiveEnv };
    } catch (_) {
      // ignore — will throw below if still missing
    }
  }

  const primaryUrl = effectiveEnv.ETH_RPC_URL;
  if (!primaryUrl) {
    throw new Error(
      'ETH_RPC_URL is required in env to create a provider'
    );
  }

  const fallbackUrls = effectiveEnv.ETH_RPC_URL_FALLBACK
    ? effectiveEnv.ETH_RPC_URL_FALLBACK.split(',').map((u) => u.trim()).filter(Boolean)
    : [];

  return new FallbackProvider(primaryUrl, fallbackUrls);
}

```

### scripts/lib/signer.js

```javascript
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';

/** Expand leading ~ to the user's home directory. */
function expandTilde(p) {
  if (typeof p === 'string' && p.startsWith('~/')) {
    return join(homedir(), p.slice(2));
  }
  return p;
}
import { pbkdf2Sync } from 'crypto';
import { Wallet } from 'ethers6';

// sodium-native is a CJS module; we import it via createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const sodium = require('sodium-native');
const b4a = require('b4a');
const bip39 = require('bip39-mnemonic');

// ---------------------------------------------------------------------------
// WDK vault decryption helpers
//
// @tetherto/wdk-secret-manager uses bare-crypto for PBKDF2 key derivation,
// which relies on Bare-runtime native bindings (.bare files) incompatible
// with standard Node.js.  We replicate the same algorithm using Node.js
// built-in `crypto.pbkdf2Sync` + `sodium-native`, producing byte-for-byte
// identical results.
//
// Vault format:
//   { encryptedEntropy: <hex>, salt: <hex> }
//
// Encryption format (from WdkSecretManager source):
//   byte 0     : version (0)
//   bytes 1..N : nonce   (crypto_secretbox_NONCEBYTES = 24)
//   bytes N+1..: ciphertext (1 + plaintextLen + MACBYTES)
//                  plain[0]  = original payload length
//                  plain[1..]: payload bytes
// ---------------------------------------------------------------------------

/**
 * Derive a 32-byte key from password + salt using PBKDF2-SHA256.
 * Matches WdkSecretManager#deriveKeyFromPassKey() (100 000 iterations).
 *
 * @param {string|Buffer} password
 * @param {Buffer} salt  16-byte salt
 * @returns {Buffer}
 */
function wdkDeriveKey(password, salt) {
  return pbkdf2Sync(password, salt, 100_000, 32, 'sha256');
}

/**
 * Decrypt a WDK-encrypted payload.
 * Matches WdkSecretManager.decrypt().
 *
 * @param {Buffer} payload  Encrypted bytes
 * @param {Buffer} key      32-byte derived key
 * @returns {Buffer}  Decrypted entropy bytes
 */
function wdkDecrypt(payload, key) {
  const NONCEBYTES = sodium.crypto_secretbox_NONCEBYTES;
  const MACBYTES = sodium.crypto_secretbox_MACBYTES;

  if (payload[0] !== 0) throw new Error('WDK vault: unsupported encryption version');

  const nonce = payload.subarray(1, 1 + NONCEBYTES);
  const cipher = payload.subarray(1 + NONCEBYTES);
  const plain = b4a.alloc(cipher.byteLength - MACBYTES);

  if (!sodium.crypto_secretbox_open_easy(plain, cipher, nonce, key)) {
    throw new Error('WDK vault: decryption failed — wrong password or corrupted vault');
  }

  const bytes = plain[0];
  const result = b4a.alloc(bytes);
  result.set(plain.subarray(1, 1 + bytes));
  sodium.sodium_memzero(plain);
  return result;
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Create an ethers.Wallet connected to `provider` from the wallet backend
 * specified by cfg.env.WALLET_MODE.
 *
 * Supported modes:
 *   'foundry' — decrypt a Foundry keystore JSON file with KEYSTORE_PASSWORD_FILE
 *   'wdk'     — decrypt a WDK vault file with WDK_PASSWORD_FILE
 *
 * @param {{ env: object, yaml: object, configDir: string }} cfg
 *   Config object returned by loadConfig().
 * @param {import('ethers').Provider|null} provider
 *   ethers provider to connect the wallet to (may be null/undefined).
 * @param {{ keystoreDir?: string }} [opts]
 *   Optional overrides for testing (e.g. keystoreDir to override Foundry default).
 * @returns {Promise<import('ethers').Wallet>}
 */
export async function createSigner(cfg, provider, opts = {}) {
  const walletMode = cfg?.env?.WALLET_MODE;

  if (!walletMode) {
    throw new Error(
      'WALLET_MODE not set in .env. Run setup to select a wallet mode.',
    );
  }

  if (walletMode === 'foundry') {
    return _createFoundrySigner(cfg, provider, opts);
  }

  if (walletMode === 'wdk') {
    return _createWdkSigner(cfg, provider);
  }

  throw new Error(
    `Unknown wallet_mode "${walletMode}". Expected "foundry" or "wdk".`,
  );
}

// ---------------------------------------------------------------------------
// Foundry keystore backend
// ---------------------------------------------------------------------------

async function _createFoundrySigner(cfg, provider, opts) {
  const accountName = cfg.env.FOUNDRY_ACCOUNT;
  if (!accountName) {
    throw new Error('FOUNDRY_ACCOUNT not set in .env');
  }

  const keystoreDir =
    opts.keystoreDir ?? join(homedir(), '.foundry', 'keystores');
  const keystorePath = join(expandTilde(keystoreDir), accountName);

  let keystoreJson;
  try {
    keystoreJson = readFileSync(keystorePath, 'utf8');
  } catch (err) {
    throw new Error(
      `Foundry keystore not found at "${keystorePath}": ${err.message}`,
    );
  }

  const passwordFile = expandTilde(cfg.env.KEYSTORE_PASSWORD_FILE);
  if (!passwordFile) {
    throw new Error('KEYSTORE_PASSWORD_FILE not set in .env');
  }

  let password;
  try {
    password = readFileSync(passwordFile, 'utf8').trim();
  } catch (err) {
    throw new Error(
      `Cannot read KEYSTORE_PASSWORD_FILE "${passwordFile}": ${err.message}`,
    );
  }

  const wallet = await Wallet.fromEncryptedJson(keystoreJson, password);
  return provider ? wallet.connect(provider) : wallet;
}

// ---------------------------------------------------------------------------
// WDK vault backend
// ---------------------------------------------------------------------------

async function _createWdkSigner(cfg, provider) {
  const vaultPath = expandTilde(
    cfg.env.WDK_VAULT_FILE ??
    join(homedir(), '.aurehub', '.wdk_vault'),
  );

  let vaultJson;
  try {
    vaultJson = readFileSync(vaultPath, 'utf8');
  } catch (err) {
    throw new Error(`WDK vault not found at "${vaultPath}": ${err.message}`);
  }

  const vault = JSON.parse(vaultJson);
  if (!vault.encryptedEntropy || !vault.salt) {
    throw new Error(
      'WDK vault is missing required fields: encryptedEntropy, salt',
    );
  }

  const passwordFile = expandTilde(
    cfg.env.WDK_PASSWORD_FILE ??
    join(homedir(), '.aurehub', '.wdk_password'),
  );

  let password;
  try {
    password = readFileSync(passwordFile, 'utf8').trim();
  } catch (err) {
    throw new Error(
      `Cannot read WDK_PASSWORD_FILE "${passwordFile}": ${err.message}`,
    );
  }

  const salt = Buffer.from(vault.salt, 'hex');
  const encryptedEntropy = Buffer.from(vault.encryptedEntropy, 'hex');

  const key = wdkDeriveKey(password, salt);
  const entropy = wdkDecrypt(encryptedEntropy, key);
  key.fill(0);
  let wallet;
  try {
    const mnemonic = bip39.entropyToMnemonic(entropy);
    wallet = Wallet.fromPhrase(mnemonic);
  } finally {
    sodium.sodium_memzero(entropy);
  }
  return provider ? wallet.connect(provider) : wallet;
}

```

### scripts/lib/uniswap.js

```javascript
/**
 * uniswap.js — Quote via QuoterV2 and build swap calldata via SwapRouter02
 *
 * Uses ethers v6 ABI encoding directly instead of @uniswap/v3-sdk SwapRouter
 * to avoid a JSBI class identity mismatch between @uniswap/sdk-core@6 and
 * @uniswap/[email protected] that causes Trade.minimumAmountOut to throw.
 */

import { Interface, AbiCoder, parseUnits } from 'ethers6';

// ---------------------------------------------------------------------------
// ABIs
// ---------------------------------------------------------------------------

const QUOTER_V2_ABI = [
  'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
];

// SwapRouter02 exactInputSingle (used for ERC-20 → ERC-20)
const SWAP_ROUTER_ABI = [
  'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',
];

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

// ---------------------------------------------------------------------------
// quote()
// ---------------------------------------------------------------------------

/**
 * Get an on-chain quote from Uniswap V3 QuoterV2.
 *
 * @param {object} params
 * @param {{ address: string, decimals: number }} params.tokenIn
 * @param {{ address: string, decimals: number }} params.tokenOut
 * @param {string} params.amountIn  Human-readable amount (e.g. "1.5")
 * @param {number} params.fee       Pool fee tier (e.g. 3000)
 * @param {{ quoter: string }} params.contracts
 * @param {object} params.provider  ethers-compatible provider
 * @returns {Promise<{ amountOut: string, amountOutRaw: bigint, sqrtPriceX96: bigint, gasEstimate: bigint }>}
 */
export async function quote({ tokenIn, tokenOut, amountIn, fee, contracts, provider }) {
  const iface = new Interface(QUOTER_V2_ABI);

  const amountInRaw = parseUnits(amountIn, tokenIn.decimals);

  const calldata = iface.encodeFunctionData('quoteExactInputSingle', [{
    tokenIn: tokenIn.address,
    tokenOut: tokenOut.address,
    amountIn: amountInRaw,
    fee,
    sqrtPriceLimitX96: 0n,
  }]);

  const raw = await provider.call({ to: contracts.quoter, data: calldata });

  const coder = AbiCoder.defaultAbiCoder();
  const [amountOutRaw, sqrtPriceX96After, , gasEstimate] = coder.decode(
    ['uint256', 'uint160', 'uint32', 'uint256'],
    raw,
  );

  // Convert decoded values to plain bigints (ethers returns BigInt natively in v6)
  const amountOutBig = BigInt(amountOutRaw.toString());
  const sqrtPriceBig = BigInt(sqrtPriceX96After.toString());
  const gasEstimateBig = BigInt(gasEstimate.toString());

  // Format human-readable output
  const divisor = 10n ** BigInt(tokenOut.decimals);
  const whole = amountOutBig / divisor;
  const remainder = amountOutBig % divisor;
  const fracStr = remainder.toString().padStart(tokenOut.decimals, '0').replace(/0+$/, '') || '0';
  const amountOutFormatted = `${whole}.${fracStr}`;

  return {
    amountOut: amountOutFormatted,
    amountOutRaw: amountOutBig,
    sqrtPriceX96: sqrtPriceBig,
    gasEstimate: gasEstimateBig,
  };
}

// ---------------------------------------------------------------------------
// buildSwap()
// ---------------------------------------------------------------------------

/**
 * Build SwapRouter02 exactInputSingle calldata.
 *
 * slippage is already baked into minAmountOut by the caller — we pass it
 * directly as amountOutMinimum.
 *
 * @param {object} params
 * @param {{ address: string, decimals: number }} params.tokenIn
 * @param {{ address: string, decimals: number }} params.tokenOut
 * @param {string} params.amountIn     Human-readable amount in
 * @param {string} params.minAmountOut Human-readable minimum amount out
 * @param {number} params.fee          Pool fee tier
 * @param {string} params.recipient    Recipient address
 * @param {number} params.deadline     Unix timestamp deadline
 * @param {{ router: string }} params.contracts
 * @returns {{ to: string, data: string, value: bigint }}
 */
export function buildSwap({ tokenIn, tokenOut, amountIn, minAmountOut, fee, recipient, deadline, contracts }) {
  const isEth = tokenIn.address === ZERO_ADDRESS;

  const amountInRaw = parseUnits(amountIn, tokenIn.decimals);
  const amountOutMinRaw = parseUnits(minAmountOut, tokenOut.decimals);

  const iface = new Interface(SWAP_ROUTER_ABI);

  // When the input token is native ETH, use WETH address for the swap struct
  // and send value with the transaction. SwapRouter02 handles WETH wrapping.
  const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
  const tokenInAddress = isEth ? WETH : tokenIn.address;

  const data = iface.encodeFunctionData('exactInputSingle', [{
    tokenIn: tokenInAddress,
    tokenOut: tokenOut.address,
    fee,
    recipient,
    amountIn: amountInRaw,
    amountOutMinimum: amountOutMinRaw,
    sqrtPriceLimitX96: 0n,
  }]);

  return {
    to: contracts.router,
    data,
    value: isEth ? amountInRaw : 0n,
  };
}

```

### scripts/limit-order.js

```javascript
#!/usr/bin/env node
// skills/xaut-trade/scripts/limit-order.js
//
// Usage:
//   node limit-order.js place  --token-in <addr> --token-out <addr> \
//                               --amount-in <uint> --min-amount-out <uint> \
//                               --expiry <seconds> --wallet <addr> \
//                               --chain-id <int> --api-url <url>
//   node limit-order.js status --order-hash <0x...> --api-url <url> --chain-id <int>
//   node limit-order.js list   --wallet <addr> --api-url <url> --chain-id <int> [--order-status open|filled|expired|cancelled]
//   node limit-order.js cancel --order-hash <0x...>   (reads nonce from ~/.aurehub/orders/)
//   node limit-order.js cancel --nonce <uint>          (fallback)
//
// Signing mode: delegates to swap.js sign (supports both WDK and Foundry).
//   PRIVATE_KEY runtime signing is intentionally not supported.
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import yaml from 'js-yaml';
import { computeNonceComponents, checkPrecision, resolveExpiry } from './helpers.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const { ethers } = require('ethers');
const { DutchOrderBuilder } = require('@uniswap/uniswapx-sdk');

// ethers v5 BigNumber (SDK uses .gte() etc — native BigInt not compatible)
const BN = ethers.BigNumber;

const [,, subcommand, ...argv] = process.argv;

// Load defaults from ~/.aurehub/config.yaml
function loadDefaults() {
  try {
    const configDir = path.join(os.homedir(), '.aurehub');
    const raw = fs.readFileSync(path.join(configDir, 'config.yaml'), 'utf8');
    const cfg = yaml.load(raw, { schema: yaml.JSON_SCHEMA }) ?? {};
    return {
      apiUrl: cfg.limit_order?.uniswapx_api || null,
      chainId: String(cfg.networks?.ethereum_mainnet?.chain_id || '1'),
      limitOrderCfg: cfg.limit_order ?? {},
    };
  } catch { return { apiUrl: null, chainId: '1', limitOrderCfg: {} }; }
}

function parseArgs(args) {
  const result = {};
  for (let i = 0; i < args.length; i += 2) {
    if (i + 1 >= args.length) {
      console.error(`Missing value for flag: ${args[i]}`);
      process.exit(1);
    }
    const key = args[i].replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase());
    result[key] = args[i + 1];
  }
  return result;
}

async function main() {
  const args = parseArgs(argv);
  // Apply config.yaml defaults for apiUrl and chainId when not provided via CLI
  const defaults = loadDefaults();
  if (!args.apiUrl && defaults.apiUrl) args.apiUrl = defaults.apiUrl;
  if (!args.chainId && defaults.chainId) args.chainId = defaults.chainId;
  args._limitOrderCfg = defaults.limitOrderCfg;
  switch (subcommand) {
    case 'place':  return await place(args);
    case 'status': return await status(args);
    case 'list':   return await list(args);
    case 'cancel': return await cancel(args);
    default:
      console.error('Usage: limit-order.js <place|status|list|cancel> [options]');
      process.exit(1);
  }
}

main().catch(err => { console.error(err.message); process.exit(1); });

async function place(args) {
  const {
    tokenIn, tokenOut, amountIn, minAmountOut,
    expiry, wallet, chainId, apiUrl,
  } = args;

  // 1. Validate required args
  const required = { tokenIn, tokenOut, amountIn, minAmountOut, wallet, chainId, apiUrl };
  for (const [k, v] of Object.entries(required)) {
    if (!v) { console.error(`Missing required argument: --${k.replace(/([A-Z])/g, '-$1').toLowerCase()}`); process.exit(1); }
  }

  // Precision check: both XAUT and USDT have 6 decimals max
  if (!checkPrecision(amountIn, 6)) {
    console.error('ERROR: amountIn exceeds maximum precision (6 decimal places)');
    process.exit(1);
  }
  if (!checkPrecision(minAmountOut, 6)) {
    console.error('ERROR: minAmountOut exceeds maximum precision (6 decimal places)');
    process.exit(1);
  }

  const chainIdNum = parseInt(chainId, 10);
  if (Number.isNaN(chainIdNum) || chainIdNum <= 0) {
    console.error('ERROR: --chain-id must be a positive integer');
    process.exit(1);
  }
  const limitOrderCfg = args._limitOrderCfg || {};
  const expiryLimits = {
    defaultSeconds: limitOrderCfg.default_expiry_seconds ?? 86400,
    minSeconds: limitOrderCfg.min_expiry_seconds ?? 300,
    maxSeconds: limitOrderCfg.max_expiry_seconds ?? 2592000,
  };
  const rawExpiry = expiry ? parseInt(expiry, 10) : null;
  if (rawExpiry !== null && (Number.isNaN(rawExpiry) || rawExpiry <= 0)) {
    console.error('ERROR: --expiry must be a positive integer (seconds)');
    process.exit(1);
  }
  const expirySec = resolveExpiry(rawExpiry, expiryLimits);
  const deadline = Math.floor(Date.now() / 1000) + expirySec;

  // 2. Validate amounts (invariant — does not depend on nonce)
  const amountInBN = BN.from(amountIn);
  const minAmountOutBN = BN.from(minAmountOut);
  if (minAmountOutBN.lte(BN.from(0))) {
    console.error('ERROR: --min-amount-out must be greater than 0');
    process.exit(1);
  }

  // 3. Fetch nonce from UniswapX API
  // UniswapX API requires Origin header matching app.uniswap.org; may break if Uniswap changes policy
  const apiKey = process.env.UNISWAPX_API_KEY || '';
  const uniswapHeaders = {
    'Origin': 'https://app.uniswap.org',
    'User-Agent': 'Mozilla/5.0',
    ...(apiKey ? { 'x-api-key': apiKey } : {}),
  };
  const nonceRes = await fetch(`${apiUrl}/nonce?address=${wallet}&chainId=${chainId}`, { headers: uniswapHeaders });
  if (!nonceRes.ok) throw new Error(`Nonce fetch failed: ${nonceRes.status} ${await nonceRes.text()}`);
  const nonceData = await nonceRes.json();
  let nonce = nonceData.nonce;

  // 4. Build, sign, and submit order with nonce retry logic (max 5 attempts)
  const MAX_NONCE_RETRIES = 5;
  for (let attempt = 0; attempt < MAX_NONCE_RETRIES; attempt++) {
    // Build order (must rebuild each attempt — nonce changes on retry)
    // Fixed-price limit order: decayStart == decayEnd == deadline so the
    // DutchOrder never decays — the swapper receives exactly minAmountOut
    // (no Dutch-auction price improvement, but also no price deterioration).
    const builder = new DutchOrderBuilder(chainIdNum);
    const order = builder
      .deadline(deadline)
      .decayStartTime(deadline)
      .decayEndTime(deadline)
      .nonce(BN.from(nonce))
      .swapper(wallet)
      .input({ token: tokenIn, startAmount: amountInBN, endAmount: amountInBN })
      .output({
        token: tokenOut,
        startAmount: minAmountOutBN,
        endAmount: minAmountOutBN,
        recipient: wallet,
      })
      .build();

    // Sign via EIP-712
    const { domain, types, values } = order.permitData();
    const bnReplacer = (_, v) => (v && v.type === 'BigNumber' && v.hex) ? v.hex : v;
    const eip712Domain = [
      { name: 'name', type: 'string' },
      ...(domain.version !== undefined ? [{ name: 'version', type: 'string' }] : []),
      ...(domain.chainId !== undefined ? [{ name: 'chainId', type: 'uint256' }] : []),
      ...(domain.verifyingContract !== undefined ? [{ name: 'verifyingContract', type: 'address' }] : []),
    ];
    const primaryType = 'PermitWitnessTransferFrom';
    const typesWithDomain = { EIP712Domain: eip712Domain, ...types };
    const typedDataJson = JSON.stringify({ domain, types: typesWithDomain, primaryType, message: values }, bnReplacer);
    let signature;

    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'limit-order-'));
    const tmpFile = path.join(tmpDir, 'typed-data.json');
    fs.writeFileSync(tmpFile, typedDataJson);
    try {
      const signResult = spawnSync(
        process.execPath,
        [path.join(__dirname, 'swap.js'), 'sign', '--data-file', tmpFile],
        { encoding: 'utf8', timeout: 30_000 }
      );
      if (signResult.error) {
        throw new Error(`Failed to spawn swap.js sign: ${signResult.error.message}`);
      }
      if (signResult.status !== 0) {
        throw new Error(signResult.stderr?.trim() || 'swap.js sign failed');
      }
      signature = signResult.stdout.trim();
    } finally {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }

    // Submit to UniswapX API
    const encodedOrder = order.serialize();
    const orderHash = order.hash();

    const submitRes = await fetch(`${apiUrl}/order`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...uniswapHeaders },
      body: JSON.stringify({ encodedOrder, signature, chainId: chainIdNum }),
    });

    if (!submitRes.ok) {
      const body = await submitRes.text();
      if (body.includes('NonceUsed') && attempt < MAX_NONCE_RETRIES - 1) {
        nonce = BN.from(nonce).add(BN.from(1)).toString();
        continue;
      }
      throw new Error(`Order submission failed: ${submitRes.status} ${body}`);
    }

    // 5. Auto-save order to ~/.aurehub/orders/ for later cancellation
    const orderData = {
      orderHash,
      nonce,
      deadline: new Date(deadline * 1000).toISOString(),
      deadlineUnix: deadline,
      tokenIn,
      tokenOut,
      amountIn,
      minAmountOut,
      wallet,
      createdAt: new Date().toISOString(),
    };
    const ordersDir = path.join(os.homedir(), '.aurehub', 'orders');
    fs.mkdirSync(ordersDir, { recursive: true });
    fs.writeFileSync(
      path.join(ordersDir, `${orderHash.slice(0, 10)}.json`),
      JSON.stringify(orderData, null, 2)
    );

    // 6. Output JSON for SKILL.md to parse
    console.log(JSON.stringify({
      address: wallet,
      orderHash,
      deadline: orderData.deadline,
      deadlineUnix: deadline,
      nonce,
    }));
    return;
  }
}

async function status(args) {
  const { orderHash, apiUrl, chainId } = args;
  if (!orderHash || !apiUrl || !chainId) {
    console.error('Missing required: --order-hash, --api-url, --chain-id');
    process.exit(1);
  }

  const apiKey = process.env.UNISWAPX_API_KEY || '';
  const res = await fetch(
    `${apiUrl}/orders?orderHash=${orderHash}&chainId=${chainId}`,
    { headers: {
      'Origin': 'https://app.uniswap.org',
      'User-Agent': 'Mozilla/5.0',
      ...(apiKey ? { 'x-api-key': apiKey } : {}),
    }}
  );
  if (!res.ok) throw new Error(`Status fetch failed: ${res.status} ${await res.text()}`);

  const data = await res.json();
  const orders = data.orders || [];

  if (orders.length === 0) {
    console.log(JSON.stringify({ status: 'not_found', orderHash }));
    return;
  }

  const o = orders[0];
  console.log(JSON.stringify({
    status: o.orderStatus,
    orderHash: o.orderHash,
    deadline: o.deadline ? new Date(o.deadline * 1000).toISOString() : null,
    txHash: o.txHash || null,
    settledAmounts: o.settledAmounts || null,
  }));
}

async function list(args) {
  const { wallet, apiUrl, chainId, orderStatus } = args;
  if (!wallet || !apiUrl || !chainId) {
    console.error('Missing required: --wallet, --api-url, --chain-id');
    process.exit(1);
  }

  const apiKey = process.env.UNISWAPX_API_KEY || '';
  const headers = {
    'Origin': 'https://app.uniswap.org',
    'User-Agent': 'Mozilla/5.0',
    ...(apiKey ? { 'x-api-key': apiKey } : {}),
  };

  // The UniswapX API ignores the offerer param when orderStatus is omitted,
  // returning unrelated orders. Work around by querying each status separately.
  const statuses = orderStatus
    ? [orderStatus]
    : ['open', 'filled', 'expired', 'cancelled'];

  const allOrders = [];
  const walletLower = wallet.toLowerCase();

  for (const s of statuses) {
    const url = `${apiUrl}/orders?offerer=${wallet}&chainId=${chainId}&orderStatus=${s}`;
    const res = await fetch(url, { headers });
    if (!res.ok) throw new Error(`List fetch failed (${s}): ${res.status} ${await res.text()}`);
    const data = await res.json();
    for (const o of (data.orders || [])) {
      // Double-check swapper matches our wallet
      if ((o.swapper || '').toLowerCase() === walletLower) {
        allOrders.push(o);
      }
    }
  }

  const orders = allOrders.map(o => ({
    orderHash: o.orderHash,
    status: o.orderStatus,
    inputToken: o.input?.token,
    inputAmount: o.input?.startAmount,
    outputToken: (o.outputs?.[0])?.token,
    outputAmount: (o.outputs?.[0])?.startAmount,
    txHash: o.txHash || null,
    createdAt: o.createdAt ? new Date(o.createdAt * 1000).toISOString() : null,
  }));

  console.log(JSON.stringify({ address: wallet, total: orders.length, orders }));
}

async function cancel(args) {
  let { nonce, orderHash } = args;

  // Resolve nonce from local order file if --order-hash is provided
  if (!nonce && orderHash) {
    const ordersDir = path.join(os.homedir(), '.aurehub', 'orders');
    if (fs.existsSync(ordersDir)) {
      const files = fs.readdirSync(ordersDir).filter(f => f.endsWith('.json'));
      const prefix = orderHash.toLowerCase();
      if (prefix.length < 10) {
        console.error('ERROR: --order-hash prefix too short (minimum 10 characters, e.g. "0x9079c4f2")');
        process.exit(1);
      }
      const match = files.find(f => {
        try {
          const data = JSON.parse(fs.readFileSync(path.join(ordersDir, f), 'utf8'));
          return data.orderHash && data.orderHash.toLowerCase().startsWith(prefix);
        } catch { return false; }
      });
      if (match) {
        const data = JSON.parse(fs.readFileSync(path.join(ordersDir, match), 'utf8'));
        nonce = data.nonce;
      }
    }
  }

  if (!nonce) {
    console.error('Missing required: --nonce or --order-hash (with matching local order file)');
    process.exit(1);
  }

  const { wordPos, mask } = computeNonceComponents(BigInt(nonce));

  console.log(JSON.stringify({
    wordPos: wordPos.toString(),
    mask: mask.toString(),
    permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3',
  }));
}

```

### scripts/package.json

```json
{
  "name": "xaut-trade-scripts",
  "version": "2.1.1",
  "description": "Helper scripts for xaut-trade skill",
  "type": "module",
  "main": "limit-order.js",
  "scripts": {
    "test": "vitest run --globals __tests__/helpers.test.js __tests__/swap-cli.test.js __tests__/integration.test.js lib/__tests__/*.test.js"
  },
  "dependencies": {
    "@uniswap/sdk-core": "^6",
    "@uniswap/v3-sdk": "3.20.0",
    "@uniswap/uniswapx-sdk": "^2.0.0",
    "b4a": "^1.6.7",
    "bip39-mnemonic": "^2.5.0",
    "ethers": "^5.7.2",
    "ethers6": "npm:ethers@^6",
    "js-yaml": "^4",
    "jsbi": "^4",
    "sodium-native": "^5.0.8",
    "tslib": "^2.8.1"
  },
  "devDependencies": {
    "vitest": "^4.1.0"
  }
}

```

### scripts/setup.sh

```bash
#!/usr/bin/env bash
# xaut-trade environment setup
# Usage: bash skills/xaut-trade/scripts/setup.sh
#
# Exit codes:
#   0 — all automated steps complete; check the manual steps summary at the end
#   1 — setup failed (including missing prerequisites); see references/onboarding.md

set -euo pipefail

# ── Colours ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'

STEP=0
NPM_DEPS_INSTALLED=false

step()   { STEP=$((STEP+1)); echo -e "\n${BLUE}${BOLD}[${STEP}] $1${NC}"; }
ok()     { echo -e "  ${GREEN}✓ $1${NC}"; }
warn()   { echo -e "  ${YELLOW}⚠ $1${NC}"; }
manual() {
  echo -e "\n  ${YELLOW}${BOLD}┌─ Manual action required ────────────────────────────────┐${NC}"
  while IFS= read -r line; do
    echo -e "  ${YELLOW}│${NC} $line"
  done <<< "$1"
  echo -e "  ${YELLOW}${BOLD}└─────────────────────────────────────────────────────────┘${NC}\n"
}

_cleanup() { rm -f "${CAST_ERR_FILE:-}" "${ENV_TMP_FILE:-}"; }
trap '_cleanup; echo -e "\n${RED}❌ Step ${STEP} failed.${NC}\nSee references/onboarding.md for manual instructions, then re-run this script."; exit 1' ERR
trap '_cleanup' EXIT

_ensure_scripts_deps() {
  if [ "$NPM_DEPS_INSTALLED" = true ]; then
    return 0
  fi
  echo "  Installing npm packages..."
  (cd "$SCRIPT_DIR" && npm install --silent)
  NPM_DEPS_INSTALLED=true
  ok "npm packages installed"
}

# ── Locate skill directory from the script's own path ──────────────────────────
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
SKILL_DIR=$(dirname "$SCRIPT_DIR")    # skills/xaut-trade/
ACCOUNT_NAME="aurehub-wallet"

echo -e "\n${BOLD}xaut-trade environment setup${NC}"
echo "Skill directory: $SKILL_DIR"

# ── Step 1: Global config directory ───────────────────────────────────────────
step "Create global config directory ~/.aurehub"
mkdir -p ~/.aurehub
ok "~/.aurehub ready"

# ── Step 2: Wallet Mode Selection ─────────────────────────────────────────────
step "Wallet mode selection"
echo ""
echo -e "  ${BOLD}=== Wallet Mode ===${NC}"
echo -e "  ${BOLD}[1]${NC} WDK (recommended) — seed-phrase based, no external tools needed"
echo -e "  ${BOLD}[2]${NC} Foundry — requires Foundry installed, keystore-based"
echo ""
read -rp "  Select [1]: " wallet_mode_choice
wallet_mode_choice="${wallet_mode_choice:-1}"

if [ "$wallet_mode_choice" = "1" ]; then
  WALLET_MODE="wdk"
elif [ "$wallet_mode_choice" = "2" ]; then
  WALLET_MODE="foundry"
else
  echo -e "  ${YELLOW}Invalid choice. Defaulting to WDK.${NC}"
  WALLET_MODE="wdk"
fi
ok "Wallet mode: $WALLET_MODE"

# ── WDK wallet setup ─────────────────────────────────────────────────────────
if [ "$WALLET_MODE" = "wdk" ]; then

  # ── Step 3: Check Node.js >= 18 ────────────────────────────────────────────
  step "Check Node.js (required for WDK)"
  if ! command -v node &>/dev/null; then
    echo -e "  ${RED}Error: Node.js is required for WDK mode. Install from https://nodejs.org/${NC}"
    exit 1
  fi
  NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
  if [ "$NODE_VERSION" -lt 18 ]; then
    echo -e "  ${RED}Error: Node.js >= 18 required (found v$NODE_VERSION)${NC}"
    exit 1
  fi
  ok "Node.js $(node -v)"
  _ensure_scripts_deps

  # ── Step 4: Prompt for wallet password ─────────────────────────────────────
  step "WDK wallet password"
  WDK_PASSWORD_FILE="$HOME/.aurehub/.wdk_password"
  if [ -f "$WDK_PASSWORD_FILE" ] && [ -s "$WDK_PASSWORD_FILE" ]; then
    ok "WDK password file already exists, skipping"
  else
    echo ""
    while true; do
      read -rs -p "  Enter wallet password (min 12 characters): " WDK_PASSWORD
      echo ""
      if [ ${#WDK_PASSWORD} -lt 12 ]; then
        echo -e "  ${RED}Error: Password must be at least 12 characters.${NC}"
        continue
      fi
      read -rs -p "  Confirm password: " WDK_PASSWORD_CONFIRM
      echo ""
      if [ "$WDK_PASSWORD" != "$WDK_PASSWORD_CONFIRM" ]; then
        echo -e "  ${RED}Error: Passwords do not match.${NC}"
        continue
      fi
      break
    done

    # Write password file
    ( umask 077; printf '%s' "$WDK_PASSWORD" > "$WDK_PASSWORD_FILE" )
    unset WDK_PASSWORD WDK_PASSWORD_CONFIRM
    ok "Password saved to $WDK_PASSWORD_FILE (permissions: 600)"
  fi

  # ── Step 5: Create encrypted wallet ────────────────────────────────────────
  step "Create WDK encrypted wallet"
  MARKET_DIR="$SCRIPT_DIR"

  VAULT_FILE="$HOME/.aurehub/.wdk_vault"
  if [ -f "$VAULT_FILE" ]; then
    ok "Vault file already exists, reading address..."
    WALLET_ADDRESS=$(node "$MARKET_DIR/swap.js" address --config-dir "$HOME/.aurehub" 2>/dev/null \
      | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).address))" 2>/dev/null) || true
    if [ -z "$WALLET_ADDRESS" ]; then
      echo -e "  ${RED}❌ Could not resolve wallet address — verify vault and password file are correct.${NC}"
      exit 1
    fi
  else
    RESULT=$(node "$MARKET_DIR/lib/create-wallet.js" --password-file "$WDK_PASSWORD_FILE" --vault-file "$VAULT_FILE")
    WALLET_ADDRESS=$(echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).address))")
  fi
  ok "Wallet address: $WALLET_ADDRESS"

  # ── Security notice: seed phrase backup ──────────────────────────────────────
  echo ""
  echo -e "  ${YELLOW}${BOLD}┌─ IMPORTANT: Back up your seed phrase ─────────────────────┐${NC}"
  echo -e "  ${YELLOW}│${NC}"
  echo -e "  ${YELLOW}│${NC} Your wallet is protected by an encrypted vault, but if"
  echo -e "  ${YELLOW}│${NC} the vault file or password is lost, ${BOLD}your funds are gone${NC}."
  echo -e "  ${YELLOW}│${NC}"
  echo -e "  ${YELLOW}│${NC} Export your 12-word seed phrase ${BOLD}now${NC} and store it safely"
  echo -e "  ${YELLOW}│${NC} (paper, hardware backup — never in cloud or chat)."
  echo -e "  ${YELLOW}│${NC}"
  echo -e "  ${YELLOW}│${NC} Run this command in a private terminal:"
  echo -e "  ${YELLOW}│${NC}"
  echo -e "  ${YELLOW}│${NC}   ${BOLD}node $MARKET_DIR/lib/export-seed.js${NC}"
  echo -e "  ${YELLOW}│${NC}"
  echo -e "  ${YELLOW}│${NC} Write down the 12 words and store offline."
  echo -e "  ${YELLOW}│${NC} ${RED}Never share your seed phrase with anyone.${NC}"
  echo -e "  ${YELLOW}│${NC}"
  echo -e "  ${YELLOW}${BOLD}└───────────────────────────────────────────────────────────┘${NC}"
  echo ""

fi

# ── Foundry wallet setup ─────────────────────────────────────────────────────
if [ "$WALLET_MODE" = "foundry" ]; then

  # ── Step 3: Foundry ──────────────────────────────────────────────────────────
  step "Check Foundry (cast)"

  if command -v cast &>/dev/null; then
    CAST_VERSION_LINE=$(cast --version | head -1)
    ok "Foundry already installed: $CAST_VERSION_LINE"
    CAST_VERSION=$(echo "$CAST_VERSION_LINE" | awk '{print $3}' | sed 's/-.*$//')
    if [ -n "$CAST_VERSION" ] && [ "$(printf '%s\n' "$CAST_VERSION" "1.6.0" | sort -V | head -1)" != "1.6.0" ]; then
      warn "Foundry version is below recommended baseline (found: $CAST_VERSION, recommended: >= 1.6.0)."
      echo -e "  You can upgrade with: ${BOLD}foundryup${NC}"
    fi
  else
    # S1: disclose what is about to run before executing curl|bash
    echo -e "\n  ${YELLOW}Foundry (cast) is not installed.${NC}"
    echo -e "  About to download and run the official Foundry installer from foundry.paradigm.xyz"
    echo -e "  Source: https://github.com/foundry-rs/foundry"
    echo
    read -rp "  Proceed with installation? [Y/n]: " CONFIRM_FOUNDRY
    if [[ "${CONFIRM_FOUNDRY:-}" =~ ^[Nn]$ ]]; then
      echo -e "  Skipped. Install Foundry manually: https://book.getfoundry.sh/getting-started/installation"
      exit 1
    fi
    echo "  Downloading Foundry installer (this may take a moment)..."
    curl -L https://foundry.paradigm.xyz | bash

    # foundryup may not be in PATH yet; add it temporarily for this session
    export PATH="$HOME/.foundry/bin:$PATH"
    echo "  Installing cast, forge, and anvil binaries (~100 MB, please wait)..."
    foundryup

    manual "Reason: Foundry writes itself to ~/.foundry/bin and appends to ~/.zshrc
(or ~/.bashrc), but the current terminal's PATH is not refreshed automatically.
The script has temporarily added Foundry to this session's PATH so setup can
continue without interruption.

After setup finishes, refresh your shell so 'cast' works in new terminals:
  $ source ~/.zshrc    # zsh users
  $ source ~/.bashrc   # bash users
Or open a new terminal window."
  fi

  # ── Step 4: Keystore password file ─────────────────────────────────────────────
  step "Prepare keystore password file"

  if [ -f ~/.aurehub/.wallet.password ] && [ -s ~/.aurehub/.wallet.password ]; then
    ok "Password file already exists and is non-empty, skipping"
  else
    if [ ! -f ~/.aurehub/.wallet.password ]; then
      ( umask 077; touch ~/.aurehub/.wallet.password )
    else
      warn "Password file exists but is empty: ~/.aurehub/.wallet.password"
    fi

    echo -e "  ${BLUE}Why this is needed:${NC} The Agent signs transactions using your Foundry"
    echo -e "  keystore. The password is stored in a protected file (chmod 600) so the"
    echo -e "  Agent can unlock the keystore without the password appearing in shell history."
    echo -e "  Password will be saved to: ${BOLD}~/.aurehub/.wallet.password${NC}"
    echo
    read -rsp "  Enter your desired keystore password: " WALLET_PASSWORD
    echo
    if [ -z "$WALLET_PASSWORD" ]; then
      echo -e "  ${RED}❌ Password cannot be empty.${NC}"; exit 1
    fi
    ( umask 077; printf '%s' "$WALLET_PASSWORD" > ~/.aurehub/.wallet.password )
    unset WALLET_PASSWORD
    ok "Password saved to ~/.aurehub/.wallet.password (permissions: 600)"
  fi

  # ── Step 5: Wallet keystore ────────────────────────────────────────────────────
  step "Configure wallet keystore"

  if cast wallet list 2>/dev/null | grep -qF "$ACCOUNT_NAME"; then
    ok "Keystore account '$ACCOUNT_NAME' already exists, skipping"
  else
    echo -e "  No keystore account '${BOLD}$ACCOUNT_NAME${NC}' found."
    echo -e "  Choose wallet initialization mode:"
    echo -e "    ${BOLD}1)${NC} Import existing private key into keystore (interactive)"
    echo -e "    ${BOLD}2)${NC} Create a brand-new keystore wallet (recommended)"
    read -rp "  Enter 1 or 2: " WALLET_CHOICE

    case "${WALLET_CHOICE:-}" in
      1)
        echo
        echo -e "  Run this in your terminal (interactive input is hidden):"
        echo -e "    cast wallet import $ACCOUNT_NAME --interactive"
        echo
        while ! cast wallet list 2>/dev/null | grep -qF "$ACCOUNT_NAME"; do
          read -rp "  Press Enter after import is complete, or type 'abort' to exit: " RETRY_INPUT
          if [[ "${RETRY_INPUT:-}" == "abort" ]]; then
            echo -e "  ${RED}Aborted.${NC}"; exit 1
          fi
        done
        ;;
      2)
        mkdir -p ~/.foundry/keystores
        WALLET_NEW_HELP=$(cast wallet new --help 2>/dev/null || true)
        if echo "$WALLET_NEW_HELP" | grep -q '\[ACCOUNT_NAME\]'; then
          if echo "$WALLET_NEW_HELP" | grep -q -- '--password-file'; then
            cast wallet new ~/.foundry/keystores "$ACCOUNT_NAME" \
              --password-file ~/.aurehub/.wallet.password
          elif echo "$WALLET_NEW_HELP" | grep -q -- '--password'; then
            echo -e "  This Foundry version does not support --password-file for wallet new."
            echo -e "  Proceeding with interactive password prompt (input hidden)."
            cast wallet new ~/.foundry/keystores "$ACCOUNT_NAME" --password
          else
            echo -e "  ${RED}❌ Unsupported 'cast wallet new' password mode in this Foundry version.${NC}"
            echo -e "  Please upgrade Foundry: ${BOLD}foundryup${NC}"
            exit 1
          fi
        else
          echo -e "  ${RED}❌ Your Foundry version does not support named account creation for 'cast wallet new'.${NC}"
          echo -e "  Please upgrade Foundry and re-run setup:"
          echo -e "    ${BOLD}foundryup${NC}"
          exit 1
        fi
        ;;
      *)
        echo -e "  ${RED}Invalid choice, exiting.${NC}"
        exit 1
        ;;
    esac

    ok "Keystore account '$ACCOUNT_NAME' is ready"
  fi

  # ── Step 6: Read wallet address ────────────────────────────────────────────────
  step "Read wallet address"

  # U6: distinguish wrong password vs other errors
  WALLET_ADDRESS=""
  CAST_ERR_FILE=$(mktemp /tmp/xaut_cast_err.XXXXXX)
  if ! WALLET_ADDRESS=$(cast wallet address \
      --account "$ACCOUNT_NAME" \
      --password-file ~/.aurehub/.wallet.password 2>"$CAST_ERR_FILE"); then
    CAST_ERR=$(cat "$CAST_ERR_FILE" 2>/dev/null || true)
    rm -f "$CAST_ERR_FILE"
    echo -e "  ${RED}❌ Could not read wallet address.${NC}"
    if echo "$CAST_ERR" | grep -qiE "password|decrypt|mac mismatch|invalid|wrong"; then
      echo -e "  Likely cause: the password in ~/.aurehub/.wallet.password does not match"
      echo -e "  the password used when this keystore was created."
      echo -e "  To fix: delete the password file and re-run this script to enter the correct one."
      echo -e "    \$ rm ~/.aurehub/.wallet.password && bash \"$0\""
    elif echo "$CAST_ERR" | grep -qiE "not found|no such file|keystore"; then
      echo -e "  Likely cause: keystore file for '$ACCOUNT_NAME' is missing."
      echo -e "  Run 'cast wallet list' to check available accounts."
    else
      echo -e "  Details: $CAST_ERR"
      echo -e "  Run 'cast wallet list' to confirm the account exists."
    fi
    exit 1
  fi
  rm -f "$CAST_ERR_FILE"
  ok "Wallet address: $WALLET_ADDRESS"

fi

# ── Step 7: Generate config files ─────────────────────────────────────────────
step "Generate config files"

# Helper: set a key in .env (update if exists, append if not)
_env_set() {
  local key="$1" value="$2" file="$HOME/.aurehub/.env"
  if grep -q "^${key}=" "$file" 2>/dev/null; then
    sed -i.bak "s|^${key}=.*|${key}=${value}|" "$file" && rm -f "$file.bak"
  else
    echo "${key}=${value}" >> "$file"
  fi
}

if [ -f ~/.aurehub/.env ]; then
  ok ".env already exists, updating wallet mode"
  _env_set "WALLET_MODE" "$WALLET_MODE"
  if [ "$WALLET_MODE" = "wdk" ]; then
    _env_set "WDK_PASSWORD_FILE" "$HOME/.aurehub/.wdk_password"
    # Remove stale Foundry keys when switching to WDK
    sed -i.bak '/^FOUNDRY_ACCOUNT=/d; /^KEYSTORE_PASSWORD_FILE=/d' ~/.aurehub/.env && rm -f ~/.aurehub/.env.bak
  else
    _env_set "FOUNDRY_ACCOUNT" "$ACCOUNT_NAME"
    _env_set "KEYSTORE_PASSWORD_FILE" "$HOME/.aurehub/.wallet.password"
    # Remove stale WDK keys when switching to Foundry
    sed -i.bak '/^WDK_PASSWORD_FILE=/d' ~/.aurehub/.env && rm -f ~/.aurehub/.env.bak
  fi
  # Ensure ETH_RPC_URL exists
  if ! grep -q "^ETH_RPC_URL=" ~/.aurehub/.env 2>/dev/null; then
    _env_set "ETH_RPC_URL" "https://eth.llamarpc.com"
  fi
  chmod 600 ~/.aurehub/.env
  ok "WALLET_MODE=$WALLET_MODE updated in .env"
else
  DEFAULT_RPC="https://eth.llamarpc.com"
  echo -e "  Ethereum node URL (press Enter to use the free public node):"
  echo -e "  Default: ${BOLD}$DEFAULT_RPC${NC}"
  echo -e "  Tip: Alchemy or Infura private nodes are more reliable. You can update"
  echo -e "  this later by editing ETH_RPC_URL in ~/.aurehub/.env"
  read -rp "  Node URL: " INPUT_RPC
  ETH_RPC_URL="${INPUT_RPC:-$DEFAULT_RPC}"

  if [ "$WALLET_MODE" = "wdk" ]; then
    cat > ~/.aurehub/.env << EOF
WALLET_MODE=$WALLET_MODE
ETH_RPC_URL=$ETH_RPC_URL
ETH_RPC_URL_FALLBACK=https://eth.merkle.io,https://rpc.flashbots.net/fast,https://eth.drpc.org,https://ethereum.publicnode.com
WDK_PASSWORD_FILE=$HOME/.aurehub/.wdk_password
# Required for limit orders only:
# UNISWAPX_API_KEY=your_api_key_here
# Optional — set during setup or first-success prompt if omitted:
# NICKNAME=YourName
EOF
  else
    cat > ~/.aurehub/.env << EOF
WALLET_MODE=$WALLET_MODE
ETH_RPC_URL=$ETH_RPC_URL
# Fallback RPCs tried in order when primary fails with a network error (429/502/timeout)
# Add a paid Alchemy/Infura node at the front for higher reliability
ETH_RPC_URL_FALLBACK=https://eth.merkle.io,https://rpc.flashbots.net/fast,https://eth.drpc.org,https://ethereum.publicnode.com
FOUNDRY_ACCOUNT=$ACCOUNT_NAME
KEYSTORE_PASSWORD_FILE=$HOME/.aurehub/.wallet.password
# Required for limit orders only:
# UNISWAPX_API_KEY=your_api_key_here
# Optional — set during setup or first-success prompt if omitted:
# NICKNAME=YourName
EOF
  fi
  chmod 600 ~/.aurehub/.env
  ok ".env generated (RPC: $ETH_RPC_URL)"
fi

if [ -f ~/.aurehub/config.yaml ]; then
  ok "config.yaml already exists, skipping"
else
  cp "$SKILL_DIR/config.example.yaml" ~/.aurehub/config.yaml
  ok "config.yaml generated"
fi

# ── Step 8: Limit order dependencies (npm + UniswapX API Key) ─────────────────
step "Limit order dependencies (npm + UniswapX API Key)"

_install_nodejs() {
  local suggestion=""
  local install_mode=""
  if [[ "$OSTYPE" == "darwin"* ]]; then
    if command -v brew &>/dev/null; then
      suggestion="brew install node"
      install_mode="brew"
    else
      suggestion=$'# Install Homebrew first: https://brew.sh\nbrew install node'
      install_mode="manual-brew"
    fi
  elif command -v apt-get &>/dev/null; then
    suggestion="sudo apt install nodejs npm"
    install_mode="apt"
  elif command -v dnf &>/dev/null; then
    suggestion="sudo dnf install nodejs"
    install_mode="dnf"
  elif command -v yum &>/dev/null; then
    suggestion="sudo yum install nodejs"
    install_mode="yum"
  else
    echo -e "  ${YELLOW}Could not detect package manager. Install Node.js >= 18 from: https://nodejs.org${NC}"
    return 1
  fi

  echo -e "  Node.js >= 18 is required for market and limit orders."
  echo -e "  Suggested install command:"
  echo -e "    ${BOLD}$(echo -e "$suggestion")${NC}"
  echo
  read -rp "  Run it now? [Y/n]: " RUN_NODE_INSTALL
  if [[ "${RUN_NODE_INSTALL:-}" =~ ^[Nn]$ ]]; then
    echo -e "  ${YELLOW}Skipped. Market and limit orders will not be available until Node.js >= 18 is installed.${NC}"
    return 1
  fi
  case "$install_mode" in
    brew)
      brew install node
      ;;
    apt)
      sudo apt install nodejs npm
      ;;
    dnf)
      sudo dnf install nodejs
      ;;
    yum)
      sudo yum install nodejs
      ;;
    manual-brew)
      echo -e "  ${YELLOW}Homebrew is not installed. Please install Homebrew first, then run: brew install node${NC}"
      return 1
      ;;
    *)
      echo -e "  ${YELLOW}Unsupported install mode. Install Node.js >= 18 from: https://nodejs.org${NC}"
      return 1
      ;;
  esac
}

# Check Node.js
NODE_OK=false
if command -v node &>/dev/null; then
  NODE_MAJOR=$(node -e 'process.stdout.write(process.version.split(".")[0].slice(1))')
  if [ "$NODE_MAJOR" -ge 18 ]; then
    ok "Node.js $(node --version)"
    NODE_OK=true
  else
    warn "Node.js version too old: $(node --version) (requires >= 18)"
    if _install_nodejs; then
      NODE_OK=true
    fi
  fi
else
  warn "Node.js not found"
  if _install_nodejs; then
    NODE_OK=true
  fi
fi

if [ "$NODE_OK" = true ]; then
  _ensure_scripts_deps

  # Prompt for UniswapX API Key with explicit choices
  CURRENT_UNISWAPX_KEY=$(grep '^UNISWAPX_API_KEY=' ~/.aurehub/.env 2>/dev/null | head -1 | cut -d= -f2- || true)
  echo
  echo -e "  ${BOLD}UniswapX API Key${NC} (required for limit orders, not needed for market orders)"
  echo -e "  Get one free (~5 min): ${BOLD}https://developers.uniswap.org/dashboard${NC}"
  echo -e "  Sign in with Google/GitHub → Generate Token (Free tier)"
  echo
  if [ -n "$CURRENT_UNISWAPX_KEY" ]; then
    echo -e "  Existing key detected."
    echo -e "    ${BOLD}1)${NC} Keep existing key (recommended)"
    echo -e "    ${BOLD}2)${NC} Replace with a new key now"
    read -rp "  Choose 1 or 2 [default: 1]: " UNISWAPX_CHOICE
    UNISWAPX_CHOICE="${UNISWAPX_CHOICE:-1}"
  else
    echo -e "  No key currently configured."
    echo -e "    ${BOLD}1)${NC} Set key now"
    echo -e "    ${BOLD}2)${NC} Skip for now (market orders still available)"
    read -rp "  Choose 1 or 2 [default: 2]: " UNISWAPX_CHOICE
    UNISWAPX_CHOICE="${UNISWAPX_CHOICE:-2}"
  fi

  if [ "$UNISWAPX_CHOICE" = "1" ] || { [ "$UNISWAPX_CHOICE" = "2" ] && [ -n "$CURRENT_UNISWAPX_KEY" ]; }; then
    if [ "$UNISWAPX_CHOICE" = "1" ] && [ -n "$CURRENT_UNISWAPX_KEY" ]; then
      ok "UNISWAPX_API_KEY unchanged"
    else
      while true; do
        read -rp "  Enter API Key: " UNISWAPX_KEY
        UNISWAPX_KEY=$(printf '%s' "$UNISWAPX_KEY" | xargs)
        if [ -z "$UNISWAPX_KEY" ]; then
          echo -e "  ${YELLOW}Key cannot be empty. Enter a key or press Ctrl+C to abort.${NC}"
          continue
        fi
        if [ ${#UNISWAPX_KEY} -lt 10 ]; then
          echo -e "  ${YELLOW}Key looks too short. Please verify and try again.${NC}"
          continue
        fi
        ENV_TMP_FILE=$(umask 077; mktemp /tmp/xaut_env.XXXXXX)
        grep -v '^UNISWAPX_API_KEY=' ~/.aurehub/.env > "$ENV_TMP_FILE" 2>/dev/null || true
        mv "$ENV_TMP_FILE" ~/.aurehub/.env
        echo "UNISWAPX_API_KEY=$UNISWAPX_KEY" >> ~/.aurehub/.env
        chmod 600 ~/.aurehub/.env
        unset UNISWAPX_KEY
        ok "UNISWAPX_API_KEY saved to ~/.aurehub/.env"
        break
      done
    fi
  else
    ok "Skipped (add UNISWAPX_API_KEY to ~/.aurehub/.env later if needed)"
  fi
else
  warn "Market and limit orders unavailable (Node.js not installed). Re-run setup.sh after installing Node.js >= 18."
fi

# ── Step 9: Activity rankings (optional) ─────────────────────────────────────
step "Activity rankings (optional)"

echo -e "  Would you like to join the XAUT trade activity rankings?"
echo -e "  This will share your ${BOLD}wallet address${NC} and a ${BOLD}nickname${NC} with https://xaue.com"
echo -e "  You can change this anytime by editing ~/.aurehub/.env"
echo
read -rp "  Join rankings? [y/N]: " JOIN_RANKINGS
if [[ "${JOIN_RANKINGS:-}" =~ ^[Yy]$ ]]; then
  read -rp "  Enter your nickname: " RANKINGS_NICKNAME
  if [ -n "$RANKINGS_NICKNAME" ]; then
    _env_set "RANKINGS_OPT_IN" "true"
    _env_set "NICKNAME" "$RANKINGS_NICKNAME"
    ok "Rankings enabled (nickname: $RANKINGS_NICKNAME)"
  else
    _env_set "RANKINGS_OPT_IN" "false"
    ok "Rankings skipped (empty nickname)"
  fi
else
  _env_set "RANKINGS_OPT_IN" "false"
  ok "Rankings skipped"
fi

# ── Step 10: Verification ───────────────────────────────────────────────────────
step "Verify environment"

# WALLET_MODE is always set from Step 2. ETH_RPC_URL is set in the fresh-install
# path but NOT in the re-run path — read it from .env if unset.
if [ -z "${ETH_RPC_URL:-}" ]; then
  ETH_RPC_URL=$(grep '^ETH_RPC_URL=' ~/.aurehub/.env 2>/dev/null | head -1 | cut -d= -f2-)
fi

if [ "$WALLET_MODE" = "wdk" ]; then
  # Verify RPC connectivity using node
  if BLOCK=$(node "$SCRIPT_DIR/swap.js" address 2>/dev/null | head -1); then
    ok "WDK wallet accessible"
  else
    warn "Could not verify WDK wallet (market module may not be fully set up yet)"
  fi

  # RPC check via cast if available, otherwise curl
  if command -v cast &>/dev/null; then
    if BLOCK=$(cast block-number --rpc-url "$ETH_RPC_URL" 2>/dev/null); then
      ok "RPC reachable (latest block #$BLOCK)"
    else
      echo -e "  ${RED}❌ RPC check failed — ETH_RPC_URL is unreachable: $ETH_RPC_URL${NC}"
      echo -e "  Fix: edit ~/.aurehub/.env and set a valid ETH_RPC_URL, then re-run this script."
      echo -e "  Free public nodes: https://chainlist.org/chain/1"
      exit 1
    fi
  else
    # Fallback: use node to check RPC
    if ETH_RPC_URL="$ETH_RPC_URL" node -e "const u=process.env.ETH_RPC_URL;fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'eth_blockNumber',params:[],id:1})}).then(r=>r.json()).then(d=>{if(d.result)process.exit(0);else process.exit(1)}).catch(()=>process.exit(1))" 2>/dev/null; then
      ok "RPC reachable"
    else
      echo -e "  ${RED}❌ RPC check failed — ETH_RPC_URL is unreachable: $ETH_RPC_URL${NC}"
      echo -e "  Fix: edit ~/.aurehub/.env and set a valid ETH_RPC_URL, then re-run this script."
      echo -e "  Free public nodes: https://chainlist.org/chain/1"
      exit 1
    fi
  fi

  [ -r "$HOME/.aurehub/.wdk_password" ] \
    && ok "WDK password file readable" \
    || { echo -e "  ${RED}❌ WDK password file not readable${NC}"; exit 1; }

  [ -f "$HOME/.aurehub/.wdk_vault" ] \
    && ok "WDK vault file exists" \
    || { echo -e "  ${RED}❌ WDK vault file not found${NC}"; exit 1; }

else
  # Foundry verification
  cast --version | head -1 | xargs -I{} echo "  ✓ {}"

  # U8: make RPC failure a hard stop instead of a warning
  if BLOCK=$(cast block-number --rpc-url "$ETH_RPC_URL" 2>/dev/null); then
    ok "RPC reachable (latest block #$BLOCK)"
  else
    echo -e "  ${RED}❌ RPC check failed — ETH_RPC_URL is unreachable: $ETH_RPC_URL${NC}"
    echo -e "  Fix: edit ~/.aurehub/.env and set a valid ETH_RPC_URL, then re-run this script."
    echo -e "  Free public nodes: https://chainlist.org/chain/1"
    exit 1
  fi

  cast wallet list 2>/dev/null | grep -qF "$ACCOUNT_NAME" \
    && ok "Keystore account exists" \
    || { echo -e "  ${RED}❌ Account not found${NC}"; exit 1; }

  [ -r ~/.aurehub/.wallet.password ] \
    && ok "Password file readable" \
    || { echo -e "  ${RED}❌ Password file not readable${NC}"; exit 1; }
fi

# ── Completion summary ─────────────────────────────────────────────────────────
echo -e "\n${GREEN}${BOLD}━━━ Automated setup complete ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "  Wallet mode:    ${BOLD}$WALLET_MODE${NC}"
echo -e "  Wallet address: ${BOLD}$WALLET_ADDRESS${NC}"
if [ "$NODE_OK" = true ] && grep -q '^UNISWAPX_API_KEY=.\+' ~/.aurehub/.env 2>/dev/null; then
  echo -e "  Market orders: ${GREEN}READY${NC}"
  echo -e "  Limit orders:  ${GREEN}READY${NC}"
elif [ "$NODE_OK" = true ]; then
  echo -e "  Market orders: ${GREEN}READY${NC}"
  echo -e "  Limit orders:  ${YELLOW}NOT READY${NC} (requires UNISWAPX_API_KEY)"
else
  echo -e "  Market orders: ${YELLOW}NOT READY${NC} (requires Node.js >= 18)"
  echo -e "  Limit orders:  ${YELLOW}NOT READY${NC} (requires Node.js >= 18 and UNISWAPX_API_KEY)"
fi
echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"

echo -e "\n${YELLOW}${BOLD}The following steps require manual action (the script cannot do them for you):${NC}"

echo -e "\n  ${BOLD}1. Fund the wallet with ETH (required for gas)${NC}"
echo -e "     Reason: on-chain operations consume gas; the script cannot transfer funds."
echo -e "     Minimum: ≥ 0.005 ETH"
echo -e "     Wallet address: ${BOLD}$WALLET_ADDRESS${NC}"

echo -e "\n  ${BOLD}2. Fund the wallet with trading capital (as needed)${NC}"
echo -e "     Buy XAUT  → deposit USDT to the wallet"
echo -e "     Sell XAUT → deposit XAUT to the wallet"
echo -e "     Same address: ${BOLD}$WALLET_ADDRESS${NC}"

echo -e "\n  ${BOLD}3. Get a UniswapX API Key (limit orders only — skip if you already entered one above)${NC}"
echo -e "     Reason: the UniswapX API requires authentication; the script cannot register on your behalf."
echo -e "     How to get one (about 5 minutes, free):"
echo -e "       a. Visit https://developers.uniswap.org/dashboard"
echo -e "       b. Sign in with Google or GitHub"
echo -e "       c. Generate a Token (Free tier)"
echo -e "     Then add it to your config:"
echo -e "       \$ echo 'UNISWAPX_API_KEY=your_key' >> ~/.aurehub/.env"

echo -e "\n${BLUE}Once the steps above are done, send any trade instruction to the Agent to begin.${NC}\n"

# ── Save setup script path for future re-runs ──────────────────────────────────
printf '%s\n' "$SCRIPT_DIR/setup.sh" > ~/.aurehub/.setup_path

```

### scripts/swap.js

```javascript
/**
 * swap.js — CLI entry point for market operations.
 *
 * Usage:
 *   node swap.js <command> [options]
 *
 * Commands:
 *   address    — output wallet address
 *   balance    — output ETH, USDT, XAUT balances
 *   allowance  — output ERC-20 allowance  (requires --token)
 *   quote      — get a Uniswap V3 quote   (requires --side, --amount)
 *   approve    — approve a token spender  (requires --token, --amount)
 *   swap       — execute a swap           (requires --side, --amount, --min-out)
 */

import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { formatUnits, Interface } from 'ethers6';
import { loadConfig, resolveToken, validateContracts } from './lib/config.js';
import { createProvider } from './lib/provider.js';
import { createSigner } from './lib/signer.js';
import { getBalance, getAllowance, approve } from './lib/erc20.js';
import { quote, buildSwap } from './lib/uniswap.js';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const VALID_COMMANDS = new Set(['quote', 'balance', 'allowance', 'approve', 'swap', 'address', 'sign', 'cancel-nonce']);

// ---------------------------------------------------------------------------
// CLI argument parser — exported for unit-testing without RPC
// ---------------------------------------------------------------------------

/**
 * Parse raw argv (everything after "node swap.js") into a structured object.
 *
 * @param {string[]} argv  e.g. ['quote', '--side', 'buy', '--amount', '100']
 * @returns {{ command: string, side?: string, amount?: string, minOut?: string, token?: string, configDir?: string }}
 */
export function parseCliArgs(argv) {
  const [command, ...rest] = argv;

  if (!VALID_COMMANDS.has(command)) {
    throw new Error(`Unknown command: "${command}". Valid commands: ${[...VALID_COMMANDS].join(', ')}`);
  }

  const parsed = { command };

  for (let i = 0; i < rest.length; i++) {
    const flag = rest[i];
    const value = rest[i + 1];

    switch (flag) {
      case '--side':
        parsed.side = value;
        i++;
        break;
      case '--amount':
        parsed.amount = value;
        i++;
        break;
      case '--min-out':
        parsed.minOut = value;
        i++;
        break;
      case '--token':
        parsed.token = value;
        i++;
        break;
      case '--config-dir':
        parsed.configDir = value;
        i++;
        break;
      case '--data-file':
        parsed.dataFile = value;
        i++;
        break;
      case '--word-pos':
        parsed.wordPos = value;
        i++;
        break;
      case '--mask':
        parsed.mask = value;
        i++;
        break;
      case '--spender':
        parsed.spender = value;
        i++;
        break;
      default:
        // Ignore unknown flags silently
        break;
    }
  }

  if ((command === 'quote' || command === 'swap') && parsed.side) {
    const normalized = String(parsed.side).trim().toLowerCase();
    if (normalized !== 'buy' && normalized !== 'sell') {
      throw new Error(`Invalid --side value "${parsed.side}". Expected "buy" or "sell".`);
    }
    parsed.side = normalized;
  }

  return parsed;
}

// ---------------------------------------------------------------------------
// Subcommand implementations
// ---------------------------------------------------------------------------

async function runAddress(cfg, provider) {
  const signer = await createSigner(cfg, provider ? provider.getEthersProvider() : null);
  console.log(JSON.stringify({ address: signer.address }, null, 2));
}

async function runBalance(cfg, provider) {
  const signer = await createSigner(cfg, provider ? provider.getEthersProvider() : null);
  const address = signer.address;

  const tokens = cfg.yaml?.tokens ?? {};
  const usdtToken = resolveToken(cfg, 'USDT');
  const xautToken = resolveToken(cfg, 'XAUT');

  const [usdtBalance, xautBalance, ethBalanceRaw] = await Promise.all([
    getBalance(usdtToken, address, provider),
    getBalance(xautToken, address, provider),
    provider.getBalance(address),
  ]);

  // ethBalanceRaw is a hex string from the raw JSON-RPC; convert to ETH string
  const ethBig = BigInt(ethBalanceRaw);
  const ethBalance = formatUnits(ethBig, 18);

  console.log(JSON.stringify({ address, ETH: ethBalance, USDT: usdtBalance, XAUT: xautBalance }, null, 2));
}

async function runAllowance(cfg, provider, args) {
  if (!args.token) throw new Error('--token is required for allowance');

  const signer = await createSigner(cfg, provider ? provider.getEthersProvider() : null);
  const address = signer.address;

  const token = resolveToken(cfg, args.token);
  const contracts = cfg.yaml?.contracts ?? {};
  const spender = args.spender || contracts.router;
  if (!spender) throw new Error('--spender or contracts.router must be set');

  const allowance = await getAllowance(token, address, spender, provider);
  console.log(JSON.stringify({ address, token: args.token, allowance, spender }, null, 2));
}

async function runQuote(cfg, provider, args) {
  if (!args.side) throw new Error('--side is required for quote');
  if (!args.amount) throw new Error('--amount is required for quote');
  if (args.side !== 'buy' && args.side !== 'sell') throw new Error('--side must be "buy" or "sell"');

  // Resolve pair: buy = USDT→XAUT, sell = XAUT→USDT
  const isBuy = args.side === 'buy';
  const tokenIn = resolveToken(cfg, isBuy ? 'USDT' : 'XAUT');
  const tokenOut = resolveToken(cfg, isBuy ? 'XAUT' : 'USDT');

  const fee = _resolveFee(cfg, isBuy ? 'USDT' : 'XAUT', isBuy ? 'XAUT' : 'USDT');
  const contracts = cfg.yaml?.contracts ?? {};
  if (!contracts.quoter) throw new Error('contracts.quoter not set in config.yaml');

  const result = await quote({
    tokenIn,
    tokenOut,
    amountIn: args.amount,
    fee,
    contracts,
    provider,
  });

  // Convert bigints to strings for JSON output
  console.log(JSON.stringify({
    side: args.side,
    amountIn: args.amount,
    amountOut: result.amountOut,
    amountOutRaw: result.amountOutRaw.toString(),
    sqrtPriceX96: result.sqrtPriceX96.toString(),
    gasEstimate: result.gasEstimate.toString(),
  }, null, 2));
}

async function runApprove(cfg, provider, args) {
  if (!args.token) throw new Error('--token is required for approve');
  if (!args.amount) throw new Error('--amount is required for approve');

  const signer = await createSigner(cfg, provider ? provider.getEthersProvider() : null);
  const token = resolveToken(cfg, args.token);
  const contracts = cfg.yaml?.contracts ?? {};
  const spender = args.spender || contracts.router;
  if (!spender) throw new Error('--spender or contracts.router must be set');

  // Check token_rules for requiresResetApprove; skip reset if allowance is already 0
  const tokenRules = cfg.yaml?.token_rules ?? {};
  const rules = tokenRules[args.token] ?? {};
  let requiresResetApprove = rules.requires_reset_approve ?? false;
  if (requiresResetApprove && provider) {
    const currentAllowance = await getAllowance(token, signer.address, spender, provider);
    if (parseFloat(currentAllowance) === 0) requiresResetApprove = false;
  }

  const result = await approve(token, spender, args.amount, signer, { requiresResetApprove, fallbackProvider: provider });

  console.log(JSON.stringify({ address: signer.address, token: args.token, amount: args.amount, spender, txHash: result.hash }, null, 2));
}

async function runSwap(cfg, provider, args) {
  if (!args.side) throw new Error('--side is required for swap');
  if (!args.amount) throw new Error('--amount is required for swap');
  if (!args.minOut) throw new Error('--min-out is required for swap');
  if (!args.minOut || Number.isNaN(Number(args.minOut)) || Number(args.minOut) <= 0) throw new Error('--min-out must be a positive number greater than 0');
  if (args.side !== 'buy' && args.side !== 'sell') throw new Error('--side must be "buy" or "sell"');

  const signer = await createSigner(cfg, provider ? provider.getEthersProvider() : null);
  const address = signer.address;

  const isBuy = args.side === 'buy';
  const tokenIn = resolveToken(cfg, isBuy ? 'USDT' : 'XAUT');
  const tokenOut = resolveToken(cfg, isBuy ? 'XAUT' : 'USDT');

  const fee = _resolveFee(cfg, isBuy ? 'USDT' : 'XAUT', isBuy ? 'XAUT' : 'USDT');
  const contracts = cfg.yaml?.contracts ?? {};
  if (!contracts.router) throw new Error('contracts.router not set in config.yaml');

  const risk = cfg.yaml?.risk ?? {};
  const deadline = Math.floor(Date.now() / 1000) + (risk.deadline_seconds ?? 300);

  const tx = buildSwap({
    tokenIn,
    tokenOut,
    amountIn: args.amount,
    minAmountOut: args.minOut,
    fee,
    recipient: address,
    deadline,
    contracts,
  });

  const timeoutMs = (risk.deadline_seconds ?? 300) * 1000;

  // Send transaction — if this throws, the tx was never broadcast (safe to retry)
  const sentTx = await signer.sendTransaction(tx);

  // Use fallback-aware receipt wait when available (RPC resilience)
  const waitFn = provider?.waitForTransaction
    ? provider.waitForTransaction(sentTx.hash, 1, timeoutMs)
    : sentTx.wait();

  // Wait for confirmation — if this throws, the tx WAS broadcast but confirmation
  // failed (RPC error, timeout). Output txHash so caller can verify before retrying.
  let receipt;
  let confirmTimer;
  try {
    receipt = await Promise.race([
      waitFn,
      new Promise((_, reject) => {
        confirmTimer = setTimeout(() => reject(new Error(
          `Transaction not confirmed within ${timeoutMs / 1000}s (txHash: ${sentTx.hash}). It may still be pending — check on Etherscan.`
        )), timeoutMs);
      }),
    ]);
  } catch (confirmErr) {
    clearTimeout(confirmTimer);
    console.log(JSON.stringify({
      address,
      side: args.side,
      amountIn: args.amount,
      minAmountOut: args.minOut,
      txHash: sentTx.hash,
      status: 'unconfirmed',
      warning: `Transaction was broadcast but confirmation failed: ${confirmErr.message}. Check balance or Etherscan before retrying — the swap may have succeeded.`,
    }, null, 2));
    process.exit(1);
  }
  clearTimeout(confirmTimer);

  if (!receipt) {
    console.log(JSON.stringify({
      address,
      side: args.side,
      amountIn: args.amount,
      minAmountOut: args.minOut,
      txHash: sentTx.hash,
      status: 'unknown',
      warning: 'Transaction receipt is null — tx may have been dropped from mempool. Check on Etherscan.',
    }, null, 2));
    process.exit(1);
  }
  const failed = receipt.status !== 1;
  const result = {
    address,
    side: args.side,
    amountIn: args.amount,
    minAmountOut: args.minOut,
    txHash: sentTx.hash,
    status: failed ? 'failed' : 'success',
    gasUsed: receipt.gasUsed.toString(),
  };
  if (failed) {
    result.warning = 'Swap failed. A token approval may still be active — revoke it if you do not intend to retry.';
  }
  console.log(JSON.stringify(result, null, 2));
  if (failed) process.exit(1);
}

async function runSign(cfg, args) {
  if (!args.dataFile) throw new Error('--data-file is required for sign');

  const raw = readFileSync(args.dataFile, 'utf8');
  const { domain, types, message } = JSON.parse(raw);

  // ethers v6 derives EIP712Domain automatically — strip it to avoid conflicts
  const cleanTypes = { ...types };
  delete cleanTypes.EIP712Domain;

  const signer = await createSigner(cfg, null);
  const signature = await signer.signTypedData(domain, cleanTypes, message);
  // Output raw signature string (no JSON) to match cast wallet sign format
  process.stdout.write(signature);
}

async function runCancelNonce(cfg, provider, args) {
  if (!args.wordPos) throw new Error('--word-pos is required for cancel-nonce');
  if (!args.mask) throw new Error('--mask is required for cancel-nonce');

  // Validate wordPos and mask as valid uint256 values
  let wordPosBig, maskBig;
  try { wordPosBig = BigInt(args.wordPos); } catch { throw new Error(`--word-pos is not a valid integer: ${args.wordPos}`); }
  try { maskBig = BigInt(args.mask); } catch { throw new Error(`--mask is not a valid integer: ${args.mask}`); }
  if (wordPosBig < 0n || wordPosBig >= (1n << 248n)) throw new Error(`--word-pos out of range: ${args.wordPos}`);
  if (maskBig <= 0n || maskBig >= (1n << 256n)) throw new Error(`--mask out of range: ${args.mask}`);

  const signer = await createSigner(cfg, provider ? provider.getEthersProvider() : null);

  const permit2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
  const iface = new Interface(['function invalidateUnorderedNonces(uint256 wordPos, uint256 mask)']);
  const data = iface.encodeFunctionData('invalidateUnorderedNonces', [wordPosBig, maskBig]);

  const tx = await signer.sendTransaction({ to: permit2, data });
  const receipt = await tx.wait();

  if (!receipt) {
    console.log(JSON.stringify({
      address: signer.address,
      txHash: tx.hash,
      status: 'unknown',
      warning: 'Transaction receipt is null — tx may have been dropped from mempool. Check on Etherscan.',
    }, null, 2));
    process.exit(1);
  }

  const failed = receipt.status !== 1;
  console.log(JSON.stringify({
    address: signer.address,
    txHash: tx.hash,
    status: failed ? 'failed' : 'success',
  }, null, 2));
  if (failed) process.exit(1);
}

// ---------------------------------------------------------------------------
// Helper: resolve pool fee from config.yaml pairs
// ---------------------------------------------------------------------------

function _resolveFee(cfg, symbolIn, symbolOut) {
  // Note: resolveToken() already restricts symbols to known tokens (USDT, XAUT),
  // so this function is only called with whitelisted pairs. The default fallback
  // exists as a safety net but should never be reached in normal operation.
  const pairs = cfg.yaml?.pairs ?? [];
  for (const pair of pairs) {
    if (!pair.enabled) continue;
    if (
      (pair.token_in === symbolIn && pair.token_out === symbolOut) ||
      (pair.token_in === symbolOut && pair.token_out === symbolIn)
    ) {
      return pair.fee_tier;
    }
  }
  // Default to 3000 (0.3%) if no matching pair found
  return 3000;
}

// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------

const isDirectRun =
  process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];

if (isDirectRun) {
  const argv = process.argv.slice(2);

  let parsed;
  try {
    parsed = parseCliArgs(argv);
  } catch (err) {
    console.error(JSON.stringify({ error: err.message }));
    process.exit(1);
  }

  const cfg = loadConfig(parsed.configDir);
  validateContracts(cfg);
  const COMMANDS_NEEDING_PROVIDER = new Set([
    'address',
    'balance',
    'allowance',
    'quote',
    'approve',
    'swap',
    'cancel-nonce',
  ]);

  (async () => {
    try {
      const provider = COMMANDS_NEEDING_PROVIDER.has(parsed.command)
        ? createProvider(cfg.env)
        : null;

      switch (parsed.command) {
        case 'address':
          await runAddress(cfg, provider);
          break;
        case 'balance':
          await runBalance(cfg, provider);
          break;
        case 'allowance':
          await runAllowance(cfg, provider, parsed);
          break;
        case 'quote':
          await runQuote(cfg, provider, parsed);
          break;
        case 'approve':
          await runApprove(cfg, provider, parsed);
          break;
        case 'swap':
          await runSwap(cfg, provider, parsed);
          break;
        case 'sign':
          await runSign(cfg, parsed);
          break;
        case 'cancel-nonce':
          await runCancelNonce(cfg, provider, parsed);
          break;
      }
      process.exit(0);
    } catch (err) {
      console.error(JSON.stringify({ error: err.message }));
      process.exit(1);
    }
  })();
}

```