Skill: 0xardi/clawlett (version 0.4.1)

# Clawlett Secure token swaps and Trenches trading on **Base Mainnet**, powered by Safe + Zodiac Roles. Swap engines: - **KyberSwap** (DEX aggregator for optimal routes) — default - **CoW Protocol** (MEV-protected batch auctions) Token creation & trading: **Trenches** (via AgentKeyFactoryV3). > **Network: Base Mainnet (Chain ID: 8453)** ## Overview This skill enables autonomous token swaps and Trenches token creation/trading through a Gnosis Safe. The agent operates through Zodiac Roles which restricts operations to: - Swapping tokens via KyberSwap Aggregator (default) or CoW Protocol - Creating tokens on Trenches - Buying and selling Trenches tokens via factory - Approving tokens for KyberSwap Router and CoW Vault Relayer - Executing swaps via ZodiacHelpers delegatecall - Wrapping ETH to WETH and unwrapping WETH to ETH via ZodiacHelpers - Sending swapped tokens only back to the Safe (no draining) ## Execution Policy **CRITICAL: The agent MUST NEVER execute an on-chain transaction unless the user explicitly asks for it.** The default behavior for any swap, token creation, or trade request is **quote/preview only**. The agent should show the details and wait for the user to explicitly confirm execution (e.g., "execute", "do it", "go ahead", "yes, swap"). - "Swap 0.1 ETH for USDC" → get a quote and display it, do NOT execute - "Swap 0.1 ETH for USDC and execute" → get a quote, display it, then execute - Showing a quote and asking "should I execute?" is fine, but the agent must wait for an affirmative response before executing This applies to all on-chain operations: swaps, token creation, buys, sells, wrapping/unwrapping. ## Capabilities | Action | Autonomous | Notes | |--------|------------|-------| | Check balances | ✅ | ETH and any ERC20 on Base Mainnet | | Get swap quote (CoW) | ✅ | Via CoW Protocol (MEV-protected) | | Get swap quote (Kyber) | ✅ | Via KyberSwap Aggregator (best routes) | | Swap tokens (CoW) | ⚠️ | Requires explicit user confirmation | | Swap tokens (Kyber) | ⚠️ | Requires explicit user confirmation | | Wrap/Unwrap ETH | ⚠️ | Requires explicit user confirmation | | Approve tokens | ⚠️ | Only for CoW Vault Relayer and KyberSwap Router; requires explicit user confirmation | | Create token (Trenches) | ⚠️ | Requires explicit user confirmation | | Buy tokens (Trenches) | ⚠️ | Requires explicit user confirmation | | Sell tokens (Trenches) | ⚠️ | Requires explicit user confirmation | | Token info | ✅ | Fetch token details from Trenches API | | Token discovery | ✅ | Trending, new, top volume, gainers, losers | | Transfer funds | ❌ | Blocked by Roles | ## Agent Name (CNS) Each agent can optionally register a **unique name** via the Clawlett Name Service (CNS). This name is the agent's app-wide identifier — no two agents can share the same name. The name is minted as an NFT on Base. Pass `--name` during initialization to register a CNS name. If omitted, CNS registration is skipped. Once registered, it cannot be changed. ## Token Safety ### Verified Tokens Protected tokens can ONLY resolve to verified Base Mainnet addresses: | Token | Verified Address | |-------|--------------------| | ETH | Native ETH ([REDACTED_SECRET]) | | WETH | [REDACTED_SECRET] | | USDC | [REDACTED_SECRET] | | USDT | [REDACTED_SECRET] | | DAI | [REDACTED_SECRET] | | USDS | [REDACTED_SECRET] | | AERO | [REDACTED_SECRET] | | cbBTC | [REDACTED_SECRET] | | VIRTUAL | [REDACTED_SECRET] | | DEGEN | [REDACTED_SECRET] | | BRETT | [REDACTED_SECRET] | | TOSHI | [REDACTED_SECRET] | | WELL | [REDACTED_SECRET] | | BID | [REDACTED_SECRET] | If a scam token impersonates these symbols, the agent will detect and warn. ### Unverified Token Search Tokens not in the verified list are searched via DexScreener (Base pairs). Search results include: - Contract address (on-chain verified) - 24h trading volume and liquidity - DEX where the token trades **Agent behavior for unverified tokens:** - Always display the warning with contract address, volume, and liquidity - Ask the user to confirm before proceeding with the swap - Never silently swap an unverified token ## Setup 1. Owner provides their wallet address (and optionally an **agent name**) 2. Agent generates keypair → **Owner sends at least 0.0001 ETH on Base Mainnet** to agent for gas (0.001 ETH recommended to cover all deployment transactions) 3. Agent deploys Safe on Base Mainnet (agent as initial owner) 4. Agent deploys Zodiac Roles module 5. Agent configures Roles permissions via MultiSend (enable module, scope targets, assign roles) 6. Agent registers with backend API 7. Agent optionally mints CNS name on-chain (if `--name` provided) 8. Agent transfers Safe ownership to human owner and removes itself as owner (keeps Roles access) 9. Agent registers on ERC-8004 Identity Registry and transfers identity NFT to Safe 10. **Owner funds Safe on Base Mainnet** with tokens to trade ## Usage ### Initialize ``` Initialize my wallet with owner 0x123... Initialize my wallet with owner 0x123... and name MYAGENT ``` ### Check Balance ``` What's my balance? How much USDC do I have? ``` ### Swap Tokens ``` Swap 0.1 ETH for USDC Swap 100 USDC for ETH Exchange 50 DAI to AERO ``` Two swap engines are available: **KyberSwap Aggregator** (default): - Finds optimal routes across multiple DEXs - Native ETH supported directly (no wrapping needed) - Best for finding the best price across fragmented liquidity - 0.1% partner fee **CoW Protocol**: - MEV-protected batch auctions - ETH is automatically wrapped to WETH (CoW requires ERC20s) - Best for larger swaps where MEV protection matters - 0.5% partner fee To use CoW Protocol explicitly: ``` Swap 0.1 ETH for USDC using CoW Use cow to swap 100 USDC for ETH ``` ### Wrap/Unwrap ETH ``` Wrap 0.5 ETH to WETH Unwrap 0.5 WETH to ETH ``` Wrapping and unwrapping is done via ZodiacHelpers delegatecall. When swapping from ETH via CoW, wrapping is handled automatically as part of the swap transaction. ### Trenches Trading Trenches enables token creation and trading on Base. Tokens are created and traded via the AgentKeyFactoryV3 contract. All on-chain operations go through ZodiacHelpers wrapper functions (`createViaFactory`, `tradeViaFactory`) which validate the factory address and forward calls with explicit `ethValue` (since `msg.value` doesn't work in delegatecall). ``` Create a token called "My Token" with symbol MTK Create a token paired with BID (default base token) Create a token with anti-bot disabled and an initial buy Buy 0.01 ETH worth of MTK on Trenches Buy 100 BID worth of CLAWLETT on Trenches Buy all my BID into CLAWLETT Sell all my MTK tokens What's trending on Trenches? Show me the top gainers Get info on MTK token ``` **IMPORTANT — Token creation parameter collection:** When a user asks to create a token, the agent MUST collect ALL of the following parameters before executing. If the user's request is missing any of these, ask them to provide the missing values. Do NOT use defaults silently — always confirm each parameter with the user. | Parameter | Required | Description | |-----------|----------|-------------| | Name | **Yes** | The token name (e.g., "My Token") | | Symbol | **Yes** | The token ticker symbol (e.g., MTK) | | Description | **Yes** | A description of the token | | Image | **Yes** | Path to a token image file (PNG/JPEG/WEBP, max 4MB) | | Base token | No | `BID` (default) or `ETH` — which token to pair with | | Anti-bot protection | No | ON by default (10-minute sniper protection). Ask user if they want it enabled or disabled | | Initial buy | No | Amount of base token (ETH or BID, depending on pair) to buy immediately after creation (only works with anti-bot OFF) | | Twitter | No | Twitter/X handle for the token | | Website | No | Website URL for the token | | Team allocation | No | SSL team positions that the team can claim after the price moves beyond a given position | The agent should present the user with a summary of all parameters (including defaults) and ask for confirmation before executing the creation. **Token creation defaults:** - Base token: BID (use `--base-token ETH` for ETH pairing) - Anti-bot protection: ON (10-minute sniper protection window) - Initial buy is blocked when anti-bot is enabled (agent can't buy during protection window) - Use `--no-antibot` to disable protection and allow initial buy - Use `--image` to attach a custom token image (PNG/JPEG/WEBP, max 4MB) **Image upload flow:** - Images are uploaded to `/api/skill/image` before token creation - The returned `imageUrl` is passed to the token creation API - If image upload fails, the token creation will fail (image is required) **Anti-bot protection and buying:** - The agent cannot buy any token that has anti-bot protection currently active (within the 10-minute window after creation) - This applies to all tokens, not just ones the agent created - Both the client and backend enforce this — the backend will refuse to issue a swap signature for protected tokens - Wait for the protection window to expire before buying Token creation flow: 1. Upload token image via `/api/skill/image` (returns `imageUrl`) 2. Get creation signature from `/api/skill/token/create` (includes `imageUrl`) 3. **Display token details — do NOT execute yet** 4. **STOP and wait for the user to explicitly confirm** — do not proceed without confirmation 5. Only after user confirms: execute via Safe + Roles (ZodiacHelpers delegatecall) 6. **After creation, share the token page URL:** `https://trenches.bid/tokens/[address]` Swap flow: 1. Resolve token symbols (with scam protection) 2. Get quote from KyberSwap (default) or CoW Protocol 3. **Display swap details (quote only — do NOT execute yet):** - Token symbols (e.g., ETH → USDC) - Token addresses (verified Base Mainnet contracts) - Input amount (what you're selling) - Output amount (estimated amount you'll receive) - Fee breakdown 4. **STOP and wait for the user to explicitly ask to execute** — do not proceed without confirmation 5. Only after user confirms: execute via Safe + Roles ## Scripts | Script | Description | |--------|-------------| | `initialize.js` | Deploy Safe + Roles, register CNS name | | `swap.js` | Swap tokens via KyberSwap Aggregator (default, optimal routes) | | `cow.js` | Swap tokens via CoW Protocol (MEV-protected) | | `balance.js` | Check ETH and token balances | | `trenches.js` | Create and trade Trenches tokens via factory | ### Examples ```bash # Initialize (name is optional, registers on CNS if provided) node scripts/initialize.js --owner 0x123... node scripts/initialize.js --owner 0x123... --name MYAGENT # Check balance node scripts/balance.js node scripts/balance.js --token USDC # Swap tokens (KyberSwap Aggregator, default - optimal routes) node scripts/swap.js --from ETH --to USDC --amount 0.1 node scripts/swap.js --from USDC --to ETH --amount 100 --execute node scripts/swap.js --from DAI --to AERO --amount 50 --execute --slippage 1 # Swap tokens (CoW Protocol, MEV-protected) node scripts/cow.js --from ETH --to USDC --amount 0.1 node scripts/cow.js --from USDC --to WETH --amount 100 --execute node scripts/cow.js --from USDC --to DAI --amount 50 --execute --timeout 600 # With custom slippage (0-0.5 range, e.g., 0.05 = 5%) node scripts/cow.js --from ETH --to USDC --amount 0.1 --slippage 0.03 --execute # Trenches: Create a token (BID base token by default, anti-bot ON) node scripts/trenches.js create --name "My Token" --symbol MTK --description "A cool token" node scripts/trenches.js create --name "My Token" --symbol MTK --description "desc" --base-token ETH node scripts/trenches.js create --name "My Token" --symbol MTK --description "desc" --no-antibot --initial-buy 0.01 node scripts/trenches.js create --name "My Token" --symbol MTK --description "desc" --image ./logo.png # Trenches: Buy/sell tokens (amount is in base token: ETH or BID depending on pair) node scripts/trenches.js buy --token MTK --amount 0.01 node scripts/trenches.js buy --token CLAWLETT --all node scripts/trenches.js sell --token MTK --amount 1000 node scripts/trenches.js sell --token MTK --all # Trenches: Token info and discovery node scripts/trenches.js info MTK node scripts/trenches.js trending node scripts/trenches.js trending --window 1h --limit 5 node scripts/trenches.js new node scripts/trenches.js top-volume node scripts/trenches.js gainers node scripts/trenches.js losers ``` ## Configuration Scripts read from `config/wallet.json` (configured for Base Mainnet): ```json { "chainId": 8453, "owner": "0x...", "agent": "0x...", "safe": "0x...", "roles": "0x...", "roleKey": "0x...", "name": "MYAGENT", "cnsTokenId": 1 } ``` ### Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `BASE_RPC_URL` | `https://mainnet.base.org` | Base Mainnet RPC endpoint | | `WALLET_CONFIG_DIR` | `config` | Config directory | | `TRENCHES_API_URL` | `https://trenches.bid` | Trenches API endpoint | ## Contracts (Base Mainnet) | Contract | Address | Description | |----------|---------|-------------| | Safe Singleton | [REDACTED_SECRET] | Safe L2 impl | | CoW Settlement | [REDACTED_SECRET] | CoW Protocol settlement | | CoW Vault Relayer | [REDACTED_SECRET] | CoW token allowance target | | KyberSwap Router | [REDACTED_SECRET] | KyberSwap Meta Aggregation Router V2 | | ZodiacHelpers | [REDACTED_SECRET] | Approvals, CoW presign, KyberSwap, WETH wrap/unwrap, Trenches factory wrappers via delegatecall | | AgentKeyFactoryV3 | [REDACTED_SECRET] | Trenches token creation and trading | | Safe Factory | [REDACTED_SECRET] | Safe deployer | | Roles Singleton | [REDACTED_SECRET] | Zodiac Roles | | Module Factory | [REDACTED_SECRET] | Module deployer | | CNS | [REDACTED_SECRET] | Clawlett Name Service (unique agent names) | ## Updating When the user says **"update to latest"**, follow this procedure: 1. `git fetch --tags origin` in the clawlett repo 2. Read current version from `scripts/package.json` 3. Identify the latest git tag (e.g., `git tag -l --sort=-v:refname | head -1`) 4. Read **[MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md)** for the migration path between current and latest version 5. Show the user: current version, new version, summary of changes, and whether on-chain steps are required 6. **ASK the user: "Do you want to proceed with this update?"** — do NOT proceed without explicit confirmation 7. If confirmed: `git checkout <tag>`, then walk through each migration step with the user Some updates are code-only (just checkout the new tag). Others require on-chain transactions signed by the Safe owner (e.g., updating Roles permissions for a new ZodiacHelpers contract). The migration guide specifies which. ## Security Model 1. **Safe holds all funds** - Agent wallet only has gas 2. **Zodiac Roles restricts operations**: - Can only interact with ZodiacHelpers - ZodiacHelpers scoped with `allowTarget` (Send + DelegateCall) - Can only approve tokens for CoW Vault Relayer and KyberSwap Router 3. **No transfer/withdraw** - Agent cannot move funds out 4. **Scam protection** - Common tokens resolve to verified addresses only 5. **MEV protection** - CoW Protocol batches orders, preventing sandwich attacks and other MEV extraction

path: MIGRATION_GUIDE.md
content: # Migration Guide

This file documents all breaking changes and required migration steps between versions. When the user says **"update to latest"**, the agent must:

1. Run `git fetch --tags origin` in the clawlett repo
2. Compare the current version (`version` in `scripts/package.json`) against the latest git tag
3. Look up the migration path in this file (current version → latest version)
4. Show the user a summary of what changed and what steps are required (code-only vs on-chain)
5. **Ask the user explicitly: "Do you want to proceed with this update?"**
6. Only if the user confirms: run `git checkout <latest-tag>` and execute the migration steps

If the migration requires on-chain transactions (e.g., updating Roles permissions), the agent must clearly explain this and confirm the user understands before proceeding.

---

## v0.1.0 → v0.2.0

**Summary:** CoW Protocol added as default swap mechanism (MEV-protected). Aerodrome preserved as alternative.

### What changed

| Change | Type |
|--------|------|
| New `swap.js` — CoW Protocol presign flow | Code |
| Old `swap.js` renamed to `swap-aerodrome.js` | Code |
| ZodiacHelpers contract updated to [REDACTED_SECRET] | Code + On-chain |
| `package.json` — added `swap-aerodrome` script entry | Code |
| `SKILL.md` — documents CoW as default, Aerodrome as alternative | Code |

### ZodiacHelpers address change

| | Address |
|---|---------|
| Old | [REDACTED_SECRET] |
| New | [REDACTED_SECRET] |

The new contract adds `cowPreSign()` and `approveForCow()` while retaining all existing functions (`approveForRouter`, `executeSwap`).

### Migration steps

#### Step 1: Update code (automatic)

```bash
cd <clawlett-repo>
git fetch --tags origin
git checkout v0.2.0
```

#### Step 2: Update on-chain Roles permissions (required for existing Safes)

The Safe **owner** must submit a transaction to allow the new ZodiacHelpers address in the Roles modifier. This requires the owner's wallet — the agent cannot do this autonomously.

Use the [Safe Transaction Builder](https://app.safe.global) to batch these calls to the **Roles Modifier** (`roles` address from `config/wallet.json`):

```
1. scopeTarget(roleKey, 0xb34a6210013977FC7D6082287e03915a66249799)
2. allowTarget(roleKey, 0xb34a6210013977FC7D6082287e03915a66249799, 3)
3. revokeTarget(roleKey, 0xc235D2475E4424F277B53D19724E2453a8686C54)
```

Where:
- `roleKey` = the `WalletSwapper` role key from `config/wallet.json`
- `3` = ExecutionOptions.Both (Send + DelegateCall)
- Step 3 (revokeTarget) is optional but recommended to clean up the old address

The Roles Modifier ABI for these functions:
```
function scopeTarget(bytes32 roleKey, address targetAddress) external
function allowTarget(bytes32 roleKey, address targetAddress, uint8 options) external
function revokeTarget(bytes32 roleKey, address targetAddress) external
```

#### Step 3: Refresh config

Re-run initialize to update `config/wallet.json` with the new contracts:

```bash
node scripts/initialize.js --owner <OWNER_ADDRESS>
```

This is idempotent — it detects the existing Safe/Roles and refreshes the config file.

#### Step 4: Verify

```bash
# Test CoW quote (no execution)
node scripts/cow.js --from USDC --to WETH --amount 10

# Test Aerodrome still works (no execution)
node scripts/swap-aerodrome.js --from ETH --to USDC --amount 0.1
```

### New deployments

No migration needed. Run `initialize.js` which uses the new ZodiacHelpers address automatically.

---

## v0.2.0 → v0.3.0

**Summary:** KyberSwap Aggregator added as **default** swap mechanism. CoW Protocol remains available.

### What changed

| Change | Type |
|--------|------|
| `swap.js` renamed to `cow.js` | Code |
| New `swap.js` — KyberSwap aggregator (now default) | Code |
| ZodiacHelpers contract updated (adds `kyberSwap` function) | Code + On-chain |
| `SKILL.md` — documents KyberSwap as default | Code |
| `initialize.js` — adds KyberSwapRouter address | Code |

### ZodiacHelpers address change

| | Address |
|---|---------|
| Old | [REDACTED_SECRET] |
| New | [REDACTED_SECRET] |

The new contract adds `kyberSwap()` for executing swaps via KyberSwap Meta Aggregation Router V2 while retaining all existing functions (`cowPreSign`, `wrapETH`, `unwrapWETH`, `createViaFactory`, `tradeViaFactory`).

### New features

**KyberSwap Aggregator:**
- Finds optimal routes across multiple DEXs on Base
- Native ETH supported directly (no wrapping needed)
- 0.1% partner fee (10 bps)
- Slippage protection via minAmountOut validation

### Migration steps

#### Step 1: Update code

```bash
cd <clawlett-repo>
git fetch --tags origin
git checkout v0.3.0
```

#### Step 2: Update on-chain Roles permissions (required for existing Safes)

The Safe **owner** must submit a transaction to allow the new ZodiacHelpers address in the Roles modifier. This requires the owner's wallet — the agent cannot do this autonomously.

Use the [Safe Transaction Builder](https://app.safe.global) to batch these calls to the **Roles Modifier** (`roles` address from `config/wallet.json`):

```
1. scopeTarget(roleKey, 0x38441B5bd6370b000747c97a12877c83c0A32eaF)
2. allowTarget(roleKey, 0x38441B5bd6370b000747c97a12877c83c0A32eaF, 3)
3. revokeTarget(roleKey, 0xb34a6210013977FC7D6082287e03915a66249799)
```

Where:
- `roleKey` = the `WalletSwapper` role key from `config/wallet.json`
- `3` = ExecutionOptions.Both (Send + DelegateCall)
- Step 3 (revokeTarget) is optional but recommended to clean up the old address

#### Step 3: Refresh config

Re-run initialize to update `config/wallet.json` with the new contracts:

```bash
node scripts/initialize.js --owner <OWNER_ADDRESS>
```

This is idempotent — it detects the existing Safe/Roles and refreshes the config file.

#### Step 4: Verify

```bash
# Test KyberSwap quote (now default swap.js)
node scripts/swap.js --from ETH --to USDC --amount 0.1

# Test CoW still works (now cow.js)
node scripts/cow.js --from USDC --to WETH --amount 10
```

### New deployments

No migration needed. Run `initialize.js` which uses the new ZodiacHelpers address automatically.

### When to use KyberSwap vs CoW

| Use Case | Recommended |
|----------|-------------|
| Large swaps (MEV concern) | CoW Protocol |
| Best price discovery | KyberSwap |
| Native ETH swaps | KyberSwap |
| Partial fills acceptable | CoW Protocol |
| Immediate execution needed | KyberSwap |


path: scripts/package.json
content: {
  "name": "clawlett-scripts",
  "version": "0.4.0",
  "type": "module",
  "scripts": {
    "initialize": "node initialize.js",
    "swap": "node swap.js",
    "cow": "node cow.js",
    "balance": "node balance.js",
    "trenches": "node trenches.js"
  },
  "dependencies": {
    "ethers": "^6.16.0"
  }
}


path: scripts/package-lock.json
content: {
  "name": "clawlett-scripts",
  "version": "0.4.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "clawlett-scripts",
      "version": "0.4.0",
      "dependencies": {
        "ethers": "^6.16.0"
      }
    },
    "node_modules/@adraffy/ens-normalize": {
      "version": "1.10.1",
      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/@noble/curves": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/hashes": "1.3.2"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/hashes": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@types/node": {
      "version": "22.7.5",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "undici-types": "~6.19.2"
      }
    },
    "node_modules/aes-js": {
      "version": "4.0.0-beta.5",
      "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/ethers": {
      "version": "6.16.0",
      "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/ethers-io/"
        },
        {
          "type": "individual",
          "url": "https://www.buymeacoffee.com/ricmoo"
        }
      ],
      "dependencies": {
        "@adraffy/ens-normalize": "1.10.1",
        "@noble/curves": "1.2.0",
        "@noble/hashes": "1.3.2",
        "@types/node": "22.7.5",
        "aes-js": "4.0.0-beta.5",
        "tslib": "2.7.0",
        "ws": "8.17.1"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/tslib": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/undici-types": {
      "version": "6.19.8",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/ws": {
      "version": "8.17.1",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: scripts/balance.js
content: #!/usr/bin/env node

/**
 * Check Safe balances
 *
 * Usage:
 *   node balance.js              # ETH balance
 *   node balance.js --token USDC # Specific token
 *   node balance.js --all        # All verified tokens
 */

import { ethers } from 'ethers'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

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

const DEFAULT_RPC_URL = 'https://mainnet.base.org'

const VERIFIED_TOKENS = {
    'ETH': '0x4200000000000000000000000000000000000006',
    'USDC': [REDACTED_SECRET],
    'USDT': [REDACTED_SECRET],
    'DAI': [REDACTED_SECRET],
    'AERO': [REDACTED_SECRET],
    'cbBTC': [REDACTED_SECRET],
    'VIRTUAL': [REDACTED_SECRET],
    'DEGEN': [REDACTED_SECRET],
    'BRETT': [REDACTED_SECRET],
    'TOSHI': [REDACTED_SECRET],
    'WELL': [REDACTED_SECRET],
    'BID': '0xa1832f7f4e534ae557f9b5ab76de54b1873e498b',
}

const ERC20_ABI = [
    'function symbol() view returns (string)',
    'function decimals() view returns (uint8)',
    'function balanceOf(address) view returns (uint256)',
]

function loadConfig(configDir) {
    const configPath = path.join(configDir, 'wallet.json')
    if (!fs.existsSync(configPath)) {
        throw new Error(`Config not found: ${configPath}\nRun initialize.js first.`)
    }
    return JSON.parse(fs.readFileSync(configPath, 'utf8'))
}

function parseArgs() {
    const args = process.argv.slice(2)
    const result = {
        token: null,
        all: false,
        configDir: process.env.WALLET_CONFIG_DIR || path.join(__dirname, '..', 'config'),
        rpc: process.env.BASE_RPC_URL || DEFAULT_RPC_URL,
    }

    for (let i = 0; i < args.length; i++) {
        switch (args[i]) {
            case '--token':
            case '-t':
                result.[REDACTED_SECRET]
                break
            case '--all':
            case '-a':
                result.all = true
                break
            case '--config-dir':
            case '-c':
                result.configDir = args[++i]
                break
            case '--rpc':
            case '-r':
                result.rpc = args[++i]
                break
            case '--help':
            case '-h':
                printHelp()
                process.exit(0)
        }
    }

    return result
}

function printHelp() {
    console.log(`
Usage: node balance.js [options]

Options:
  --token, -t      Check specific token balance
  --all, -a        Check all verified token balances
  --config-dir, -c Config directory
  --rpc, -r        RPC URL (default: ${DEFAULT_RPC_URL})

Examples:
  node balance.js              # ETH balance only
  node balance.js --token USDC # USDC balance
  node balance.js --all        # All token balances
`)
}

function formatAmount(amount, decimals, symbol) {
    const formatted = ethers.formatUnits(amount, decimals)
    const num = parseFloat(formatted)
    if (num === 0) return `0 ${symbol}`
    if (num < 0.0001) return `${formatted} ${symbol}`
    return `${num.toLocaleString(undefined, { maximumFractionDigits: 6 })} ${symbol}`
}

async function getTokenBalance(provider, safeAddress, tokenAddress) {
    const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider)
    const [symbol, decimals, balance] = await Promise.all([
        contract.symbol(),
        contract.decimals(),
        contract.balanceOf(safeAddress),
    ])
    return { symbol, decimals: Number(decimals), balance }
}

async function main() {
    const args = parseArgs()

    let config
    try {
        config = loadConfig(args.configDir)
    } catch (error) {
        console.error(`Error: ${error.message}`)
        process.exit(1)
    }

    const provider = new ethers.JsonRpcProvider(args.rpc, 8453, { staticNetwork: true })
    const safeAddress = config.safe

    console.log(`\nSafe: ${safeAddress}\n`)

    // Always show ETH
    const ethBalance = await provider.getBalance(safeAddress)
    console.log(`ETH:    ${formatAmount(ethBalance, 18, 'ETH')}`)

    if (args.all) {
        // Show all verified tokens
        console.log('')
        for (const [symbol, address] of Object.entries(VERIFIED_TOKENS)) {
            if (symbol === 'ETH') continue
            try {
                const { balance, decimals } = await getTokenBalance(provider, safeAddress, address)
                if (balance > 0n) {
                    console.log(`${symbol.padEnd(8)} ${formatAmount(balance, decimals, symbol)}`)
                }
            } catch {
                // Token might not exist or have issues
            }
        }
    } else if (args.token) {
        // Show specific token
        const symbol = args.token.toUpperCase().replace(/^\$/, '')
        let address = VERIFIED_TOKENS[symbol]

        if (!address) {
            if (args.token.startsWith('0x') && args.token.length === 42) {
                address = args.token
            } else {
                console.error(`\nToken "${args.token}" not found. Use address or one of:`)
                console.error(Object.keys(VERIFIED_TOKENS).join(', '))
                process.exit(1)
            }
        }

        try {
            const { symbol: tokenSymbol, balance, decimals } = await getTokenBalance(provider, safeAddress, address)
            console.log(`${tokenSymbol}:    ${formatAmount(balance, decimals, tokenSymbol)}`)
        } catch (error) {
            console.error(`\nFailed to get token balance: ${error.message}`)
            process.exit(1)
        }
    }

    console.log('')
}

main().catch(error => {
    console.error(`\n❌ Error: ${error.message}`)
    process.exit(1)
})


path: scripts/tokens.js
content: /**
 * Shared token resolution module
 *
 * Provides verified token lists, symbol resolution, and DexScreener search
 * fallback for tokens not in the hardcoded list.
 */

import { ethers } from 'ethers'

// ============================================================================
// VERIFIED TOKENS - Safeguard against scam tokens
// ============================================================================
const VERIFIED_TOKENS = {
    'ETH': [REDACTED_SECRET],  // Native ETH
    'WETH': '0x4200000000000000000000000000000000000006',
    'USDC': [REDACTED_SECRET],
    'USDT': [REDACTED_SECRET],
    'DAI': [REDACTED_SECRET],
    'USDS': [REDACTED_SECRET],
    'AERO': [REDACTED_SECRET],
    'cbBTC': [REDACTED_SECRET],
    'VIRTUAL': [REDACTED_SECRET],
    'DEGEN': [REDACTED_SECRET],
    'BRETT': [REDACTED_SECRET],
    'TOSHI': [REDACTED_SECRET],
    'WELL': [REDACTED_SECRET],
    'BID': '0xa1832f7f4e534ae557f9b5ab76de54b1873e498b',
}

const TOKEN_ALIASES = {
    'ETHEREUM': 'ETH',
    'ETHER': 'ETH',
    'USD COIN': 'USDC',
    'TETHER': 'USDT',
}

const PROTECTED_SYMBOLS = ['ETH', 'WETH', 'USDC', 'USDT', 'DAI', 'USDS', 'AERO', 'cbBTC', 'BID']

const NATIVE_ETH = '0x0000000000000000000000000000000000000000'

// ABIs
const ERC20_ABI = [
    'function symbol() view returns (string)',
    'function decimals() view returns (uint8)',
    'function balanceOf(address) view returns (uint256)',
    'function allowance(address, address) view returns (uint256)',
]

// ============================================================================
// TOKEN SEARCH - DexScreener fallback
// ============================================================================

async function searchToken(symbol) {
    const url = `https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(symbol)}`
    const response = await fetch(url)
    if (!response.ok) return null

    const data = await response.json()
    if (!data.pairs || data.pairs.length === 0) return null

    // Filter to Base pairs only, then find exact symbol match on either token
    const basePairs = data.pairs.filter(p => p.chainId === 'base')
    if (basePairs.length === 0) return null

    // Find the token matching the symbol (could be baseToken or quoteToken)
    for (const pair of basePairs) {
        const match = [pair.baseToken, pair.quoteToken].find(
            t => t.symbol.toUpperCase() === symbol.toUpperCase()
        )
        if (match) {
            return {
                id: match.address,
                symbol: match.symbol,
                name: match.name,
                volumeUSD: pair.volume?.h24,
                liquidity: pair.liquidity?.usd,
                dex: pair.dexId,
            }
        }
    }

    return null
}

// ============================================================================
// TOKEN RESOLUTION
// ============================================================================

async function resolveToken(token, provider) {
    [REDACTED_SECRET])

    if (token.startsWith('0x') && token.length === 42) {
        return resolveByAddress(token, provider)
    }

    const symbol = token.toUpperCase().replace(/^\$/, '')
    const aliasedSymbol = TOKEN_ALIASES[symbol] || symbol

    if (VERIFIED_TOKENS[aliasedSymbol]) {
        const address = VERIFIED_TOKENS[aliasedSymbol]

        // Native ETH doesn't have a contract
        if (address === NATIVE_ETH) {
            return { address, symbol: 'ETH', decimals: 18, verified: true }
        }

        const tokenContract = new ethers.Contract(address, ERC20_ABI, provider)
        const [onChainSymbol, decimals] = await Promise.all([
            tokenContract.symbol(),
            tokenContract.decimals(),
        ])
        return {
            address,
            symbol: onChainSymbol,
            decimals: Number(decimals),
            verified: true,
        }
    }

    if (PROTECTED_SYMBOLS.includes(aliasedSymbol)) {
        throw new Error(
            `SECURITY: "${symbol}" is a protected token but no verified address found.\n` +
            `This could be a scam token. Use contract address directly if intended.`
        )
    }

    // Fallback: search DexScreener for Base token
    const searchResult = await searchToken(aliasedSymbol)

    if (searchResult) {
        const address = ethers.getAddress(searchResult.id)
        const tokenContract = new ethers.Contract(address, ERC20_ABI, provider)
        const [onChainSymbol, decimals] = await Promise.all([
            tokenContract.symbol(),
            tokenContract.decimals(),
        ])

        const volumeStr = searchResult.volumeUSD
            ? `$${Number(searchResult.volumeUSD).toLocaleString(undefined, { maximumFractionDigits: 0 })}`
            : 'unknown'
        const liqStr = searchResult.liquidity
            ? `$${Number(searchResult.liquidity).toLocaleString(undefined, { maximumFractionDigits: 0 })}`
            : 'unknown'

        return {
            address,
            symbol: onChainSymbol,
            decimals: Number(decimals),
            verified: false,
            name: searchResult.name,
            volumeUSD: searchResult.volumeUSD,
            liquidity: searchResult.liquidity,
            dex: searchResult.dex,
            warning:
                `UNVERIFIED [REDACTED_SECRET] (${address}) found on DexScreener.\n` +
                ` 24h volume: ${volumeStr} | Liquidity: ${liqStr} | DEX: ${searchResult.dex}\n` +
                ` This token is NOT in the verified list. Verify the contract address before proceeding.`,
        }
    }

    throw new Error(
        `Token "${symbol}" not found in verified list or DexScreener (Base).\n` +
        `Use contract address directly: --from 0x...`
    )
}

async function resolveByAddress(address, provider) {
    address = ethers.getAddress(address)

    const verifiedEntry = Object.entries(VERIFIED_TOKENS).find(
        ([, addr]) => addr.toLowerCase() === address.toLowerCase()
    )

    const tokenContract = new ethers.Contract(address, ERC20_ABI, provider)
    const [symbol, decimals] = await Promise.all([
        tokenContract.symbol(),
        tokenContract.decimals(),
    ])

    const result = {
        address,
        symbol,
        decimals: Number(decimals),
        verified: !!verifiedEntry,
    }

    if (!verifiedEntry && PROTECTED_SYMBOLS.includes(symbol.toUpperCase())) {
        result.warning =
            `WARNING: Token has symbol "${symbol}" but is NOT the verified ${symbol}.\n` +
            `Verified address: ${VERIFIED_TOKENS[symbol.toUpperCase()]}\n` +
            `You provided: ${address}\n` +
            `This could be a SCAM TOKEN.`
    }

    return result
}

export {
    VERIFIED_TOKENS,
    TOKEN_ALIASES,
    PROTECTED_SYMBOLS,
    ERC20_ABI,
    resolveToken,
    resolveByAddress,
    searchToken,
}


path: scripts/cow.js
content: #!/usr/bin/env node

/**
 * Swap tokens via CoW Protocol (via Safe + Zodiac Roles)
 *
 * Uses CoW Protocol's presign flow for MEV-protected swaps.
 * CoW batches orders and finds optimal execution paths, protecting
 * against sandwich attacks and other MEV extraction.
 *
 * Features:
 * - Resolves token symbols to addresses
 * - Safeguards for common tokens (ETH, USDC, USDT, etc.)
 * - Auto-substitutes ETH with WETH (CoW requires ERC20s)
 * - Gets quote before execution
 * - Presigns orders via Zodiac Roles delegatecall
 * - Polls order status until filled
 *
 * Usage:
 *   node swap.js --from ETH --to USDC --amount 0.1
 *   node swap.js --from USDC --to ETH --amount 100 --execute
 *
 * ETH is auto-wrapped to WETH when needed (CoW requires ERC20s)
 */

import { ethers } from 'ethers'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { VERIFIED_TOKENS, ERC20_ABI, resolveToken } from './tokens.js'

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

const DEFAULT_RPC_URL = 'https://mainnet.base.org'

// Contracts
const WETH_ADDRESS = '0x4200000000000000000000000000000000000006'
const NATIVE_ETH = '0x0000000000000000000000000000000000000000'

// CoW Protocol constants
const COW_API_BASE = 'https://api.cow.fi/base'
const COW_SETTLEMENT = [REDACTED_SECRET]
const COW_VAULT_RELAYER = [REDACTED_SECRET]

// bytes32 keccak hashes for order struct fields
const KIND_SELL = '0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775'
const KIND_BUY = '0x6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc'
const BALANCE_ERC20 = '0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9'

// ABIs
const ROLES_ABI = [
    'function execTransactionWithRole(address to, uint256 value, bytes data, uint8 operation, bytes32 roleKey, bool shouldRevert) returns (bool)',
]

const COW_PRESIGN_ABI = [
    'function cowPreSign(tuple(address sellToken, address buyToken, address receiver, uint256 sellAmount, uint256 buyAmount, uint32 validTo, bytes32 appData, uint256 feeAmount, bytes32 kind, bool partiallyFillable, bytes32 sellTokenBalance, bytes32 buyTokenBalance) order, bytes orderUid) external',
]

const ZODIAC_HELPERS_ABI = [
    'function wrapETH(uint256 amount) external',
    'function unwrapWETH(uint256 amount) external',
]

// ============================================================================
// COW PROTOCOL API
// ============================================================================

async function getCowQuote(sellToken, buyToken, sellAmount, safeAddress) {
    const response = await fetch(`${COW_API_BASE}/api/v1/quote`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            sellToken: sellToken.address,
            buyToken: buyToken.address,
            from: safeAddress,
            receiver: safeAddress,
            sellAmountBeforeFee: sellAmount.toString(),
            kind: 'sell',
            signingScheme: 'presign',
            sellTokenBalance: 'erc20',
            buyTokenBalance: 'erc20',
        }),
    })

    const data = await response.json()

    if (!response.ok) {
        const errorMsg = data.description || data.errorType || JSON.stringify(data)
        throw new Error(`CoW quote failed: ${errorMsg}`)
    }

    return data
}

async function buildAppData(slippageBips) {
    const doc = {
        appCode: "Clawlett",
        environment: "production",
        metadata: {
            orderClass: { orderClass: "market" },
            quote: { slippageBips, smartSlippage: true },
            partnerFee: {
                bps: 50,
                recipient: [REDACTED_SECRET],
            },
        },
        version: "1.14.0",
    }

    const fullAppData = JSON.stringify(doc)
    const appDataHash = ethers.keccak256(ethers.toUtf8Bytes(fullAppData))

    // Register with CoW so solvers can resolve the hash
    await fetch(`${COW_API_BASE}/api/v1/app_data/${appDataHash}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ fullAppData }),
    }).catch(() => {}) // non-critical

    return appDataHash
}

async function submitCowOrder(quoteResponse, safeAddress, timeoutSeconds) {
    const q = quoteResponse.quote

    // Default 30 minutes — matches CoW FE. Solvers need time to batch orders.
    const validTo = Math.floor(Date.now() / 1000) + (timeoutSeconds || 1800)

    // Smart slippage (based on CoW FE, more aggressive for small orders):
    //   1. Fee-based:   150% of feeAmount (dominates small orders → wider tolerance)
    //   2. Volume-based: 0.5% of sellAmount (dominates large orders → ~0.5%)
    const feeSlippage = BigInt(q.feeAmount) * 3n / 2n
    const volumeSlippage = BigInt(q.sellAmount) * 5n / 1000n
    const totalSlippage = feeSlippage + volumeSlippage
    // Convert sell-token slippage to buy-[REDACTED_SECRET] * buyAmount / sellAmount
    const buySlippage = BigInt(q.sellAmount) > 0n
        ? totalSlippage * BigInt(q.buyAmount) / BigInt(q.sellAmount)
        : BigInt(q.buyAmount) * 5n / 1000n
    const discountedBuyAmount = (BigInt(q.buyAmount) - buySlippage).toString()

    // Build appData with slippage metadata (matches CoW FE)
    const slippageBips = Number(buySlippage * 10000n / BigInt(q.buyAmount))
    const appData = await buildAppData(slippageBips)

    const order = {
        sellToken: q.sellToken,
        buyToken: q.buyToken,
        receiver: q.receiver || safeAddress,
        sellAmount: q.sellAmount,
        buyAmount: discountedBuyAmount,
        validTo,
        appData,
        feeAmount: "0",
        kind: q.kind,
        partiallyFillable: q.partiallyFillable || false,
        sellTokenBalance: q.sellTokenBalance || 'erc20',
        buyTokenBalance: q.buyTokenBalance || 'erc20',
        signingScheme: 'presign',
        signature: safeAddress,
        from: safeAddress,
    }

    const response = await fetch(`${COW_API_BASE}/api/v1/orders`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(order),
    })

    const data = await response.json()

    if (!response.ok) {
        const errorMsg = data.description || data.errorType || JSON.stringify(data)
        throw new Error(`CoW order submission failed: ${errorMsg}`)
    }

    // data is the orderUid string
    return { orderUid: data, order }
}

function kindToBytes32(kind) {
    switch (kind) {
        case 'sell': return KIND_SELL
        case 'buy': return KIND_BUY
        default: throw new Error(`Unknown order kind: ${kind}`)
    }
}

function balanceToBytes32(balance) {
    switch (balance) {
        case 'erc20': return BALANCE_ERC20
        default: throw new Error(`Unknown balance type: ${balance}`)
    }
}

async function pollOrderStatus(orderUid, timeoutMs) {
    const startTime = Date.now()
    const pollInterval = 5000

    while (Date.now() - startTime < timeoutMs) {
        const response = await fetch(`${COW_API_BASE}/api/v1/orders/${orderUid}`)

        if (response.ok) {
            const order = await response.json()
            const status = order.status

            if (status === 'fulfilled') {
                return { status: 'fulfilled', order }
            } else if (status === 'expired') {
                return { status: 'expired', order }
            } else if (status === 'cancelled') {
                return { status: 'cancelled', order }
            }

            // presignaturePending or open - keep polling
            const elapsed = Math.round((Date.now() - startTime) / 1000)
            console.log(`   Status: ${status} (${elapsed}s elapsed)`)
        }

        await new Promise(resolve => setTimeout(resolve, pollInterval))
    }

    return { status: 'timeout' }
}

// ============================================================================
// HELPERS
// ============================================================================

function formatAmount(amount, decimals, symbol) {
    const formatted = ethers.formatUnits(amount, decimals)
    const num = parseFloat(formatted)
    if (num < 0.01) return `${formatted} ${symbol}`
    return `${num.toLocaleString(undefined, { maximumFractionDigits: 6 })} ${symbol}`
}

function loadConfig(configDir) {
    const configPath = path.join(configDir, 'wallet.json')
    if (!fs.existsSync(configPath)) {
        throw new Error(`Config not found: ${configPath}\nRun initialize.js first.`)
    }
    return JSON.parse(fs.readFileSync(configPath, 'utf8'))
}

function parseArgs() {
    const args = process.argv.slice(2)
    const result = {
        from: null,
        to: null,
        amount: null,
        configDir: process.env.WALLET_CONFIG_DIR || path.join(__dirname, '..', 'config'),
        rpc: process.env.BASE_RPC_URL || DEFAULT_RPC_URL,
        slippage: 0.05,
        execute: false,
        timeout: 1800, // 30 minutes default (matches CoW FE)
    }

    for (let i = 0; i < args.length; i++) {
        switch (args[i]) {
            case '--from':
            case '-f':
                result.from = args[++i]
                break
            case '--to':
            case '-t':
                result.to = args[++i]
                break
            case '--amount':
            case '-a':
                result.amount = args[++i]
                break
            case '--slippage':
                result.slippage = parseFloat(args[++i])
                break
            case '--execute':
            case '-x':
                result.execute = true
                break
            case '--timeout':
                result.timeout = parseInt(args[++i])
                break
            case '--config-dir':
            case '-c':
                result.configDir = args[++i]
                break
            case '--rpc':
            case '-r':
                result.rpc = args[++i]
                break
            case '--help':
            case '-h':
                printHelp()
                process.exit(0)
        }
    }

    return result
}

function printHelp() {
    console.log(`
Usage: node swap.js --from <TOKEN> --to <TOKEN> --amount <AMOUNT> [--execute]

Swap tokens via CoW Protocol (MEV-protected).

Arguments:
  --from, -f       Token to swap from (symbol or address)
  --to, -t         Token to swap to (symbol or address)
  --amount, -a     Amount to swap
  --slippage       Slippage 0-0.5 (default: 0.05 = 5%)
  --execute, -x    Execute swap (default: quote only)
  --timeout        Order timeout in seconds (default: 1800 = 30min)
  --config-dir, -c Config directory
  --rpc, -r        RPC URL (default: ${DEFAULT_RPC_URL})

Notes:
  - CoW Protocol only works with ERC20 tokens. ETH is auto-wrapped to WETH.
  - Orders are MEV-protected (no sandwich attacks).

Verified Tokens:
  ETH, WETH, USDC, USDT, DAI, USDS, AERO, cbBTC, VIRTUAL, DEGEN, BRETT, TOSHI, WELL
  Other tokens are searched via DexScreener (Base pairs).
  Unverified tokens show a warning with contract address, volume, and liquidity.

Examples:
  node swap.js --from ETH --to USDC --amount 0.1
  node swap.js --from USDC --to WETH --amount 100 --execute
  node swap.js --from USDC --to DAI --amount 50 --execute --timeout 600
`)
}

// ============================================================================
// MAIN
// ============================================================================

async function main() {
    const args = parseArgs()

    if (!args.from || !args.to || !args.amount) {
        console.error('Error: --from, --to, and --amount are required')
        printHelp()
        process.exit(1)
    }

    let config
    try {
        config = loadConfig(args.configDir)
    } catch (error) {
        console.error(`Error: ${error.message}`)
        process.exit(1)
    }

    const provider = new ethers.JsonRpcProvider(args.rpc, 8453, { staticNetwork: true })
    const safeAddress = config.safe

    console.log('\nResolving tokens...\n')

    let tokenIn, tokenOut
    try {
        tokenIn = await resolveToken(args.from, provider)
    } catch (error) {
        console.error(`\n${error.message}`)
        process.exit(1)
    }

    try {
        tokenOut = await resolveToken(args.to, provider)
    } catch (error) {
        console.error(`\n${error.message}`)
        process.exit(1)
    }

    // CoW Protocol only works with ERC20s - substitute ETH with WETH
    let ethSubstituted = false
    if (tokenIn.address.toLowerCase() === NATIVE_ETH) {
        console.log('Note: CoW Protocol requires ERC20 tokens. Using WETH instead of ETH.')
        tokenIn = { ...tokenIn, address: WETH_ADDRESS, symbol: 'WETH' }
        ethSubstituted = true
    }
    if (tokenOut.address.toLowerCase() === NATIVE_ETH) {
        console.log('Note: CoW Protocol requires ERC20 tokens. Receiving WETH instead of ETH.')
        tokenOut = { ...tokenOut, address: WETH_ADDRESS, symbol: 'WETH' }
        ethSubstituted = true
    }

    console.log(`From: ${tokenIn.symbol} ${tokenIn.verified ? '(verified)' : '(unverified)'}`)
    console.log(`      ${tokenIn.address}`)
    if (tokenIn.warning) console.log(`\n${tokenIn.warning}\n`)

    console.log(`To:   ${tokenOut.symbol} ${tokenOut.verified ? '(verified)' : '(unverified)'}`)
    console.log(`      ${tokenOut.address}`)
    if (tokenOut.warning) console.log(`\n${tokenOut.warning}\n`)

    const amountIn = ethers.parseUnits(args.amount, tokenIn.decimals)
    console.log(`\nAmount: ${formatAmount(amountIn, tokenIn.decimals, tokenIn.symbol)}`)

    // Check balance — when selling ETH via CoW, check both ETH and WETH
    let balance
    let wrapAmount = 0n
    if (ethSubstituted && tokenIn.address.toLowerCase() === WETH_ADDRESS.toLowerCase()) {
        const wethContract = new ethers.Contract(WETH_ADDRESS, ERC20_ABI, provider)
        const wethBalance = await wethContract.balanceOf(safeAddress)
        const ethBalance = await provider.getBalance(safeAddress)

        console.log(`Safe WETH balance: ${formatAmount(wethBalance, 18, 'WETH')}`)
        console.log(`Safe ETH balance:  ${formatAmount(ethBalance, 18, 'ETH')}`)

        if (wethBalance >= amountIn) {
            balance = wethBalance
        } else if (wethBalance + ethBalance >= amountIn) {
            wrapAmount = amountIn - wethBalance
            balance = amountIn // sufficient after wrapping
            console.log(`Will wrap ${formatAmount(wrapAmount, 18, 'ETH')} to WETH as part of the swap transaction`)
        } else {
            console.error(`\nInsufficient ETH + WETH balance in Safe`)
            console.error(`Need ${formatAmount(amountIn, 18, 'WETH')}, have ${formatAmount(wethBalance, 18, 'WETH')} + ${formatAmount(ethBalance, 18, 'ETH')}`)
            process.exit(1)
        }
    } else {
        const tokenContract = new ethers.Contract(tokenIn.address, ERC20_ABI, provider)
        balance = await tokenContract.balanceOf(safeAddress)
        console.log(`Safe balance: ${formatAmount(balance, tokenIn.decimals, tokenIn.symbol)}`)

        if (balance < amountIn) {
            console.error(`\nInsufficient ${tokenIn.symbol} balance in Safe`)
            process.exit(1)
        }
    }

    // Get CoW quote
    console.log('\nGetting CoW Protocol quote...\n')

    let quoteResponse
    try {
        quoteResponse = await getCowQuote(tokenIn, tokenOut, amountIn, safeAddress)
    } catch (error) {
        console.error(`${error.message}`)
        console.error(`\nTip: If CoW has no liquidity for this pair, try using the contract address directly.`)
        process.exit(1)
    }

    const q = quoteResponse.quote
    const sellAmount = BigInt(q.sellAmount)
    const buyAmount = BigInt(q.buyAmount)
    const feeAmount = BigInt(q.feeAmount)

    console.log('='.repeat(55))
    console.log('                    SWAP SUMMARY')
    console.log('='.repeat(55))
    console.log(`  You pay:      ${formatAmount(amountIn, tokenIn.decimals, tokenIn.symbol)}`)
    console.log(`  Fee:          ${formatAmount(feeAmount, tokenIn.decimals, tokenIn.symbol)}`)
    console.log(`  You sell:     ${formatAmount(sellAmount, tokenIn.decimals, tokenIn.symbol)} (after fee)`)
    // Smart slippage for display (same formula as submitCowOrder)
    const displayFeeSlippage = feeAmount * 3n / 2n
    const displayVolSlippage = sellAmount * 5n / 1000n
    const displayTotalSlippage = displayFeeSlippage + displayVolSlippage
    const displayBuySlippage = sellAmount > 0n
        ? displayTotalSlippage * buyAmount / sellAmount
        : buyAmount * 5n / 1000n
    const minReceive = buyAmount - displayBuySlippage
    const slippagePct = Number(displayBuySlippage * 10000n / buyAmount) / 100
    console.log(`  You receive:  ~${formatAmount(buyAmount, tokenOut.decimals, tokenOut.symbol)}`)
    console.log(`  Min receive:  ${formatAmount(minReceive, tokenOut.decimals, tokenOut.symbol)} (${slippagePct.toFixed(2)}% smart slippage)`)
    console.log(`  Expires in:   ${args.timeout}s`)
    if (wrapAmount > 0n) {
        console.log(`  ETH wrap:     ${formatAmount(wrapAmount, 18, 'ETH')} → WETH (bundled in tx)`)
    }
    console.log(`  MEV protected via CoW Protocol batch auction`)
    console.log('='.repeat(55))

    if (!args.execute) {
        console.log('\nQUOTE ONLY - Add --execute to perform the swap')
        console.log(`\nTo execute: node swap.js --from "${args.from}" --to "${args.to}" --amount ${args.amount} --execute`)
        process.exit(0)
    }

    // ========================================================================
    // EXECUTION
    // ========================================================================

    console.log('\nExecuting CoW Protocol swap...\n')

    const agentPkPath = path.join(args.configDir, 'agent.pk')
    if (!fs.existsSync(agentPkPath)) {
        console.error('Error: Agent private key not found')
        process.exit(1)
    }
    let privateKey = fs.readFileSync(agentPkPath, 'utf8').trim()
    if (!privateKey.startsWith('0x')) privateKey = '0x' + privateKey

    const wallet = new ethers.Wallet(privateKey, provider)
    const roles = new ethers.Contract(config.roles, ROLES_ABI, wallet)

    const zodiacHelpersAddress = config.contracts?.ZodiacHelpers
    if (!zodiacHelpersAddress) {
        console.error('Error: ZodiacHelpers address not found in config. Re-run initialize.js.')
        process.exit(1)
    }

    // Step 1: Submit order to CoW API (off-chain, needed to get orderUid for presign)
    console.log('Step 1: Submitting order to CoW Protocol...')
    let orderUid, order
    try {
        const result = await submitCowOrder(quoteResponse, safeAddress, args.timeout)
        orderUid = result.orderUid
        order = result.order
    } catch (error) {
        console.error(`${error.message}`)
        process.exit(1)
    }
    console.log(`   Order UID: ${orderUid}`)
    console.log(`   Explorer:  https://explorer.cow.fi/base/orders/${orderUid}`)

    // Step 2: Build on-chain operations (wrap + approve + presign) and execute
    // All operations are bundled into a single MultiSend transaction when multiple
    // steps are needed, saving gas and ensuring atomicity.
    console.log('\nStep 2: Executing on-chain operations...')

    const zodiacHelpersInterface = new ethers.Interface(ZODIAC_HELPERS_ABI)
    const cowPresignInterface = new ethers.Interface(COW_PRESIGN_ABI)
    const multiSendTxs = []

    // 2a: Wrap ETH → WETH if needed (user said ETH, CoW needs WETH)
    if (wrapAmount > 0n) {
        console.log(`   - Wrap ${formatAmount(wrapAmount, 18, 'ETH')} → WETH`)
        const wrapData = zodiacHelpersInterface.encodeFunctionData('wrapETH', [wrapAmount])
        multiSendTxs.push({ operation: 1, to: zodiacHelpersAddress, value: 0n, data: wrapData })
    }

    // 2b: Presign the order on-chain (must match submitted order exactly)
    console.log('   - Presign CoW order')
    const orderStruct = {
        sellToken: order.sellToken,
        buyToken: order.buyToken,
        receiver: order.receiver || safeAddress,
        sellAmount: BigInt(order.sellAmount),
        buyAmount: BigInt(order.buyAmount),
        validTo: order.validTo,
        appData: order.appData,
        feeAmount: 0n,
        kind: kindToBytes32(order.kind),
        partiallyFillable: order.partiallyFillable || false,
        sellTokenBalance: balanceToBytes32(order.sellTokenBalance || 'erc20'),
        buyTokenBalance: balanceToBytes32(order.buyTokenBalance || 'erc20'),
    }

    const presignData = cowPresignInterface.encodeFunctionData('cowPreSign', [
        orderStruct,
        orderUid,
    ])
    multiSendTxs.push({ operation: 1, to: zodiacHelpersAddress, value: 0n, data: presignData })

    // Execute each operation individually via Roles (ZodiacHelpers is the allowed target)
    for (let i = 0; i < multiSendTxs.length; i++) {
        const tx = multiSendTxs[i]
        console.log(`   Executing operation ${i + 1}/${multiSendTxs.length}...`)
        const onChainTx = await roles.execTransactionWithRole(
            tx.to,
            tx.value,
            tx.data,
            1, // delegatecall
            config.roleKey,
            true
        )
        console.log(`   Transaction: ${onChainTx.hash}`)
        const receipt = await onChainTx.wait()
        if (receipt.status !== 1) {
            console.error(`   Operation ${i + 1} failed!`)
            process.exit(1)
        }
    }
    const lastTxHash = multiSendTxs.length > 0 ? 'see above' : 'none'

    console.log('   All on-chain operations complete!')

    // Step 3: Poll order status
    console.log('\nStep 3: Waiting for order to be filled...')
    console.log(`   Timeout: ${args.timeout}s`)

    const result = await pollOrderStatus(orderUid, args.timeout * 1000)

    switch (result.status) {
        case 'fulfilled': {
            let newBalance
            const outContract = new ethers.Contract(tokenOut.address, ERC20_ABI, provider)
            newBalance = await outContract.balanceOf(safeAddress)

            console.log('\nSWAP COMPLETE')
            console.log(`   Sold: ${formatAmount(sellAmount, tokenIn.decimals, tokenIn.symbol)}`)
            console.log(`   Received: ~${formatAmount(buyAmount, tokenOut.decimals, tokenOut.symbol)}`)
            console.log(`   New ${tokenOut.symbol} balance: ${formatAmount(newBalance, tokenOut.decimals, tokenOut.symbol)}`)
            console.log(`   Explorer: https://explorer.cow.fi/base/orders/${orderUid}`)
            break
        }
        case 'expired':
            console.error('\nOrder expired without being filled.')
            console.error('Tip: Try again with a higher slippage tolerance.')
            process.exit(1)
            break
        case 'cancelled':
            console.error('\nOrder was cancelled.')
            process.exit(1)
            break
        case 'timeout':
            console.error(`\nTimed out after ${args.timeout}s. Order may still be filled.`)
            console.error(`Check status: https://explorer.cow.fi/base/orders/${orderUid}`)
            process.exit(1)
            break
    }
}

main().catch(error => {
    console.error(`\nError: ${error.message}`)
    process.exit(1)
})


path: scripts/trenches.js
content: #!/usr/bin/env node

/**
 * Trenches token creation and bonding curve trading (via Safe + Zodiac Roles)
 *
 * Creates tokens and trades on Trenches bonding curves on Base.
 * All on-chain operations go through ZodiacHelpers wrapper functions
 * which validate the factory address and forward calls with explicit ethValue
 * (since msg.value doesn't work in delegatecall).
 *
 * Subcommands:
 *   create      Create a new token on Trenches
 *   buy         Buy tokens with ETH
 *   sell        Sell tokens for ETH
 *   info        Get token information
 *   trending    Show trending tokens
 *   new         Show newly created tokens
 *   top-volume  Show top volume tokens
 *   gainers     Show top gainers
 *   losers      Show top losers
 *
 * Usage:
 *   node trenches.js create --name "Token" --symbol TKN --description "desc" [--initial-buy 0.01]
 *   node trenches.js buy --token TKN --amount 0.01 [--all]
 *   node trenches.js sell --token TKN --amount 1000 [--all]
 *   node trenches.js info TKN
 *   node trenches.js trending [--window 1h] [--limit 10]
 */

import { ethers } from 'ethers'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

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

// ============================================================================
// CONSTANTS
// ============================================================================

const DEFAULT_RPC_URL = 'https://mainnet.base.org'
const CHAIN_ID = 8453
const TRENCHES_API_URL = process.env.TRENCHES_API_URL || 'https://trenches.bid'

// Contracts
const AGENT_KEY_FACTORY = [REDACTED_SECRET]
const WETH_ADDRESS = '0x4200000000000000000000000000000000000006'
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const BID_ADDRESS = [REDACTED_SECRET]

// Uniswap V3 sqrt price limits
const MIN_SQRT_PRICE = 4295128739n
const MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342n

// ZodiacHelpers ABIs (wrapper functions — NOT the raw factory ABIs)
const ZODIAC_HELPERS_ABI = [
    'function createViaFactory(address factory, (bytes signature, bytes data, uint256 expiresAt) signature, uint256 ethValue) external',
    'function tradeViaFactory(address factory, address inputToken, uint256 approvalAmount, (bytes signature, bytes data, uint256 expiresAt, uint256 nonce) signature, (uint160 sqrtPriceLimit, uint256 minAmountOut) tradeLimits, uint256 ethValue) external',
    'function approveForFactory(address factory, address token, uint256 amount) external',
]

// Read-only factory ABI (for mintCost)
const FACTORY_READ_ABI = [
    'function mintCost() external view returns (uint256)',
]

const ROLES_ABI = [
    'function execTransactionWithRole(address to, uint256 value, bytes data, uint8 operation, bytes32 roleKey, bool shouldRevert) returns (bool)',
]

const ERC20_ABI = [
    'function symbol() view returns (string)',
    'function decimals() view returns (uint8)',
    'function balanceOf(address) view returns (uint256)',
    'function allowance(address, address) view returns (uint256)',
]

// ============================================================================
// HELPERS
// ============================================================================

function loadConfig(configDir) {
    const configPath = path.join(configDir, 'wallet.json')
    if (!fs.existsSync(configPath)) {
        throw new Error(`Config not found: ${configPath}\nRun initialize.js first.`)
    }
    return JSON.parse(fs.readFileSync(configPath, 'utf8'))
}

function apiHeaders(extra = {}) {
    return { ...extra }
}

function loadAgentAndRoles(config, configDir, rpcUrl) {
    const provider = new ethers.JsonRpcProvider(rpcUrl, CHAIN_ID, { staticNetwork: true })

    const agentPkPath = path.join(configDir, 'agent.pk')
    if (!fs.existsSync(agentPkPath)) {
        throw new Error('Agent private key not found. Run initialize.js first.')
    }
    let privateKey = fs.readFileSync(agentPkPath, 'utf8').trim()
    if (!privateKey.startsWith('0x')) privateKey = '0x' + privateKey

    const wallet = new ethers.Wallet(privateKey, provider)
    const roles = new ethers.Contract(config.roles, ROLES_ABI, wallet)

    const zodiacHelpersAddress = config.contracts?.ZodiacHelpers
    if (!zodiacHelpersAddress) {
        throw new Error('ZodiacHelpers address not found in config. Re-run initialize.js.')
    }

    return { provider, wallet, roles, zodiacHelpersAddress }
}

function getSqrtPriceLimit(isBuy, tokenAddress, baseTokenAddress) {
    const zeroForOne = isBuy
        ? baseTokenAddress.toLowerCase() < tokenAddress.toLowerCase()
        : tokenAddress.toLowerCase() < baseTokenAddress.toLowerCase()
    return zeroForOne ? MIN_SQRT_PRICE + 1n : MAX_SQRT_PRICE - 1n
}

function formatAmount(amount, decimals, symbol) {
    const formatted = ethers.formatUnits(amount, decimals)
    const num = parseFloat(formatted)
    if (num < 0.01) return `${formatted} ${symbol}`
    return `${num.toLocaleString(undefined, { maximumFractionDigits: 6 })} ${symbol}`
}

function formatEth(wei) {
    return formatAmount(wei, 18, 'ETH')
}

// ============================================================================
// AUTHENTICATION
// ============================================================================

async function getAuthCookies(config, configDir, rpcUrl) {
    if (config.cookies) {
        return config.cookies
    }

    // Re-authenticate using agent wallet
    console.log('No stored cookies, authenticating...')
    const agentPkPath = path.join(configDir, 'agent.pk')
    let privateKey = fs.readFileSync(agentPkPath, 'utf8').trim()
    if (!privateKey.startsWith('0x')) privateKey = '0x' + privateKey

    const provider = new ethers.JsonRpcProvider(rpcUrl, CHAIN_ID, { staticNetwork: true })
    const wallet = new ethers.Wallet(privateKey, provider)

    const challengeRes = await fetch(
        `${TRENCHES_API_URL}/api/auth/wallet?wallet=${wallet.address.toLowerCase()}`,
        { headers: apiHeaders() },
    )
    const challengeData = await challengeRes.json()
    if (!challengeRes.ok) {
        throw new Error(challengeData.error || 'Failed to get auth challenge')
    }

    const parts = challengeData.jwt.split('.')
    const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
    const message = challengeData.message || payload.message

    const signature = await wallet.signMessage(message)
    const authRes = await fetch(`${TRENCHES_API_URL}/api/auth/wallet`, {
        method: 'POST',
        headers: apiHeaders({ 'Content-Type': 'application/json' }),
        body: JSON.stringify({
            wallet: wallet.address.toLowerCase(),
            signature,
            jwt: challengeData.jwt,
        }),
    })

    const authData = await authRes.json()
    if (!authRes.ok) {
        throw new Error(authData.error || 'Authentication failed')
    }

    const cookies = authRes.headers.getSetCookie?.() || []
    const cookieString = cookies.map(c => c.split(';')[0]).join('; ')
    if (!cookieString) {
        throw new Error('No session cookies received from authentication')
    }

    return cookieString
}

// ============================================================================
// API FUNCTIONS
// ============================================================================

async function uploadImageApi(cookies, imagePath) {
    if (!fs.existsSync(imagePath)) {
        throw new Error(`Image file not found: ${imagePath}`)
    }

    const stat = fs.statSync(imagePath)
    if (stat.size > 4_000_000) {
        throw new Error(`Image too large (${(stat.size / 1_000_000).toFixed(1)}MB). Max 4MB.`)
    }

    const ext = path.extname(imagePath).toLowerCase()
    const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' }
    const mimeType = mimeTypes[ext]
    if (!mimeType) {
        throw new Error(`Unsupported image type: ${ext}. Use PNG, JPEG, or WEBP.`)
    }

    const imageData = fs.readFileSync(imagePath)
    const blob = new Blob([imageData], { type: mimeType })
    const formData = new FormData()
    formData.append('image', blob, path.basename(imagePath))

    const response = await fetch(`${TRENCHES_API_URL}/api/skill/image`, {
        method: 'POST',
        headers: apiHeaders({ 'Cookie': cookies }),
        body: formData,
    })

    const data = await response.json()
    if (!response.ok) {
        throw new Error(data.error || data.message || `Image upload failed: ${JSON.stringify(data)}`)
    }

    return data.imageUrl
}

async function createTokenApi(cookies, params) {
    const response = await fetch(`${TRENCHES_API_URL}/api/skill/token/create`, {
        method: 'POST',
        headers: apiHeaders({
            'Content-Type': 'application/json',
            'Cookie': cookies,
        }),
        body: JSON.stringify({
            name: params.name,
            symbol: params.symbol,
            description: params.description,
            twitter: params.twitter || '',
            website: params.website || '',
            initialBuyAmount: params.initialBuyAmount || '0',
            chainId: CHAIN_ID,
            baseToken: params.baseToken || ZERO_ADDRESS,
            isAntiSnipeEnabled: params.isAntiSnipeEnabled !== false,
            imageUrl: params.imageUrl || '',
        }),
    })

    const data = await response.json()
    if (!response.ok) {
        throw new Error(data.error || data.message || `Token creation API failed: ${JSON.stringify(data)}`)
    }

    return data
}

async function getSwapSignature(cookies, tokenAddress, amountIn, isBuy) {
    const response = await fetch(`${TRENCHES_API_URL}/api/skill/swap`, {
        method: 'POST',
        headers: apiHeaders({
            'Content-Type': 'application/json',
            'Cookie': cookies,
        }),
        body: JSON.stringify({
            tokenAddress,
            amountIn: amountIn.toString(),
            isBuy,
            chainId: CHAIN_ID,
        }),
    })

    const data = await response.json()
    if (!response.ok) {
        throw new Error(data.error || data.message || `Swap API failed: ${JSON.stringify(data)}`)
    }

    return data
}

async function getTokenInfo(symbol) {
    const response = await fetch(
        `${TRENCHES_API_URL}/api/skill/token?symbol=${encodeURIComponent(symbol)}&chainId=${CHAIN_ID}`,
        { headers: apiHeaders() },
    )

    const data = await response.json()
    if (!response.ok) {
        throw new Error(data.error || data.message || `Token info failed: ${JSON.stringify(data)}`)
    }

    return data
}

async function getDiscovery(endpoint, params = {}) {
    const query = new URLSearchParams({ chainId: String(CHAIN_ID), ...params })
    const response = await fetch(
        `${TRENCHES_API_URL}/api/skill/tokens/${endpoint}?${query}`,
        { headers: apiHeaders() },
    )

    const data = await response.json()
    if (!response.ok) {
        throw new Error(data.error || data.message || `Discovery API failed: ${JSON.stringify(data)}`)
    }

    return data
}

// ============================================================================
// ARG PARSING
// ============================================================================

function parseArgs() {
    const args = process.argv.slice(2)
    const result = {
        subcommand: null,
        // create params
        name: null,
        symbol: null,
        description: null,
        twitter: null,
        website: null,
        initialBuy: null,
        baseToken: null,
        noAntibot: false,
        image: null,
        // buy/sell params
        token: null,
        amount: null,
        all: false,
        // discovery params
        window: null,
        limit: null,
        // common
        configDir: process.env.WALLET_CONFIG_DIR || path.join(__dirname, '..', 'config'),
        rpc: process.env.BASE_RPC_URL || DEFAULT_RPC_URL,
    }

    // First positional arg is subcommand
    if (args.length > 0 && !args[0].startsWith('-')) {
        result.subcommand = args[0]
    }

    for (let i = result.subcommand ? 1 : 0; i < args.length; i++) {
        switch (args[i]) {
            case '--name':
                result.name = args[++i]
                break
            case '--symbol':
                result.symbol = args[++i]
                break
            case '--description':
                result.description = args[++i]
                break
            case '--twitter':
                result.twitter = args[++i]
                break
            case '--website':
                result.website = args[++i]
                break
            case '--initial-buy':
                result.initialBuy = args[++i]
                break
            case '--base-token':
                result.baseToken = args[++i]
                break
            case '--no-antibot':
                result.noAntibot = true
                break
            case '--image':
                result.image = args[++i]
                break
            case '--token':
            case '-t':
                result.[REDACTED_SECRET]
                break
            case '--amount':
            case '-a':
                result.amount = args[++i]
                break
            case '--all':
                result.all = true
                break
            case '--window':
            case '-w':
                result.window = args[++i]
                break
            case '--limit':
            case '-l':
                result.limit = args[++i]
                break
            case '--config-dir':
            case '-c':
                result.configDir = args[++i]
                break
            case '--rpc':
            case '-r':
                result.rpc = args[++i]
                break
            case '--help':
            case '-h':
                printHelp(result.subcommand)
                process.exit(0)
                break
            default:
                // Positional arg after subcommand (e.g., "info TKN")
                if (!args[i].startsWith('-') && result.subcommand && !result.token) {
                    result.[REDACTED_SECRET]
                }
                break
        }
    }

    return result
}

function printHelp(subcommand) {
    if (!subcommand) {
        console.log(`
Usage: node trenches.js <subcommand> [options]

Trenches token creation and bonding curve trading on Base.

Subcommands:
  create      Create a new token on Trenches (0.005 ETH mint cost)
  buy         Buy tokens on bonding curve (with ETH or BID depending on pair)
  sell        Sell tokens on bonding curve (receive ETH or BID depending on pair)
  info        Get token information
  trending    Show trending tokens
  new         Show newly created tokens
  top-volume  Show top volume tokens
  gainers     Show top gainers
  losers      Show top losers

Common Options:
  --config-dir, -c  Config directory
  --rpc, -r         RPC URL (default: ${DEFAULT_RPC_URL})
  --help, -h        Show help

Examples:
  node trenches.js create --name "My Token" --symbol MTK --description "A cool token"
  node trenches.js create --name "My Token" --symbol MTK --description "desc" --base-token ETH
  node trenches.js create --name "My Token" --symbol MTK --description "desc" --no-antibot --initial-buy 0.01
  node trenches.js buy --token MTK --amount 0.01
  node trenches.js buy --token CLAWLETT --all
  node trenches.js sell --token MTK --amount 1000
  node trenches.js sell --token MTK --all
  node trenches.js info MTK
  node trenches.js trending
  node trenches.js trending --window 1h --limit 5

Use "node trenches.js <subcommand> --help" for subcommand-specific help.
`)
        return
    }

    switch (subcommand) {
        case 'create':
            console.log(`
Usage: node trenches.js create --name <NAME> --symbol <SYMBOL> --description <DESC> [options]

Create a new token on Trenches bonding curve.

Mint cost: 0.005 ETH (paid to the factory contract on creation).
If --initial-buy is specified, the amount is in the base token (ETH or BID).
Mint cost (0.005 ETH) is always paid in ETH regardless of base token.
Note: --initial-buy cannot be used with anti-bot protection (enabled by default).

Arguments:
  --name            Token name (required)
  --symbol          Token symbol (required)
  --description     Token description (required)
  --twitter         Twitter handle
  --website         Website URL
  --initial-buy     Initial buy amount in base token (optional, e.g., 0.01)
  --base-token      Base token for bonding curve: BID (default) or ETH
  --no-antibot      Disable anti-bot/sniper protection (enabled by default)
  --image           Path to token image file (PNG/JPEG/WEBP, max 4MB)

Examples:
  node trenches.js create --name "My Token" --symbol MTK --description "A cool token"
  node trenches.js create --name "My Token" --symbol MTK --description "desc" --base-token ETH
  node trenches.js create --name "My Token" --symbol MTK --description "desc" --no-antibot --initial-buy 0.01
  node trenches.js create --name "My Token" --symbol MTK --description "desc" --image ./logo.png
`)
            break
        case 'buy':
            console.log(`
Usage: node trenches.js buy --token <SYMBOL> --amount <AMOUNT> [--all]

Buy tokens on the Trenches bonding curve. The amount is denominated in the
pair's base token (ETH for ETH-paired tokens, BID for BID-paired tokens).
The base token is detected automatically from the Trenches API.

Arguments:
  --token, -t       Token symbol or address (required)
  --amount, -a      Base token amount to spend (required unless --all)
  --all             Spend entire base token balance

Examples:
  node trenches.js buy --token MTK --amount 0.01
  node trenches.js buy --token CLAWLETT --amount 100
  node trenches.js buy --token CLAWLETT --all
`)
            break
        case 'sell':
            console.log(`
Usage: node trenches.js sell --token <SYMBOL> --amount <TOKEN_AMOUNT> [--all]

Sell tokens on the Trenches bonding curve. You receive the pair's base token
(ETH for ETH-paired tokens, BID for BID-paired tokens).

Arguments:
  --token, -t       Token symbol or address (required)
  --amount, -a      Token amount to sell (required unless --all)
  --all             Sell entire token balance

Examples:
  node trenches.js sell --token MTK --amount 1000
  node trenches.js sell --token MTK --all
`)
            break
        case 'info':
            console.log(`
Usage: node trenches.js info <SYMBOL>

Get token information from the Trenches API.

Arguments:
  First positional arg or --token: Token symbol (required)

Examples:
  node trenches.js info MTK
  node trenches.js info --token MTK
`)
            break
        default:
            console.log(`
Usage: node trenches.js ${subcommand} [--window <WINDOW>] [--limit <N>]

Show ${subcommand} tokens from Trenches.

Arguments:
  --window, -w      Time window (e.g., 1h, 24h) - trending only
  --limit, -l       Number of results (default: 10)

Examples:
  node trenches.js ${subcommand}
  node trenches.js ${subcommand} --limit 5
`)
            break
    }
}

// ============================================================================
// SUBCOMMAND HANDLERS
// ============================================================================

async function handleCreate(args) {
    if (!args.name || !args.symbol || !args.description) {
        console.error('Error: --name, --symbol, and --description are required')
        printHelp('create')
        process.exit(1)
    }

    // Resolve base token
    let baseTokenAddress
    if (!args.baseToken || args.baseToken.toUpperCase() === 'BID') {
        baseTokenAddress = BID_ADDRESS
        console.log(`\nBase token: BID (${BID_ADDRESS})`)
    } else if (args.baseToken.toUpperCase() === 'ETH') {
        baseTokenAddress = ZERO_ADDRESS
        console.log(`\nBase token: ETH`)
    } else if (args.baseToken.startsWith('0x') && args.baseToken.length === 42) {
        baseTokenAddress = ethers.getAddress(args.baseToken)
        console.log(`\nBase [REDACTED_SECRET]`)
    } else {
        console.error(`Error: Invalid --base-token. Use BID, ETH, or a token address.`)
        process.exit(1)
    }

    const config = loadConfig(args.configDir)
    const { provider, wallet, roles, zodiacHelpersAddress } = loadAgentAndRoles(config, args.configDir, args.rpc)
    const safeAddress = config.safe

    console.log(`Safe: ${safeAddress}`)
    console.log(`Creating [REDACTED_SECRET] (${args.symbol})`)
    console.log(`Anti-bot protection: ${isAntiSnipeEnabled ? 'ON' : 'OFF'}`)

    // Read mintCost from factory
    const factory = new ethers.Contract(AGENT_KEY_FACTORY, FACTORY_READ_ABI, provider)
    const mintCost = await factory.mintCost()
    console.log(`Mint cost: ${formatEth(mintCost)}`)

    const initialBuy = args.initialBuy ? ethers.parseEther(args.initialBuy) : 0n
    const isErc20Base = baseTokenAddress !== ZERO_ADDRESS

    // When base token is ERC20 (e.g. BID), initial buy is in that token, not ETH
    // ethValue only covers mint cost; the ERC20 is pulled via approval
    const totalEthValue = isErc20Base ? mintCost : mintCost + initialBuy

    // Check Safe ETH balance
    const ethBalance = await provider.getBalance(safeAddress)
    console.log(`Safe ETH balance: ${formatEth(ethBalance)}`)

    if (ethBalance < totalEthValue) {
        const detail = isErc20Base
            ? `Need ${formatEth(totalEthValue)} for mint cost`
            : `Need ${formatEth(totalEthValue)} (mint: ${formatEth(mintCost)} + initial buy: ${formatEth(initialBuy)})`
        console.error(`\nInsufficient ETH. ${detail}`)
        process.exit(1)
    }

    // Check ERC20 base token balance for initial buy
    if (isErc20Base && initialBuy > 0n) {
        const baseTokenContract = new ethers.Contract(baseTokenAddress, ERC20_ABI, provider)
        const baseSymbol = await baseTokenContract.symbol()
        const baseDecimals = Number(await baseTokenContract.decimals())
        const baseBalance = await baseTokenContract.balanceOf(safeAddress)
        console.log(`Safe ${baseSymbol} balance: ${formatAmount(baseBalance, baseDecimals, baseSymbol)}`)

        if (baseBalance < initialBuy) {
            console.error(`\nInsufficient ${baseSymbol}. Need ${formatAmount(initialBuy, baseDecimals, baseSymbol)} for initial buy, have ${formatAmount(baseBalance, baseDecimals, baseSymbol)}`)
            process.exit(1)
        }
    }

    // Get auth cookies
    const cookies = await getAuthCookies(config, args.configDir, args.rpc)

    // Upload image if provided
    let imageUrl = ''
    if (args.image) {
        console.log(`\nUploading image: ${args.image}`)
        const imagePath = path.resolve(args.image)
        imageUrl = await uploadImageApi(cookies, imagePath)
        console.log('   Image uploaded successfully!')
    }

    // Call create API
    console.log('\nRequesting token creation signature...')
    const apiResponse = await createTokenApi(cookies, {
        name: args.name,
        symbol: args.symbol,
        description: args.description,
        twitter: args.twitter,
        website: args.website,
        initialBuyAmount: args.initialBuy || '0',
        baseToken: baseTokenAddress,
        isAntiSnipeEnabled,
        imageUrl,
    })
    console.log('Signature received.')

    // Approve ERC20 base token for factory if needed (e.g. BID initial buy)
    const zodiacHelpers = new ethers.Interface(ZODIAC_HELPERS_ABI)

    if (isErc20Base && initialBuy > 0n) {
        console.log(`\nApproving ${formatEth(initialBuy)} of base token for factory...`)
        const approveData = zodiacHelpers.encodeFunctionData('approveForFactory', [
            AGENT_KEY_FACTORY,
            baseTokenAddress,
            initialBuy,
        ])

        const approveTx = await roles.execTransactionWithRole(
            zodiacHelpersAddress,
            0n,
            approveData,
            1, // delegatecall
            config.roleKey,
            true,
        )
        console.log(`   Transaction: ${approveTx.hash}`)
        const approveReceipt = await approveTx.wait()
        if (approveReceipt.status !== 1) {
            console.error('Approval transaction failed!')
            process.exit(1)
        }
        console.log('   Approved!')
    }

    // Encode ZodiacHelpers.createViaFactory call
    const encodedData = zodiacHelpers.encodeFunctionData('createViaFactory', [
        AGENT_KEY_FACTORY,
        {
            signature: apiResponse.signature,
            data: apiResponse.data,
            expiresAt: BigInt(apiResponse.expiresAt),
        },
        totalEthValue,
    ])

    // Execute via Roles (delegatecall, value=0n — ETH passed as explicit ethValue param)
    console.log('\nExecuting on-chain...')
    console.log(`   Total ETH: ${formatEth(totalEthValue)}`)
    if (initialBuy > 0n) {
        console.log(`   Initial buy: ${formatEth(initialBuy)}`)
    }

    const tx = await roles.execTransactionWithRole(
        zodiacHelpersAddress,
        0n,
        encodedData,
        1, // delegatecall
        config.roleKey,
        true,
    )
    console.log(`   Transaction: ${tx.hash}`)

    const receipt = await tx.wait()
    if (receipt.status !== 1) {
        console.error('Transaction failed!')
        process.exit(1)
    }

    // Try to parse TokenCreated event for token address
    let tokenAddress = null
    for (const log of receipt.logs) {
        try {
            if (log.topics[0] === ethers.id('TokenCreated(address,address,string,string)')) {
                tokenAddress = ethers.getAddress('0x' + log.topics[1].slice(26))
                break
            }
        } catch {}
    }

    console.log('\nTOKEN CREATED')
    if (tokenAddress) {
        console.log(`   [REDACTED_SECRET]`)
    }
    console.log(`   Name: ${args.name}`)
    console.log(`   Symbol: ${args.symbol}`)
    console.log(`   Tx: ${receipt.hash}`)
}

async function handleBuy(args) {
    if (!args.token) {
        console.error('Error: --token is required')
        printHelp('buy')
        process.exit(1)
    }
    if (!args.amount && !args.all) {
        console.error('Error: --amount or --all is required')
        printHelp('buy')
        process.exit(1)
    }

    const config = loadConfig(args.configDir)
    const { provider, wallet, roles, zodiacHelpersAddress } = loadAgentAndRoles(config, args.configDir, args.rpc)
    const safeAddress = config.safe

    // Resolve token and detect base token in one API call
    console.log(`\nResolving [REDACTED_SECRET]`)
    let tokenAddress, tokenSymbol, tokenDecimals
    let baseTokenAddress = WETH_ADDRESS
    let baseTokenSymbol = 'ETH'

    if (args.token.startsWith('0x') && args.token.length === 42) {
        tokenAddress = ethers.getAddress(args.token)
        const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider)
        tokenSymbol = await tokenContract.symbol()
        tokenDecimals = Number(await tokenContract.decimals())

        // Check anti-bot status and detect base token via API
        try {
            const tokenInfo = await getTokenInfo(tokenSymbol)
            if (tokenInfo.isAntiBotActive) {
                console.error(`\nError: Anti-bot protection is active for ${tokenSymbol}.`)
                console.error('The agent cannot buy during the protection window. Try again later.')
                process.exit(1)
            }
            if (tokenInfo.baseToken && tokenInfo.baseToken.address &&
                tokenInfo.baseToken.address.toLowerCase() === BID_ADDRESS.toLowerCase()) {
                baseTokenAddress = BID_ADDRESS
                baseTokenSymbol = 'BID'
            }
        } catch {}
    } else {
        const tokenInfo = await getTokenInfo(args.token)
        tokenAddress = ethers.getAddress(tokenInfo.address || tokenInfo.tokenAddress)
        tokenSymbol = tokenInfo.symbol || args.token
        tokenDecimals = tokenInfo.decimals || 18

        if (tokenInfo.isAntiBotActive) {
            console.error(`\nError: Anti-bot protection is active for ${tokenSymbol}.`)
            console.error('The agent cannot buy during the protection window. Try again later.')
            process.exit(1)
        }
        if (tokenInfo.baseToken && tokenInfo.baseToken.address &&
            tokenInfo.baseToken.address.toLowerCase() === BID_ADDRESS.toLowerCase()) {
            baseTokenAddress = BID_ADDRESS
            baseTokenSymbol = 'BID'
        }
    }

    console.log(`[REDACTED_SECRET] (${tokenAddress})`)

    const isErc20Buy = baseTokenAddress !== WETH_ADDRESS

    // Determine buy amount
    let amountIn
    if (isErc20Buy) {
        // BID-paired: check BID balance
        const baseTokenContract = new ethers.Contract(baseTokenAddress, ERC20_ABI, provider)
        const baseBalance = await baseTokenContract.balanceOf(safeAddress)
        console.log(`Safe ${baseTokenSymbol} balance: ${formatAmount(baseBalance, 18, baseTokenSymbol)}`)

        if (args.all) {
            amountIn = baseBalance
            if (amountIn === 0n) {
                console.error(`\nNo ${baseTokenSymbol} to spend`)
                process.exit(1)
            }
            console.log(`Buying with all: ${formatAmount(amountIn, 18, baseTokenSymbol)}`)
        } else {
            amountIn = ethers.parseEther(args.amount)
            console.log(`Buy amount: ${formatAmount(amountIn, 18, baseTokenSymbol)}`)
            if (baseBalance < amountIn) {
                console.error(`\nInsufficient ${baseTokenSymbol}. Need ${formatAmount(amountIn, 18, baseTokenSymbol)}, have ${formatAmount(baseBalance, 18, baseTokenSymbol)}`)
                process.exit(1)
            }
        }
    } else {
        // ETH-paired: check ETH balance
        const ethBalance = await provider.getBalance(safeAddress)
        console.log(`Safe ETH balance: ${formatEth(ethBalance)}`)

        if (args.all) {
            amountIn = ethBalance
            if (amountIn === 0n) {
                console.error('\nNo ETH to spend')
                process.exit(1)
            }
            console.log(`Buying with all: ${formatEth(amountIn)}`)
        } else {
            amountIn = ethers.parseEther(args.amount)
            console.log(`Buy amount: ${formatEth(amountIn)}`)
            if (ethBalance < amountIn) {
                console.error(`\nInsufficient ETH. Need ${formatEth(amountIn)}, have ${formatEth(ethBalance)}`)
                process.exit(1)
            }
        }
    }

    // Get auth cookies
    const cookies = await getAuthCookies(config, args.configDir, args.rpc)

    // Get swap signature from API
    console.log('\nGetting swap signature...')
    const apiResponse = await getSwapSignature(cookies, tokenAddress, amountIn.toString(), true)
    console.log('Signature received.')

    // Compute sqrtPriceLimit using actual base token
    const sqrtPriceLimit = getSqrtPriceLimit(true, tokenAddress, baseTokenAddress)

    const zodiacHelpers = new ethers.Interface(ZODIAC_HELPERS_ABI)

    // For ERC20-paired buys (e.g. BID), approve base token for factory
    if (isErc20Buy) {
        console.log(`\nApproving ${formatAmount(amountIn, 18, baseTokenSymbol)} for factory...`)
        const approveData = zodiacHelpers.encodeFunctionData('approveForFactory', [
            AGENT_KEY_FACTORY,
            baseTokenAddress,
            amountIn,
        ])
        const approveTx = await roles.execTransactionWithRole(
            zodiacHelpersAddress,
            0n,
            approveData,
            1, // delegatecall
            config.roleKey,
            true,
        )
        await approveTx.wait()
        console.log('   Approved.')
    }

    // Encode ZodiacHelpers.tradeViaFactory call
    const encodedData = zodiacHelpers.encodeFunctionData('tradeViaFactory', [
        AGENT_KEY_FACTORY,
        isErc20Buy ? baseTokenAddress : ZERO_ADDRESS,  // inputToken
        isErc20Buy ? amountIn : 0n,                    // approvalAmount
        {
            signature: apiResponse.signature,
            data: apiResponse.data,
            expiresAt: BigInt(apiResponse.expiresAt),
            nonce: BigInt(apiResponse.nonce),
        },
        {
            sqrtPriceLimit,
            minAmountOut: 0n,
        },
        isErc20Buy ? 0n : amountIn,  // ethValue (0 for ERC20 buys)
    ])

    // Execute via Roles (delegatecall)
    console.log('\nExecuting buy...')
    const tx = await roles.execTransactionWithRole(
        zodiacHelpersAddress,
        0n,
        encodedData,
        1, // delegatecall
        config.roleKey,
        true,
    )
    console.log(`   Transaction: ${tx.hash}`)

    const receipt = await tx.wait()
    if (receipt.status !== 1) {
        console.error('Transaction failed!')
        process.exit(1)
    }

    // Show new balance
    const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider)
    const newBalance = await tokenContract.balanceOf(safeAddress)

    console.log('\nBUY COMPLETE')
    console.log(`   Spent: ${formatAmount(amountIn, 18, baseTokenSymbol)}`)
    console.log(`   [REDACTED_SECRET]`)
    console.log(`   New balance: ${formatAmount(newBalance, tokenDecimals, tokenSymbol)}`)
    console.log(`   Tx: ${receipt.hash}`)
}

async function handleSell(args) {
    if (!args.token) {
        console.error('Error: --token is required')
        printHelp('sell')
        process.exit(1)
    }
    if (!args.amount && !args.all) {
        console.error('Error: --amount or --all is required')
        printHelp('sell')
        process.exit(1)
    }

    const config = loadConfig(args.configDir)
    const { provider, wallet, roles, zodiacHelpersAddress } = loadAgentAndRoles(config, args.configDir, args.rpc)
    const safeAddress = config.safe

    // Resolve token and detect base token in one API call
    console.log(`\nResolving [REDACTED_SECRET]`)
    let tokenAddress, tokenSymbol, tokenDecimals
    let baseTokenAddress = WETH_ADDRESS
    let baseTokenSymbol = 'ETH'

    if (args.token.startsWith('0x') && args.token.length === 42) {
        tokenAddress = ethers.getAddress(args.token)
        const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider)
        tokenSymbol = await tokenContract.symbol()
        tokenDecimals = Number(await tokenContract.decimals())

        try {
            const tokenInfo = await getTokenInfo(tokenSymbol)
            if (tokenInfo.baseToken && tokenInfo.baseToken.address &&
                tokenInfo.baseToken.address.toLowerCase() === BID_ADDRESS.toLowerCase()) {
                baseTokenAddress = BID_ADDRESS
                baseTokenSymbol = 'BID'
            }
        } catch {}
    } else {
        const tokenInfo = await getTokenInfo(args.token)
        tokenAddress = ethers.getAddress(tokenInfo.address || tokenInfo.tokenAddress)
        tokenSymbol = tokenInfo.symbol || args.token
        tokenDecimals = tokenInfo.decimals || 18

        if (tokenInfo.baseToken && tokenInfo.baseToken.address &&
            tokenInfo.baseToken.address.toLowerCase() === BID_ADDRESS.toLowerCase()) {
            baseTokenAddress = BID_ADDRESS
            baseTokenSymbol = 'BID'
        }
    }

    console.log(`[REDACTED_SECRET] (${tokenAddress})`)

    // Get token balance
    const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider)
    const tokenBalance = await tokenContract.balanceOf(safeAddress)
    console.log(`Safe ${tokenSymbol} balance: ${formatAmount(tokenBalance, tokenDecimals, tokenSymbol)}`)

    let amountIn
    if (args.all) {
        amountIn = tokenBalance
        if (amountIn === 0n) {
            console.error(`\nNo ${tokenSymbol} to sell`)
            process.exit(1)
        }
        console.log(`Selling all: ${formatAmount(amountIn, tokenDecimals, tokenSymbol)}`)
    } else {
        amountIn = ethers.parseUnits(args.amount, tokenDecimals)
        if (tokenBalance < amountIn) {
            console.error(`\nInsufficient ${tokenSymbol}. Need ${formatAmount(amountIn, tokenDecimals, tokenSymbol)}, have ${formatAmount(tokenBalance, tokenDecimals, tokenSymbol)}`)
            process.exit(1)
        }
        console.log(`Sell amount: ${formatAmount(amountIn, tokenDecimals, tokenSymbol)}`)
    }

    // Get auth cookies
    const cookies = await getAuthCookies(config, args.configDir, args.rpc)

    // Get swap signature from API
    console.log('\nGetting swap signature...')
    const apiResponse = await getSwapSignature(cookies, tokenAddress, amountIn.toString(), false)
    console.log('Signature received.')

    // Compute sqrtPriceLimit using actual base token
    const sqrtPriceLimit = getSqrtPriceLimit(false, tokenAddress, baseTokenAddress)

    // Encode ZodiacHelpers.tradeViaFactory call
    // tradeViaFactory bundles approval + trade in a single call
    const zodiacHelpers = new ethers.Interface(ZODIAC_HELPERS_ABI)
    const encodedData = zodiacHelpers.encodeFunctionData('tradeViaFactory', [
        AGENT_KEY_FACTORY,
        tokenAddress,       // inputToken (selling this token)
        amountIn,           // approvalAmount (approve token for factory)
        {
            signature: apiResponse.signature,
            data: apiResponse.data,
            expiresAt: BigInt(apiResponse.expiresAt),
            nonce: BigInt(apiResponse.nonce),
        },
        {
            sqrtPriceLimit,
            minAmountOut: 0n,
        },
        0n,                 // ethValue (0 for sells)
    ])

    // Execute via Roles (delegatecall, value=0n)
    console.log('\nExecuting sell...')
    const tx = await roles.execTransactionWithRole(
        zodiacHelpersAddress,
        0n,
        encodedData,
        1, // delegatecall
        config.roleKey,
        true,
    )
    console.log(`   Transaction: ${tx.hash}`)

    const receipt = await tx.wait()
    if (receipt.status !== 1) {
        console.error('Transaction failed!')
        process.exit(1)
    }

    // Show updated balances
    const newTokenBalance = await tokenContract.balanceOf(safeAddress)

    console.log('\nSELL COMPLETE')
    console.log(`   Sold: ${formatAmount(amountIn, tokenDecimals, tokenSymbol)}`)
    console.log(`   Remaining ${tokenSymbol}: ${formatAmount(newTokenBalance, tokenDecimals, tokenSymbol)}`)

    if (baseTokenAddress === BID_ADDRESS) {
        const bidContract = new ethers.Contract(BID_ADDRESS, ERC20_ABI, provider)
        const newBidBalance = await bidContract.balanceOf(safeAddress)
        console.log(`   BID balance: ${formatAmount(newBidBalance, 18, 'BID')}`)
    } else {
        const newEthBalance = await provider.getBalance(safeAddress)
        console.log(`   ETH balance: ${formatEth(newEthBalance)}`)
    }
    console.log(`   Tx: ${receipt.hash}`)
}

async function handleInfo(args) {
    const symbol = args.token
    if (!symbol) {
        console.error('Error: token symbol is required')
        console.error('Usage: node trenches.js info <SYMBOL>')
        process.exit(1)
    }

    console.log(`\nFetching info for ${symbol}...\n`)
    const data = await getTokenInfo(symbol)

    console.log('='.repeat(50))
    console.log(`  ${data.name || symbol} (${data.symbol || symbol})`)
    console.log('='.repeat(50))
    if (data.address || data.tokenAddress) console.log(`  Address:     ${data.address || data.tokenAddress}`)
    if (data.description) console.log(`  Description: ${data.description}`)
    if (data.price) console.log(`  Price:       $${data.price}`)
    if (data.marketCap) console.log(`  Market Cap:  $${Number(data.marketCap).toLocaleString()}`)
    if (data.volume24h) console.log(`  24h Volume:  $${Number(data.volume24h).toLocaleString()}`)
    if (data.liquidity) console.log(`  Liquidity:   $${Number(data.liquidity).toLocaleString()}`)
    if (data.holders) console.log(`  Holders:     ${data.holders}`)
    if (data.creator) console.log(`  Creator:     ${data.creator}`)
    console.log('='.repeat(50))
}

async function handleDiscovery(args) {
    const endpointMap = {
        'trending': 'trending',
        'new': 'new',
        'top-volume': 'top-volume',
        'gainers': 'top-gainers',
        'losers': 'top-losers',
    }

    const endpoint = endpointMap[args.subcommand]
    if (!endpoint) {
        console.error(`Unknown discovery subcommand: ${args.subcommand}`)
        process.exit(1)
    }

    const params = {}
    if (args.window) params.window = args.window
    if (args.limit) params.limit = args.limit

    const title = args.subcommand.charAt(0).toUpperCase() + args.subcommand.slice(1).replace('-', ' ')
    console.log(`\nFetching ${title.toLowerCase()} tokens...\n`)

    const data = await getDiscovery(endpoint, params)
    const tokens = Array.isArray(data) ? data : data.tokens || data.data || []

    if (tokens.length === 0) {
        console.log('No tokens found.')
        return
    }

    console.log(`${'#'.padEnd(4)} ${'Symbol'.padEnd(10)} ${'Name'.padEnd(20)} ${'Price'.padEnd(14)} ${'Change'.padEnd(10)}`)
    console.log('-'.repeat(62))

    tokens.forEach((token, i) => {
        const num = String(i + 1).padEnd(4)
        const sym = (token.symbol || '???').padEnd(10)
        const name = (token.name || '').slice(0, 18).padEnd(20)
        const price = token.price ? `$${Number(token.price).toFixed(6)}`.padEnd(14) : 'N/A'.padEnd(14)
        const change = token.priceChange != null
            ? `${token.priceChange >= 0 ? '+' : ''}${Number(token.priceChange).toFixed(1)}%`
            : ''
        console.log(`${num} ${sym} ${name} ${price} ${change}`)
    })

    console.log(`\n${tokens.length} tokens shown.`)
}

// ============================================================================
// MAIN
// ============================================================================

async function main() {
    const args = parseArgs()

    if (!args.subcommand) {
        printHelp()
        process.exit(0)
    }

    switch (args.subcommand) {
        case 'create':
            await handleCreate(args)
            break
        case 'buy':
            await handleBuy(args)
            break
        case 'sell':
            await handleSell(args)
            break
        case 'info':
            await handleInfo(args)
            break
        case 'trending':
        case 'new':
        case 'top-volume':
        case 'gainers':
        case 'losers':
            await handleDiscovery(args)
            break
        default:
            console.error(`Unknown subcommand: ${args.subcommand}`)
            printHelp()
            process.exit(1)
    }
}

main().catch(error => {
    console.error(`\nError: ${error.message}`)
    process.exit(1)
})


path: scripts/swap.js
content: #!/usr/bin/env node

/**
 * Swap tokens via KyberSwap Aggregator (via Safe + Zodiac Roles)
 *
 * Uses KyberSwap's aggregator API to find optimal swap routes across
 * multiple DEXs on Base. Supports all ERC20 tokens with liquidity.
 *
 * Features:
 * - Resolves token symbols to addresses
 * - Safeguards for common tokens (ETH, USDC, USDT, etc.)
 * - Gets optimal routes across multiple DEXs
 * - 0.5% partner fee (same as CoW Protocol)
 * - Executes swaps via Zodiac Roles delegatecall
 *
 * Usage:
 *   node kyber.js --from ETH --to USDC --amount 0.1
 *   node kyber.js --from USDC --to ETH --amount 100 --execute
 *
 * ETH is handled natively by KyberSwap (no wrapping needed).
 */

import { ethers } from 'ethers'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { VERIFIED_TOKENS, ERC20_ABI, resolveToken } from './tokens.js'

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

const DEFAULT_RPC_URL = 'https://mainnet.base.org'

// Contracts
const WETH_ADDRESS = '0x4200000000000000000000000000000000000006'
const NATIVE_ETH = '0x0000000000000000000000000000000000000000'
const KYBER_NATIVE_TOKEN = [REDACTED_SECRET]

// KyberSwap API constants
const KYBER_API_BASE = 'https://aggregator-api.kyberswap.com'
const KYBER_CHAIN = 'base'
const KYBER_CLIENT_ID = 'clawlett'

// Partner fee (0.5% = 50 bps)
const FEE_BPS = 50
const FEE_RECEIVER = [REDACTED_SECRET]

// ABIs
const ROLES_ABI = [
    'function execTransactionWithRole(address to, uint256 value, bytes data, uint8 operation, bytes32 roleKey, bool shouldRevert) returns (bool)',
]

const ZODIAC_HELPERS_ABI = [
    'function wrapETH(uint256 amount) external',
    'function unwrapWETH(uint256 amount) external',
    'function kyberSwap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes swapData, uint256 ethValue) external',
]

// ============================================================================
// KYBERSWAP API
// ============================================================================

async function getKyberRoute(tokenIn, tokenOut, amountIn, safeAddress) {
    const params = new URLSearchParams({
        tokenIn: tokenIn.kyberAddress,
        tokenOut: tokenOut.kyberAddress,
        amountIn: amountIn.toString(),
        // Partner fee parameters (0.1%)
        feeAmount: String(FEE_BPS),
        chargeFeeBy: 'currency_in',
        isInBps: 'true',
        feeReceiver: FEE_RECEIVER,
    })

    const url = `${KYBER_API_BASE}/${KYBER_CHAIN}/api/v1/routes?${params}`

    const response = await fetch(url, {
        method: 'GET',
        headers: {
            'X-Client-Id': KYBER_CLIENT_ID,
            'Accept': 'application/json',
        },
    })

    const data = await response.json()

    if (!response.ok || data.code !== 0) {
        const errorMsg = data.message || data.description || JSON.stringify(data)
        throw new Error(`KyberSwap route failed: ${errorMsg}`)
    }

    if (!data.data || !data.data.routeSummary) {
        throw new Error('KyberSwap: No route found for this pair')
    }

    return data.data
}

async function buildKyberSwap(routeData, safeAddress, slippageTolerance) {
    const url = `${KYBER_API_BASE}/${KYBER_CHAIN}/api/v1/route/build`

    const requestBody = {
        routeSummary: routeData.routeSummary,
        sender: safeAddress,
        recipient: safeAddress,
        slippageTolerance: slippageTolerance, // in bps, e.g., 50 = 0.5%
    }

    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'X-Client-Id': KYBER_CLIENT_ID,
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        body: JSON.stringify(requestBody),
    })

    const data = await response.json()

    if (!response.ok || data.code !== 0) {
        const errorMsg = data.message || data.description || JSON.stringify(data)
        throw new Error(`KyberSwap build failed: ${errorMsg}`)
    }

    return data.data
}

// ============================================================================
// HELPERS
// ============================================================================

function formatAmount(amount, decimals, symbol) {
    const formatted = ethers.formatUnits(amount, decimals)
    const num = parseFloat(formatted)
    if (num < 0.01) return `${formatted} ${symbol}`
    return `${num.toLocaleString(undefined, { maximumFractionDigits: 6 })} ${symbol}`
}

function loadConfig(configDir) {
    const configPath = path.join(configDir, 'wallet.json')
    if (!fs.existsSync(configPath)) {
        throw new Error(`Config not found: ${configPath}\nRun initialize.js first.`)
    }
    return JSON.parse(fs.readFileSync(configPath, 'utf8'))
}

function parseArgs() {
    const args = process.argv.slice(2)
    const result = {
        from: null,
        to: null,
        amount: null,
        configDir: process.env.WALLET_CONFIG_DIR || path.join(__dirname, '..', 'config'),
        rpc: process.env.BASE_RPC_URL || DEFAULT_RPC_URL,
        slippage: 50, // 0.5% default in bps
        execute: false,
    }

    for (let i = 0; i < args.length; i++) {
        switch (args[i]) {
            case '--from':
            case '-f':
                result.from = args[++i]
                break
            case '--to':
            case '-t':
                result.to = args[++i]
                break
            case '--amount':
            case '-a':
                result.amount = args[++i]
                break
            case '--slippage':
                // Accept slippage as percentage (e.g., 0.5 for 0.5%) or bps (e.g., 50)
                const slipVal = parseFloat(args[++i])
                result.slippage = slipVal < 1 ? Math.round(slipVal * 100) : Math.round(slipVal)
                break
            case '--execute':
            case '-x':
                result.execute = true
                break
            case '--config-dir':
            case '-c':
                result.configDir = args[++i]
                break
            case '--rpc':
            case '-r':
                result.rpc = args[++i]
                break
            case '--help':
            case '-h':
                printHelp()
                process.exit(0)
        }
    }

    return result
}

function printHelp() {
    console.log(`
Usage: node kyber.js --from <TOKEN> --to <TOKEN> --amount <AMOUNT> [--execute]

Swap tokens via KyberSwap Aggregator (optimal routes across DEXs).

Arguments:
  --from, -f       Token to swap from (symbol or address)
  --to, -t         Token to swap to (symbol or address)
  --amount, -a     Amount to swap
  --slippage       Slippage in % or bps (default: 0.5% = 50 bps)
  --execute, -x    Execute swap (default: quote only)
  --config-dir, -c Config directory
  --rpc, -r        RPC URL (default: ${DEFAULT_RPC_URL})

Notes:
  - KyberSwap aggregates liquidity from multiple DEXs for optimal rates.
  - Native ETH is supported directly (no wrapping needed).
  - A 0.5% partner fee is applied (same as CoW Protocol).

Verified Tokens:
  ETH, WETH, USDC, USDT, DAI, USDS, AERO, cbBTC, VIRTUAL, DEGEN, BRETT, TOSHI, WELL
  Other tokens are searched via DexScreener (Base pairs).
  Unverified tokens show a warning with contract address, volume, and liquidity.

Examples:
  node kyber.js --from ETH --to USDC --amount 0.1
  node kyber.js --from USDC --to WETH --amount 100 --execute
  node kyber.js --from USDC --to DAI --amount 50 --execute --slippage 1
`)
}

// Convert token address to KyberSwap format
function toKyberAddress(address) {
    if (address.toLowerCase() === NATIVE_ETH) {
        return KYBER_NATIVE_TOKEN
    }
    return address
}

// ============================================================================
// MAIN
// ============================================================================

async function main() {
    const args = parseArgs()

    if (!args.from || !args.to || !args.amount) {
        console.error('Error: --from, --to, and --amount are required')
        printHelp()
        process.exit(1)
    }

    let config
    try {
        config = loadConfig(args.configDir)
    } catch (error) {
        console.error(`Error: ${error.message}`)
        process.exit(1)
    }

    const provider = new ethers.JsonRpcProvider(args.rpc, 8453, { staticNetwork: true })
    const safeAddress = config.safe

    console.log('\nResolving tokens...\n')

    let tokenIn, tokenOut
    try {
        tokenIn = await resolveToken(args.from, provider)
    } catch (error) {
        console.error(`\n${error.message}`)
        process.exit(1)
    }

    try {
        tokenOut = await resolveToken(args.to, provider)
    } catch (error) {
        console.error(`\n${error.message}`)
        process.exit(1)
    }

    // Add KyberSwap-specific address format
    tokenIn.kyberAddress = toKyberAddress(tokenIn.address)
    tokenOut.kyberAddress = toKyberAddress(tokenOut.address)

    const isNativeIn = tokenIn.address.toLowerCase() === NATIVE_ETH
    const isNativeOut = tokenOut.address.toLowerCase() === NATIVE_ETH

    console.log(`From: ${tokenIn.symbol} ${tokenIn.verified ? '(verified)' : '(unverified)'}`)
    console.log(`      ${tokenIn.address}`)
    if (tokenIn.warning) console.log(`\n${tokenIn.warning}\n`)

    console.log(`To:   ${tokenOut.symbol} ${tokenOut.verified ? '(verified)' : '(unverified)'}`)
    console.log(`      ${tokenOut.address}`)
    if (tokenOut.warning) console.log(`\n${tokenOut.warning}\n`)

    const amountIn = ethers.parseUnits(args.amount, tokenIn.decimals)
    console.log(`\nAmount: ${formatAmount(amountIn, tokenIn.decimals, tokenIn.symbol)}`)

    // Check balance
    let balance
    if (isNativeIn) {
        balance = await provider.getBalance(safeAddress)
        console.log(`Safe ETH balance: ${formatAmount(balance, 18, 'ETH')}`)
    } else {
        const tokenContract = new ethers.Contract(tokenIn.address, ERC20_ABI, provider)
        balance = await tokenContract.balanceOf(safeAddress)
        console.log(`Safe balance: ${formatAmount(balance, tokenIn.decimals, tokenIn.symbol)}`)
    }

    if (balance < amountIn) {
        console.error(`\nInsufficient ${tokenIn.symbol} balance in Safe`)
        process.exit(1)
    }

    // Get KyberSwap route
    console.log('\nGetting KyberSwap route...\n')

    let routeData
    try {
        routeData = await getKyberRoute(tokenIn, tokenOut, amountIn, safeAddress)
    } catch (error) {
        console.error(`${error.message}`)
        console.error(`\nTip: If KyberSwap has no liquidity for this pair, try CoW Protocol instead.`)
        process.exit(1)
    }

    const routeSummary = routeData.routeSummary
    const amountOut = BigInt(routeSummary.amountOut)
    const gasEstimate = routeSummary.gas || '0'
    const gasPriceGwei = routeSummary.gasPrice ? (Number(routeSummary.gasPrice) / 1e9).toFixed(2) : 'N/A'

    // Calculate fee amount (0.5% of input)
    const feeAmount = amountIn * BigInt(FEE_BPS) / 10000n

    // Calculate minimum output with slippage
    const minAmountOut = amountOut * BigInt(10000 - args.slippage) / 10000n
    const slippagePct = args.slippage / 100

    console.log('='.repeat(55))
    console.log('                    SWAP SUMMARY')
    console.log('='.repeat(55))
    console.log(`  You pay:      ${formatAmount(amountIn, tokenIn.decimals, tokenIn.symbol)}`)
    console.log(`  You receive:  ~${formatAmount(amountOut, tokenOut.decimals, tokenOut.symbol)}`)
    console.log(`  Min receive:  ${formatAmount(minAmountOut, tokenOut.decimals, tokenOut.symbol)} (${slippagePct}% slippage)`)
    console.log(`  Gas estimate: ${gasEstimate} (${gasPriceGwei} gwei)`)
    console.log(`  Router:       ${routeData.routerAddress}`)
    console.log(`  Route:        ${routeSummary.route?.length || 1} hop(s) via KyberSwap Aggregator`)
    console.log('='.repeat(55))

    if (!args.execute) {
        console.log('\nQUOTE ONLY - Add --execute to perform the swap')
        console.log(`\nTo execute: node kyber.js --from "${args.from}" --to "${args.to}" --amount ${args.amount} --execute`)
        process.exit(0)
    }

    // ========================================================================
    // EXECUTION
    // ========================================================================

    console.log('\nExecuting KyberSwap...\n')

    const agentPkPath = path.join(args.configDir, 'agent.pk')
    if (!fs.existsSync(agentPkPath)) {
        console.error('Error: Agent private key not found')
        process.exit(1)
    }
    let privateKey = fs.readFileSync(agentPkPath, 'utf8').trim()
    if (!privateKey.startsWith('0x')) privateKey = '0x' + privateKey

    const wallet = new ethers.Wallet(privateKey, provider)
    const roles = new ethers.Contract(config.roles, ROLES_ABI, wallet)

    const zodiacHelpersAddress = config.contracts?.ZodiacHelpers
    if (!zodiacHelpersAddress) {
        console.error('Error: ZodiacHelpers address not found in config. Re-run initialize.js.')
        process.exit(1)
    }

    // Step 1: Build swap transaction from KyberSwap API
    console.log('Step 1: Building swap transaction...')
    let buildData
    try {
        buildData = await buildKyberSwap(routeData, safeAddress, args.slippage)
    } catch (error) {
        console.error(`${error.message}`)
        process.exit(1)
    }
    console.log(`   Router: ${buildData.routerAddress}`)
    console.log(`   Data length: ${buildData.data.length} bytes`)

    // Step 2: Execute via ZodiacHelpers.kyberSwap
    console.log('\nStep 2: Executing swap via ZodiacHelpers...')

    const zodiacHelpersInterface = new ethers.Interface(ZODIAC_HELPERS_ABI)

    // For native ETH swaps, we need to pass the ethValue
    const ethValue = isNativeIn ? amountIn : 0n

    // Encode the kyberSwap call
    const swapData = zodiacHelpersInterface.encodeFunctionData('kyberSwap', [
        isNativeIn ? NATIVE_ETH : tokenIn.address,
        isNativeOut ? NATIVE_ETH : tokenOut.address,
        amountIn,
        minAmountOut,
        buildData.data,
        ethValue,
    ])

    // Execute via Roles (delegatecall to ZodiacHelpers)
    console.log('   Executing on-chain...')
    const tx = await roles.execTransactionWithRole(
        zodiacHelpersAddress,
        0n, // value is 0 - ethValue is passed in swapData
        swapData,
        1, // delegatecall
        config.roleKey,
        true
    )
    console.log(`   Transaction: ${tx.hash}`)

    const receipt = await tx.wait()
    if (receipt.status !== 1) {
        console.error('Transaction failed!')
        process.exit(1)
    }

    // Step 3: Show results
    console.log('\nSWAP COMPLETE')
    console.log(`   Sold: ${formatAmount(amountIn, tokenIn.decimals, tokenIn.symbol)}`)
    console.log(`   Received: ~${formatAmount(amountOut, tokenOut.decimals, tokenOut.symbol)}`)

    // Get new balance
    if (isNativeOut) {
        const newBalance = await provider.getBalance(safeAddress)
        console.log(`   New ETH balance: ${formatAmount(newBalance, 18, 'ETH')}`)
    } else {
        const outContract = new ethers.Contract(tokenOut.address, ERC20_ABI, provider)
        const newBalance = await outContract.balanceOf(safeAddress)
        console.log(`   New ${tokenOut.symbol} balance: ${formatAmount(newBalance, tokenOut.decimals, tokenOut.symbol)}`)
    }

    console.log(`   Tx: https://basescan.org/tx/${receipt.hash}`)
}

main().catch(error => {
    console.error(`\nError: ${error.message}`)
    process.exit(1)
})


path: scripts/initialize.js
content: #!/usr/bin/env node

/**
 * Initialize Claw Wallet (Fully Autonomous)
 *
 * Agent does everything autonomously, then transfers ownership to human.
 *
 * Steps:
 * 1. Generate agent keypair (or use existing)
 * 2. Deploy Safe with agent as initial owner
 * 3. Deploy Zodiac Roles module
 * 4. Configure everything via MultiSend (enable module + permissions)
 * 5. Register on ERC-8004 Identity Registry
 * 6. Register with backend API
 *
 * Usage: node initialize.js --owner 0x123...
 */

import { ethers } from 'ethers'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

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

const DEFAULT_RPC_URL = 'https://mainnet.base.org'
const CHAIN_ID = 8453
const API_BASE_URL = process.env.WALLET_API_URL || 'https://trenches.bid'

// Contract addresses
const CONTRACTS = {
    SafeProxyFactory: [REDACTED_SECRET],
    SafeSingletonL2: [REDACTED_SECRET],
    CompatibilityFallbackHandler: [REDACTED_SECRET],
    MultiSend: [REDACTED_SECRET],
    RolesSingleton: [REDACTED_SECRET],
    ModuleProxyFactory: [REDACTED_SECRET],
    AeroUniversalRouter: [REDACTED_SECRET],
    ZodiacHelpers: [REDACTED_SECRET],
    KyberSwapRouter: [REDACTED_SECRET],
    IdentityRegistry: [REDACTED_SECRET],
    CNS: [REDACTED_SECRET],
}

// ABIs
const SAFE_FACTORY_ABI = [
    'function createProxyWithNonce(address singleton, bytes initializer, uint256 saltNonce) returns (address)',
]

const SAFE_ABI = [
    'function setup(address[] owners, uint256 threshold, address to, bytes data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver)',
    'function enableModule(address module)',
    'function execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures) returns (bool)',
    'function getOwners() view returns (address[])',
    'function isModuleEnabled(address module) view returns (bool)',
    'function nonce() view returns (uint256)',
    'function addOwnerWithThreshold(address owner, uint256 _threshold)',
    'function removeOwner(address prevOwner, address owner, uint256 _threshold)',
]

const MODULE_FACTORY_ABI = [
    'function deployModule(address masterCopy, bytes initializer, uint256 saltNonce) returns (address)',
]

const ROLES_ABI = [
    'function setUp(bytes initParams)',
    'function setDefaultRole(address module, bytes32 roleKey)',
    'function assignRoles(address module, bytes32[] roleKeys, bool[] memberOf)',
    'function scopeTarget(bytes32 roleKey, address targetAddress)',
    'function allowTarget(bytes32 roleKey, address targetAddress, uint8 options)',
    'function owner() view returns (address)',
]

const MULTISEND_ABI = [
    'function multiSend(bytes transactions) payable',
]

const IDENTITY_REGISTRY_ABI = [
    'function register(string agentURI) returns (uint256 agentId)',
    'function transferFrom(address from, address to, uint256 tokenId)',
    'function ownerOf(uint256 tokenId) view returns (address)',
    'event Registered(uint256 indexed agentId, string agentURI, address indexed owner)',
]

const CNS_ABI = [
    {
        type: 'function', name: 'register', stateMutability: 'payable',
        inputs: [{
            name: '_signature', type: 'tuple',
            components: [
                { name: 'signature', type: 'bytes' },
                { name: 'data', type: 'bytes' },
                { name: 'expiresAt', type: 'uint256' },
            ],
        }],
        outputs: [{ name: 'tokenId', type: 'uint256' }],
    },
    'function fee() view returns (uint256)',
    'function isNameAvailable(string _name) view returns (bool)',
    'function balanceOf(address owner) view returns (uint256)',
    'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)',
    'function tokenName(uint256 _tokenId) view returns (string)',
    'event NameRegistered(address indexed owner, uint256 indexed tokenId, string name)',
]

// Role key for wallet operations
const ROLE_KEY = ethers.keccak256(ethers.toUtf8Bytes('WalletSwapper'))

// Execution options for Roles
const ExecutionOptions = {
    None: 0,
    Send: 1,
    DelegateCall: 2,
    Both: 3,
}

// Initialization steps
const STEPS = {
    AGENT_CREATED: 'agent_created',
    SAFE_DEPLOYED: 'safe_deployed',
    ROLES_DEPLOYED: 'roles_deployed',
    CONFIGURED: 'configured',
    ERC8004_REGISTERED: 'erc8004_registered',
    COMPLETE: 'complete',
}

function parseArgs() {
    const args = process.argv.slice(2)
    const result = {
        owner: null,
        name: null,
        configDir: process.env.WALLET_CONFIG_DIR || path.join(__dirname, '..', 'config'),
        rpc: process.env.BASE_RPC_URL || DEFAULT_RPC_URL,
    }

    for (let i = 0; i < args.length; i++) {
        switch (args[i]) {
            case '--owner':
            case '-o':
                result.owner = args[++i]
                break
            case '--name':
            case '-n':
                result.name = args[++i]
                break
            case '--config-dir':
            case '-c':
                result.configDir = args[++i]
                break
            case '--rpc':
            case '-r':
                result.rpc = args[++i]
                break
            case '--help':
            case '-h':
                printHelp()
                process.exit(0)
        }
    }

    return result
}

function printHelp() {
    console.log(`
Usage: node initialize.js --owner <OWNER_ADDRESS> [--name <AGENT_NAME>]

Arguments:
  --owner, -o      Owner wallet address (will be sole Safe owner after setup)
  --name, -n       Unique agent name for CNS (optional, registers on Clawlett Name Service)
  --config-dir, -c Config directory (default: ../config)
  --rpc, -r        RPC URL (default: ${DEFAULT_RPC_URL})

This script is idempotent - run it multiple times to continue from where it left off.
`)
}

function loadState(configDir) {
    const statePath = path.join(configDir, 'init-state.json')
    if (fs.existsSync(statePath)) {
        return JSON.parse(fs.readFileSync(statePath, 'utf8'))
    }
    return { step: null }
}

function saveState(configDir, state) {
    const statePath = path.join(configDir, 'init-state.json')
    fs.writeFileSync(statePath, JSON.stringify(state, null, 2))
}

function clearState(configDir) {
    const statePath = path.join(configDir, 'init-state.json')
    if (fs.existsSync(statePath)) {
        fs.unlinkSync(statePath)
    }
}

// Backend registration functions
async function checkExistingAgent(wallet) {
    try {
        const url = `${API_BASE_URL}/api/skill/agent?wallet=${wallet}&chainId=${CHAIN_ID}`
        const response = await fetch(url)
        if (response.ok) {
            const data = await response.json()
            // If needsRegistration is true, agent doesn't exist yet
            if (data.needsRegistration) {
                return null
            }
            return data
        }
    } catch (e) {
        // Ignore - agent not found or API unavailable
    }
    return null
}

async function getRegistrationChallenge(wallet) {
    const url = `${API_BASE_URL}/api/skill/agent?wallet=${wallet}&chainId=${CHAIN_ID}`
    const response = await fetch(url)
    const data = await response.json()

    if (!response.ok) {
        throw new Error(data.error || 'Failed to get registration challenge')
    }

    if (!data.needsRegistration || !data.jwt) {
        throw new Error('Agent already registered')
    }

    return data.jwt
}

// Decode JWT to get the message (without verification - server will verify)
function getMessageFromJwt(jwt) {
    const parts = jwt.split('.')
    if (parts.length !== 3) throw new Error('Invalid JWT')
    const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
    return payload.message
}

async function registerAgent(agentWallet, jwt, agentData) {
    // Sign the challenge message
    const message = getMessageFromJwt(jwt)
    const signature = await agentWallet.signMessage(message)

    const url = `${API_BASE_URL}/api/skill/agent`
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            jwt,
            signature,
            wallet: agentWallet.address,
            ...agentData,
        }),
    })

    const result = await response.json()
    if (!response.ok) {
        throw new Error(result.error || 'Failed to register agent')
    }

    // Extract cookies from response (same as web auth)
    const cookies = response.headers.getSetCookie?.() || []
    const cookieString = cookies.map(c => c.split(';')[0]).join('; ')

    return { ...result, cookies: cookieString }
}

// Backend auth — GET challenge, sign, POST to get session cookies
async function authenticateAgent(agentWallet) {
    // Step 1: Get challenge
    const challengeRes = await fetch(
        `${API_BASE_URL}/api/auth/wallet?wallet=${agentWallet.address.toLowerCase()}`
    )
    const challengeData = await challengeRes.json()
    if (!challengeRes.ok) {
        throw new Error(challengeData.error || 'Failed to get auth challenge')
    }

    // Step 2: Sign challenge and authenticate
    const message = challengeData.message || getMessageFromJwt(challengeData.jwt)
    const signature = await agentWallet.signMessage(message)

    const authRes = await fetch(`${API_BASE_URL}/api/auth/wallet`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            wallet: agentWallet.address.toLowerCase(),
            signature,
            jwt: challengeData.jwt,
        }),
    })

    const authData = await authRes.json()
    if (!authRes.ok) {
        throw new Error(authData.error || 'Authentication failed')
    }

    const cookies = authRes.headers.getSetCookie?.() || []
    const cookieString = cookies.map(c => c.split(';')[0]).join('; ')
    return cookieString
}

// CNS — request signature from backend, then register on-chain
async function getCnsSignature(cookies, name) {
    const res = await fetch(`${API_BASE_URL}/api/cns/signature`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Cookie': cookies,
        },
        body: JSON.stringify({ name, chainId: String(CHAIN_ID) }),
    })

    const data = await res.json()
    if (!res.ok) {
        throw new Error(data.error || 'Failed to get CNS signature')
    }

    return data
}

function saveConfig(configDir, config) {
    const configPath = path.join(configDir, 'wallet.json')
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
}

// MultiSend encoding helpers
function encodeMultiSendTx(operation, to, value, data) {
    const dataBytes = ethers.getBytes(data)
    return ethers.solidityPacked(
        ['uint8', 'address', 'uint256', 'uint256', 'bytes'],
        [operation, to, value, dataBytes.length, dataBytes]
    )
}

function encodeMultiSend(transactions) {
    const encoded = ethers.concat(
        transactions.map(tx => encodeMultiSendTx(tx.operation || 0, tx.to, tx.value || 0n, tx.data))
    )
    const multiSendInterface = new ethers.Interface(MULTISEND_ABI)
    return multiSendInterface.encodeFunctionData('multiSend', [encoded])
}

// Execute Safe transaction as owner
async function execSafeTransaction(safe, to, value, data, operation, signer) {
    const safeTxGas = 0
    const baseGas = 0
    const gasPrice = 0
    const gasToken = ethers.ZeroAddress
    const refundReceiver = ethers.ZeroAddress

    // Pre-approved signature for owner
    const signature = ethers.concat([
        ethers.zeroPadValue(signer.address, 32),
        ethers.zeroPadValue('0x', 32),
        '0x01'
    ])

    const tx = await safe.execTransaction(
        to, value, data, operation,
        safeTxGas, baseGas, gasPrice, gasToken, refundReceiver,
        signature
    )

    return tx.wait(3) // Wait for 3 confirmations
}

async function main() {
    const args = parseArgs()

    if (!args.owner) {
        console.error('Error: --owner is required')
        printHelp()
        process.exit(1)
    }

    const owner = ethers.getAddress(args.owner)
    const agentName = args.name ? args.name.toUpperCase() : null
    const provider = new ethers.JsonRpcProvider(args.rpc, CHAIN_ID, { staticNetwork: true })

    console.log('\n========================================')
    console.log('     Claw Wallet Initialization')
    console.log('========================================\n')
    console.log(`Owner: ${owner}`)
    console.log(`Chain: Base (${CHAIN_ID})`)

    // Ensure config directory exists
    if (!fs.existsSync(args.configDir)) {
        fs.mkdirSync(args.configDir, { recursive: true })
    }

    // Load state for resume capability
    let state = loadState(args.configDir)

    // Step 1: Agent keypair
    console.log('\n--- Step 1: Agent Keypair ---')
    const agentPkPath = path.join(args.configDir, 'agent.pk')
    let agentWallet

    if (fs.existsSync(agentPkPath)) {
        let pk = fs.readFileSync(agentPkPath, 'utf8').trim()
        if (!pk.startsWith('0x')) pk = '0x' + pk
        agentWallet = new ethers.Wallet(pk, provider)
        console.log(`Agent exists: ${agentWallet.address}`)
    } else {
        agentWallet = ethers.Wallet.createRandom().connect(provider)
        fs.writeFileSync(agentPkPath, agentWallet.privateKey.slice(2), { mode: 0o600 })
        console.log(`Agent created: ${agentWallet.address}`)
        state = { step: STEPS.AGENT_CREATED }
        saveState(args.configDir, state)
    }

    const agentBalance = await provider.getBalance(agentWallet.address)
    console.log(`Balance: ${ethers.formatEther(agentBalance)} ETH`)

    if (agentBalance < ethers.parseEther('0.000005')) {
        console.log(`\n⚠️  Agent needs gas for deployment.`)
        console.log(`   Send at least 0.00002 ETH to: ${agentWallet.address}`)
        console.log(`   Then run this script again.\n`)
        process.exit(0)
    }

    // Generate deterministic salt from owner + agent
    const saltNonce = BigInt(ethers.keccak256(
        ethers.solidityPacked(['address', 'address'], [owner, agentWallet.address])
    ))

    let safeAddress = state.safe
    let rolesAddress = state.roles

    // Step 2: Deploy Safe (with agent as initial owner)
    if (!safeAddress || state.step === STEPS.AGENT_CREATED) {
        console.log('\n--- Step 2: Deploy Safe ---')
        console.log('   Deploying Safe with agent as initial owner...')

        const safeFactory = new ethers.Contract(CONTRACTS.SafeProxyFactory, SAFE_FACTORY_ABI, agentWallet)
        const safeInterface = new ethers.Interface(SAFE_ABI)

        // Agent is initial owner (will transfer to human later)
        const safeInitializer = safeInterface.encodeFunctionData('setup', [
            [agentWallet.address],  // Agent as initial owner
            1,
            ethers.ZeroAddress,
            '0x',
            CONTRACTS.CompatibilityFallbackHandler,
            ethers.ZeroAddress,
            0,
            ethers.ZeroAddress,
        ])

        const safeTx = await safeFactory.createProxyWithNonce(
            CONTRACTS.SafeSingletonL2,
            safeInitializer,
            saltNonce
        )
        console.log(`   Transaction: ${safeTx.hash}`)
        console.log('   Waiting for 3 confirmations...')
        const safeReceipt = await safeTx.wait(3)

        const proxyCreationTopic = ethers.id('ProxyCreation(address,address)')
        const safeCreationEvent = safeReceipt.logs.find(
            log => log.topics[0] === proxyCreationTopic
        )
        // Address can be in topics[1] or in data depending on event definition
        if (safeCreationEvent.topics.length > 1) {
            safeAddress = ethers.getAddress('0x' + safeCreationEvent.topics[1].slice(-40))
        } else {
            const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['address', 'address'], safeCreationEvent.data)
            safeAddress = decoded[0]
        }
        console.log(`   Safe deployed: ${safeAddress}`)

        state = {
            ...state,
            step: STEPS.SAFE_DEPLOYED,
            safe: safeAddress,
            safeTxHash: safeReceipt.hash,
            safeBlockNumber: safeReceipt.blockNumber,
            safeBlockTime: new Date().toISOString(),
        }
        saveState(args.configDir, state)
    } else {
        console.log(`\n--- Step 2: Safe ---`)
        console.log(`   Already deployed: ${safeAddress}`)
    }

    const safe = new ethers.Contract(safeAddress, SAFE_ABI, agentWallet)

    // Step 3: Deploy Roles
    if (!rolesAddress || state.step === STEPS.SAFE_DEPLOYED) {
        console.log('\n--- Step 3: Deploy Zodiac Roles ---')
        const moduleFactory = new ethers.Contract(CONTRACTS.ModuleProxyFactory, MODULE_FACTORY_ABI, agentWallet)
        const rolesInterface = new ethers.Interface(ROLES_ABI)

        const rolesInitializer = rolesInterface.encodeFunctionData('setUp', [
            ethers.AbiCoder.defaultAbiCoder().encode(
                ['address', 'address', 'address'],
                [safeAddress, safeAddress, safeAddress]
            )
        ])

        const rolesTx = await moduleFactory.deployModule(
            CONTRACTS.RolesSingleton,
            rolesInitializer,
            saltNonce + 1n
        )
        console.log(`   Transaction: ${rolesTx.hash}`)
        console.log('   Waiting for 3 confirmations...')
        const rolesReceipt = await rolesTx.wait(3)

        const rolesEvent = rolesReceipt.logs.find(
            log => log.topics[0] === ethers.id('ModuleProxyCreation(address,address)')
        )
        rolesAddress = ethers.getAddress('0x' + rolesEvent.topics[1].slice(26))
        console.log(`   Roles deployed: ${rolesAddress}`)

        state = { ...state, step: STEPS.ROLES_DEPLOYED, roles: rolesAddress }
        saveState(args.configDir, state)
    } else {
        console.log(`\n--- Step 3: Zodiac Roles ---`)
        console.log(`   Already deployed: ${rolesAddress}`)
    }

    // Step 4: Configure Roles via MultiSend (no ownership transfer yet)
    const isModuleEnabled = await safe.isModuleEnabled(rolesAddress)

    if (!isModuleEnabled || state.step === STEPS.ROLES_DEPLOYED) {
        console.log('\n--- Step 4: Configure Roles ---')
        console.log('   Batching configuration via MultiSend...')

        const safeInterface = new ethers.Interface(SAFE_ABI)
        const rolesInterface = new ethers.Interface(ROLES_ABI)

        const transactions = []

        // Enable module on Safe (if not already)
        if (!isModuleEnabled) {
            console.log('   - enableModule(roles)')
            transactions.push({
                to: safeAddress,
                data: safeInterface.encodeFunctionData('enableModule', [rolesAddress]),
            })
        }

        // Scope and allow Aerodrome Universal Router
        console.log('   - scopeTarget(AeroUniversalRouter)')
        transactions.push({
            to: rolesAddress,
            data: rolesInterface.encodeFunctionData('scopeTarget', [ROLE_KEY, CONTRACTS.AeroUniversalRouter]),
        })

        console.log('   - allowTarget(AeroUniversalRouter, Send)')
        transactions.push({
            to: rolesAddress,
            data: rolesInterface.encodeFunctionData('allowTarget', [ROLE_KEY, CONTRACTS.AeroUniversalRouter, ExecutionOptions.Send]),
        })

        // Scope and allow ZodiacHelpers (with DelegateCall)
        console.log('   - scopeTarget(ZodiacHelpers)')
        transactions.push({
            to: rolesAddress,
            data: rolesInterface.encodeFunctionData('scopeTarget', [ROLE_KEY, CONTRACTS.ZodiacHelpers]),
        })

        console.log('   - allowTarget(ZodiacHelpers, Both)')
        transactions.push({
            to: rolesAddress,
            data: rolesInterface.encodeFunctionData('allowTarget', [ROLE_KEY, CONTRACTS.ZodiacHelpers, ExecutionOptions.Both]),
        })

        // Assign role to agent
        console.log('   - assignRoles(agent, WalletSwapper)')
        transactions.push({
            to: rolesAddress,
            data: rolesInterface.encodeFunctionData('assignRoles', [agentWallet.address, [ROLE_KEY], [true]]),
        })

        // Set default role for agent
        console.log('   - setDefaultRole(agent, WalletSwapper)')
        transactions.push({
            to: rolesAddress,
            data: rolesInterface.encodeFunctionData('setDefaultRole', [agentWallet.address, ROLE_KEY]),
        })

        console.log(`\n   Executing ${transactions.length} operations in single MultiSend...`)
        const multiSendData = encodeMultiSend(transactions)

        const receipt = await execSafeTransaction(safe, CONTRACTS.MultiSend, 0n, multiSendData, 1, agentWallet)
        console.log(`   Transaction: ${receipt.hash}`)
        console.log('   Confirmed (3 blocks)')
        console.log('   Configuration complete!')

        state = { ...state, step: STEPS.CONFIGURED }
        saveState(args.configDir, state)
    } else {
        console.log('\n--- Step 4: Configuration ---')
        console.log('   Already configured!')
    }

    // Step 5: Register with backend
    let registration = state.registration
    if (!registration) {
        console.log('\n--- Step 5: Register with Backend ---')
        const existingAgent = await checkExistingAgent(agentWallet.address.toLowerCase())
        if (existingAgent && !existingAgent.error && !existingAgent.needsRegistration) {
            console.log('   Already registered!')
            registration = existingAgent
        } else {
            try {
                const jwt = await getRegistrationChallenge(agentWallet.address.toLowerCase())
                const block = await provider.getBlock('latest')
                registration = await registerAgent(agentWallet, jwt, {
                    owner,
                    safe: safeAddress,
                    roles: rolesAddress,
                    approvalHelper: CONTRACTS.ZodiacHelpers,
                    roleKey: ROLE_KEY,
                    chainId: CHAIN_ID,
                    evt_tx_hash: state.safeTxHash || '',
                    evt_block_number: state.safeBlockNumber?.toString() || block.number.toString(),
                    evt_block_time: state.safeBlockTime || new Date(Number(block.timestamp) * 1000).toISOString(),
                })
                console.log(`   Registered! Agent ID: ${registration.id || 'N/A'}`)
                if (registration.referralCode) {
                    console.log(`   Referral code: ${registration.referralCode}`)
                }
                state = { ...state, registration }
                saveState(args.configDir, state)
            } catch (error) {
                console.log(`   Warning: Registration failed: ${error.message}`)
                console.log('   Continuing setup...')
            }
        }
    } else {
        console.log('\n--- Step 5: Backend ---')
        console.log('   Already registered!')
    }

    // Step 6: Register CNS name (via Safe — backend signs with account=safe)
    let cnsTokenId = state.cnsTokenId
    if (!agentName) {
        console.log('\n--- Step 6: CNS Name ---')
        console.log('   Skipped (--name not provided)')
    } else if (!cnsTokenId) {
        console.log('\n--- Step 6: Register CNS Name ---')
        const cns = new ethers.Contract(CONTRACTS.CNS, CNS_ABI, provider)

        // Check if Safe already owns a CNS token (from interrupted previous run)
        const safeCnsBalance = await cns.balanceOf(safeAddress)
        if (safeCnsBalance > 0n) {
            const existingTokenId = await cns.tokenOfOwnerByIndex(safeAddress, 0)
            const existingName = await cns.tokenName(existingTokenId)
            cnsTokenId = Number(existingTokenId)
            console.log(`   Safe already owns CNS name: ${existingName} (Token ID: ${cnsTokenId})`)
            state = { ...state, cnsTokenId }
            saveState(args.configDir, state)
        } else {
            // Check if name is available on-chain
            const available = await cns.isNameAvailable(agentName)
            if (!available) {
                console.error(`   Error: Name "${agentName}" is already taken.`)
                process.exit(1)
            }

            const fee = await cns.fee()
            console.log(`   Name: ${agentName}`)
            console.log(`   Fee: ${ethers.formatEther(fee)} ETH`)

            try {
                console.log('   Authenticating with backend...')
                const cookies = await authenticateAgent(agentWallet)

                console.log('   Requesting CNS signature...')
                const sigResponse = await getCnsSignature(cookies, agentName)

                // Backend wraps in { signature: { signature, data, expiresAt } }
                const sig = sigResponse.signature || sigResponse
                if (!sig.signature || !sig.data || sig.expiresAt === undefined) {
                    console.log('   Backend response:', JSON.stringify(sigResponse, null, 2))
                    throw new Error('Backend returned incomplete signature data')
                }

                console.log(`   Signature received (expires: ${sig.expiresAt})`)
                console.log('   Registering on-chain via Safe...')

                // Encode register() call — must go through Safe since backend
                // signs with account=safe address (msg.sender must be Safe)
                const cnsInterface = new ethers.Interface(CNS_ABI)
                const registerData = cnsInterface.encodeFunctionData('register', [
                    [sig.signature, sig.data, BigInt(sig.expiresAt)],
                ])

                const receipt = await execSafeTransaction(
                    safe, CONTRACTS.CNS, fee, registerData, 0, agentWallet
                )
                console.log(`   Transaction: ${receipt.hash}`)

                const nameRegTopic = ethers.id('NameRegistered(address,uint256,string)')
                const nameRegEvent = receipt.logs.find(log => log.topics[0] === nameRegTopic)
                if (nameRegEvent) {
                    cnsTokenId = Number(nameRegEvent.topics[2])
                }

                console.log(`   CNS registered! Name: ${agentName}, Token ID: ${cnsTokenId || 'N/A'}`)
                state = { ...state, cnsTokenId }
                saveState(args.configDir, state)
            } catch (error) {
                console.log(`   Warning: CNS registration failed: ${error.message}`)
                console.log('   Continuing setup...')
            }
        }
    } else {
        console.log('\n--- Step 6: CNS Name ---')
        console.log(`   Already registered! Token ID: ${cnsTokenId}`)
    }

    // Step 7: Transfer ownership (add human owner, remove agent)
    const currentOwners = await safe.getOwners()
    const ownerIsHuman = currentOwners.some(o => o.toLowerCase() === owner.toLowerCase())
    const agentIsOwner = currentOwners.some(o => o.toLowerCase() === agentWallet.address.toLowerCase())

    if (agentIsOwner) {
        console.log('\n--- Step 7: Transfer Ownership ---')
        const safeInterface = new ethers.Interface(SAFE_ABI)
        const transactions = []

        if (!ownerIsHuman) {
            console.log(`   - addOwnerWithThreshold(${owner.slice(0, 10)}...)`)
            transactions.push({
                to: safeAddress,
                data: safeInterface.encodeFunctionData('addOwnerWithThreshold', [owner, 1]),
            })
        }

        console.log(`   - removeOwner(${agentWallet.address.slice(0, 10)}...)`)
        // prevOwner is the owner that points to the agent in the linked list
        // After addOwner, the new owner is at the front, so prevOwner = owner
        const prevOwner = ownerIsHuman ? owner : owner
        transactions.push({
            to: safeAddress,
            data: safeInterface.encodeFunctionData('removeOwner', [prevOwner, agentWallet.address, 1]),
        })

        const multiSendData = encodeMultiSend(transactions)
        const receipt = await execSafeTransaction(safe, CONTRACTS.MultiSend, 0n, multiSendData, 1, agentWallet)
        console.log(`   Transaction: ${receipt.hash}`)
        console.log('   Confirmed (3 blocks)')
        console.log('   Ownership transferred!')
    } else {
        console.log('\n--- Step 7: Ownership ---')
        console.log('   Already transferred!')
    }

    // Verify ownership
    const finalOwners = await safe.getOwners()
    if (finalOwners.length !== 1 || finalOwners[0].toLowerCase() !== owner.toLowerCase()) {
        console.error(`\n   Warning: Unexpected owners: ${finalOwners.join(', ')}`)
    }

    // Step 8: Register on ERC-8004 Identity Registry
    // Agent mints the identity NFT, then transfers it to the Safe
    let erc8004AgentId = state.erc8004AgentId
    if (state.step !== STEPS.ERC8004_REGISTERED && state.step !== STEPS.COMPLETE) {
        console.log('\n--- Step 8: Register on ERC-8004 ---')

        const identityRegistry = new ethers.Contract(
            CONTRACTS.IdentityRegistry,
            IDENTITY_REGISTRY_ABI,
            agentWallet
        )

        // Phase 1: Mint identity NFT (if not already done)
        if (!erc8004AgentId) {
            const agentRegistration = {
                type: 'https://eips.ethereum.org/EIPS/eip-8004#registration-v1',
                name: 'Clawlett',
                description: `Autonomous MEV-protected token swap agent on Base. Safe: ${safeAddress}`,
                image: '',
                services: [
                    {
                        name: 'web',
                        endpoint: API_BASE_URL,
                    },
                ],
                x402Support: false,
                active: true,
                registrations: [],
            }

            const agentURI = 'data:application/json;base64,' + Buffer.from(JSON.stringify(agentRegistration)).toString('base64')

            console.log('   Registering agent identity on-chain...')
            const tx = await identityRegistry.register(agentURI)
            console.log(`   Transaction: ${tx.hash}`)
            console.log('   Waiting for 3 confirmations...')
            const receipt = await tx.wait(3)

            // Parse Registered event to get agentId
            const registeredTopic = ethers.id('Registered(uint256,string,address)')
            const registeredEvent = receipt.logs.find(log => log.topics[0] === registeredTopic)
            if (registeredEvent) {
                erc8004AgentId = Number(registeredEvent.topics[1])
            } else {
                // Fallback: parse from ERC-721 Transfer event
                const transferTopic = ethers.id('Transfer(address,address,uint256)')
                const transferEvent = receipt.logs.find(log => log.topics[0] === transferTopic)
                if (transferEvent) {
                    erc8004AgentId = Number(transferEvent.topics[3])
                }
            }

            if (!erc8004AgentId) {
                console.log('   Warning: Could not parse agentId from events, skipping.')
                state = { ...state, step: STEPS.ERC8004_REGISTERED }
                saveState(args.configDir, state)
            } else {
                console.log(`   Minted identity NFT #${erc8004AgentId}`)
                // Save immediately so we can resume transfer if interrupted
                state = { ...state, erc8004AgentId }
                saveState(args.configDir, state)
            }
        }

        // Phase 2: Transfer NFT to Safe (if agent still owns it)
        if (erc8004AgentId) {
            const nftOwner = await identityRegistry.ownerOf(erc8004AgentId)
            if (nftOwner.toLowerCase() === agentWallet.address.toLowerCase()) {
                console.log(`   Transferring identity NFT to Safe...`)
                const tx = await identityRegistry.transferFrom(agentWallet.address, safeAddress, erc8004AgentId)
                console.log(`   Transaction: ${tx.hash}`)
                console.log('   Waiting for 3 confirmations...')
                await tx.wait(3)
                console.log(`   Safe now owns ERC-8004 ID: ${erc8004AgentId}`)
            } else if (nftOwner.toLowerCase() === safeAddress.toLowerCase()) {
                console.log(`   Safe already owns ERC-8004 ID: ${erc8004AgentId}`)
            } else {
                console.log(`   Warning: NFT #${erc8004AgentId} owned by unexpected address: ${nftOwner}`)
            }
        }

        state = { ...state, step: STEPS.ERC8004_REGISTERED, erc8004AgentId }
        saveState(args.configDir, state)
    } else {
        console.log('\n--- Step 8: ERC-8004 Identity ---')
        if (erc8004AgentId) {
            console.log(`   Already registered! ID: ${erc8004AgentId}`)
        } else {
            console.log('   Already registered!')
        }
    }

    // Save final config
    const config = {
        chainId: CHAIN_ID,
        owner,
        agent: agentWallet.address,
        safe: safeAddress,
        roles: rolesAddress,
        roleKey: ROLE_KEY,
        name: agentName || null,
        cnsTokenId: cnsTokenId || null,
        erc8004AgentId: erc8004AgentId || null,
        contracts: CONTRACTS,
        createdAt: new Date().toISOString(),
        cookies: registration?.cookies,
    }
    saveConfig(args.configDir, config)
    clearState(args.configDir)

    // Summary
    console.log('\n========================================')
    console.log('         Setup Complete!')
    console.log('========================================')
    console.log(`\nSafe:   ${safeAddress}`)
    console.log(`Roles:  ${rolesAddress}`)
    console.log(`Agent:  ${agentWallet.address}`)
    console.log(`Owner:  ${owner}`)
    console.log(`\nFund your Safe to start trading:`)
    console.log(`   ${safeAddress}`)
    console.log(`\nUsage:`)
    console.log(`   node balance.js --all`)
    console.log(`   node swap.js --from ETH --to USDC --amount 0.1`)
    console.log('')
}

main().catch(error => {
    console.error(`\n❌ Error: ${error.message}`)
    process.exit(1)
})


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)