Skill: justjkk/elsa-x402-api (version 1.0.0)

--- name: openclaw-elsa-x402 description: DeFi tools for portfolio analysis, token search, and swap execution via Elsa API with x402 micropayments metadata: {"openclaw":{"requires":{"env":["PAYMENT_PRIVATE_KEY"]},"primaryEnv":"PAYMENT_PRIVATE_KEY"}} --- # Elsa x402 DeFi Tools This skill provides access to the Elsa DeFi API for portfolio management, token analysis, and swap execution using x402 micropayments. ## Standard Operating Procedure ### Budget Controls - Maximum $0.05 per API call (configurable) - Maximum $2.00 per day (configurable) - Maximum 30 calls per minute rate limit - Daily budget resets at midnight in configured timezone (default: UTC) ### Recommended Swap Flow (OpenClaw) When executing swaps, follow this 4-step flow: ``` ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ 1. Get Quote │ ──▶ │ 2. Dry Run │ ──▶ │ 3. Confirm │ ──▶ │ 4. Execute Pipeline │ │ elsa_get_swap_quote │ │ elsa_execute_swap_ │ │ [User says │ │ elsa_pipeline_run_ │ │ │ │ dry_run │ │ "yes"] │ │ and_wait │ └─────────────────────┘ └─────────────────────┘ └─────────────┘ └─────────────────────┘ ``` **Step 1: Get Quote** - Show user what they'll receive ```bash npx tsx scripts/index.ts elsa_get_swap_quote '{ "from_chain": "base", "from_token": [REDACTED_SECRET], "from_amount": "10", "to_chain": "base", "to_token": [REDACTED_SECRET], "wallet_address": "0x...", "slippage": 0.5 }' ``` **Step 2: Dry Run** - Create pipeline, get `pipeline_id` ```bash npx tsx scripts/index.ts elsa_execute_swap_dry_run '{...same params...}' # Returns: { "pipeline_id": "abc-123", "confirmation_token": "..." } ``` **Step 3: User Confirmation** - Present results and wait for explicit "yes" **Step 4: Execute Pipeline** - Sign and broadcast transactions ```bash ELSA_ENABLE_EXECUTION_TOOLS=true npx tsx scripts/index.ts elsa_pipeline_run_and_wait '{ "pipeline_id": "abc-123", "timeout_seconds": 180, "poll_interval_seconds": 3, "mode": "local_signer" }' # Automatically: signs approve tx → submits → signs swap tx → submits → returns tx hashes ``` ### Required Environment Variables ```bash # For x402 API payments PAYMENT_PRIVATE_KEY=0x... # For signing swap transactions (optional - falls back to PAYMENT_PRIVATE_KEY) TRADE_PRIVATE_KEY=0x... # Enable execution tools ELSA_ENABLE_EXECUTION_TOOLS=true ``` ### Critical Rules - **NEVER** execute swaps without showing the user the quote first - **NEVER** call execution tools in a loop - **NEVER** proceed if budget limits are exceeded - **ALWAYS** check `elsa_budget_status` if unsure about remaining budget - **ALWAYS** use dry-run mode first for any swap operation ## Tool Catalog ### Read-Only Tools (Always Available) #### elsa_search_token Search for tokens across supported blockchains. ```bash npx tsx scripts/index.ts elsa_search_token '{"query": "USDC", "limit": 5}' ``` **Input:** - `query` (string, required): Token name, symbol, or address - `limit` (number, optional): Max results (default: 10) --- #### elsa_get_token_price Get current price for a token. ```bash npx tsx scripts/index.ts elsa_get_token_price '{"token_address": [REDACTED_SECRET], "chain": "base"}' ``` **Input:** - `token_address` (string, required): Token contract address - `chain` (string, optional): Chain name (default: "base") --- #### elsa_get_balances Get token balances for a wallet. ```bash npx tsx scripts/index.ts elsa_get_balances '{"wallet_address": "0x..."}' ``` **Input:** - `wallet_address` (string, required): Wallet address to query --- #### elsa_get_portfolio Get comprehensive portfolio analysis. ```bash npx tsx scripts/index.ts elsa_get_portfolio '{"wallet_address": "0x..."}' ``` **Input:** - `wallet_address` (string, required): Wallet address to analyze --- #### elsa_analyze_wallet Get wallet behavior and risk assessment. ```bash npx tsx scripts/index.ts elsa_analyze_wallet '{"wallet_address": "0x..."}' ``` **Input:** - `wallet_address` (string, required): Wallet address to analyze --- #### elsa_get_swap_quote Get a swap quote with optimal routing. ```bash npx tsx scripts/index.ts elsa_get_swap_quote '{ "from_chain": "base", "from_token": [REDACTED_SECRET], "from_amount": "100", "to_chain": "base", "to_token": [REDACTED_SECRET], "wallet_address": "0x...", "slippage": 0.5 }' ``` **Input:** - `from_chain` (string, required): Source chain - `from_token` (string, required): Source token address - `from_amount` (string, required): Amount to swap - `to_chain` (string, required): Destination chain - `to_token` (string, required): Destination token address - `wallet_address` (string, required): Wallet address - `slippage` (number, required): Slippage tolerance (0-50) --- #### elsa_execute_swap_dry_run Simulate a swap without execution. Returns a confirmation token for subsequent confirmed execution. ```bash npx tsx scripts/index.ts elsa_execute_swap_dry_run '{ "from_chain": "base", "from_token": [REDACTED_SECRET], "from_amount": "100", "to_chain": "base", "to_token": [REDACTED_SECRET], "wallet_address": "0x...", "slippage": 0.5 }' ``` **Output includes:** - Swap simulation results - `confirmation_token` (if ELSA_REQUIRE_CONFIRMATION_TOKEN=true) - Token expires after ELSA_CONFIRMATION_TTL_SECONDS --- #### elsa_budget_status Check current budget usage and remaining limits. ```bash npx tsx scripts/index.ts elsa_budget_status '{}' ``` **Output:** - `spent_today_usd`: Amount spent today - `remaining_today_usd`: Remaining daily budget - `calls_last_minute`: Recent call count - `last_calls`: Recent call details --- #### elsa_get_limit_orders Get all limit orders for a wallet. ```bash npx tsx scripts/index.ts elsa_get_limit_orders '{"wallet_address": "0x...", "chain": "base"}' ``` **Input:** - `wallet_address` (string, required): Wallet address to query - `chain` (string, optional): Chain name (default: "base") **Output:** - `orders`: Array of limit orders with status, prices, and expiry --- #### elsa_get_perp_positions Get perpetual positions for a wallet. ```bash npx tsx scripts/index.ts elsa_get_perp_positions '{"wallet_address": "0x..."}' ``` **Input:** - `wallet_address` (string, required): Wallet address to query **Output:** - `positions`: Array of open perp positions with PnL, leverage, and liquidation prices - `total_unrealized_pnl`: Sum of unrealized profits/losses - `total_margin`: Total margin used --- #### elsa_get_transaction_history Get transaction history for a wallet. ```bash npx tsx scripts/index.ts elsa_get_transaction_history '{"wallet_address": "0x...", "chain": "base", "limit": 20}' ``` **Input:** - `wallet_address` (string, required): Wallet address to query - `chain` (string, optional): Filter by chain - `limit` (number, optional): Max results (default: 20, max: 100) **Output:** - `transactions`: Array of transactions with details, token transfers, and status --- ### Execution Tools (Requires ELSA_ENABLE_EXECUTION_TOOLS=true) #### elsa_execute_swap_confirmed Execute a confirmed swap. Requires prior dry-run and confirmation token. ```bash npx tsx scripts/index.ts elsa_execute_swap_confirmed '{ "from_chain": "base", "from_token": [REDACTED_SECRET], "from_amount": "100", "to_chain": "base", "to_token": [REDACTED_SECRET], "wallet_address": "0x...", "slippage": 0.5, "confirmation_token": "abc123..." }' ``` --- #### elsa_pipeline_get_status Check status of a pipeline/transaction. ```bash npx tsx scripts/index.ts elsa_pipeline_get_status '{"pipeline_id": "pip_123456789"}' ``` --- #### elsa_pipeline_submit_tx_hash Submit a signed transaction hash to the pipeline. ```bash npx tsx scripts/index.ts elsa_pipeline_submit_tx_hash '{"task_id": "task_123", "tx_hash": "0x..."}' ``` --- #### elsa_pipeline_run_and_wait Orchestrate full pipeline execution with automatic signing and submission. ```bash npx tsx scripts/index.ts elsa_pipeline_run_and_wait '{ "pipeline_id": "pip_123456789", "timeout_seconds": 120, "poll_interval_seconds": 2, "mode": "local_signer" }' ``` **Modes:** - `local_signer`: Signs and broadcasts transactions using TRADE_PRIVATE_KEY - `external_signer`: Returns unsigned tx_data for external signing --- #### elsa_create_limit_order Create a limit order to execute when price reaches target. ```bash ELSA_ENABLE_EXECUTION_TOOLS=true npx tsx scripts/index.ts elsa_create_limit_order '{ "wallet_address": "0x...", "chain": "base", "from_token": [REDACTED_SECRET], "to_token": [REDACTED_SECRET], "from_amount": "100", "limit_price": "0.0004", "expiry_hours": 24 }' ``` **Input:** - `wallet_address` (string, required): Wallet address - `chain` (string, required): Chain name - `from_token` (string, required): Token to sell - `to_token` (string, required): Token to buy - `from_amount` (string, required): Amount to sell - `limit_price` (string, required): Target price to trigger execution - `expiry_hours` (number, optional): Hours until order expires (default: 24, max: 720) --- #### elsa_cancel_limit_order Cancel an existing limit order. ```bash ELSA_ENABLE_EXECUTION_TOOLS=true npx tsx scripts/index.ts elsa_cancel_limit_order '{ "wallet_address": "0x...", "order_id": "order_123", "chain": "base" }' ``` **Input:** - `wallet_address` (string, required): Wallet address - `order_id` (string, required): Order ID to cancel - `chain` (string, optional): Chain name (default: "base") --- #### elsa_open_perp_position Open a perpetual futures position. ```bash ELSA_ENABLE_EXECUTION_TOOLS=true npx tsx scripts/index.ts elsa_open_perp_position '{ "wallet_address": "0x...", "market": "ETH-USD", "side": "long", "size_usd": "100", "leverage": 5, "take_profit": "4000", "stop_loss": "2800" }' ``` **Input:** - `wallet_address` (string, required): Wallet address - `market` (string, required): Market pair (e.g., "ETH-USD", "BTC-USD") - `side` (enum, required): "long" or "short" - `size_usd` (string, required): Position size in USD - `leverage` (number, optional): Leverage multiplier (default: 1, max: 100) - `take_profit` (string, optional): Take profit price - `stop_loss` (string, optional): Stop loss price --- #### elsa_close_perp_position Close an open perpetual position. ```bash ELSA_ENABLE_EXECUTION_TOOLS=true npx tsx scripts/index.ts elsa_close_perp_position '{ "wallet_address": "0x...", "position_id": "pos_123", "close_percentage": 100 }' ``` **Input:** - `wallet_address` (string, required): Wallet address - `position_id` (string, required): Position ID to close - `close_percentage` (number, optional): Percentage to close (default: 100, range: 1-100) ## Supported Chains - base (default) - ethereum - arbitrum - optimism - polygon - bsc - avalanche - zksync ## API Costs | Tool | Estimated Cost | |------|----------------| | elsa_search_token | $0.002 | | elsa_get_token_price | $0.002 | | elsa_get_balances | $0.002 | | elsa_get_portfolio | $0.002 | | elsa_analyze_wallet | $0.005 | | elsa_get_swap_quote | $0.002 | | elsa_execute_swap_dry_run | $0.005 | | elsa_get_limit_orders | $0.002 | | elsa_get_perp_positions | $0.002 | | elsa_get_transaction_history | $0.002 | | elsa_create_limit_order | $0.05 | | elsa_cancel_limit_order | $0.01 | | elsa_open_perp_position | $0.05 | | elsa_close_perp_position | $0.05 | | elsa_execute_swap_confirmed | $0.05 | ## Coming Soon - **Polymarket APIs** - Prediction market trading and data

path: ONE-PAGER.md
content: # OpenClaw Elsa x402

**AI-powered DeFi trading with micropayments**

---

## What is it?

A skill-pack that lets OpenClaw AI agents interact with DeFi protocols through the Elsa API, paying per-request with USDC micropayments via the x402 protocol.

## Key Features

| Feature | Description |
|---------|-------------|
| **Pay-per-use** | No subscriptions. Pay only for API calls you make (~$0.01-0.05 per call) |
| **Non-custodial** | Private keys never leave your machine. All signing is local |
| **Budget controls** | Per-call and daily limits prevent runaway spending |
| **Multi-chain** | Base, Ethereum, Arbitrum, Optimism, Polygon, BSC, Avalanche, zkSync |
| **Safe by default** | Execution tools disabled until explicitly enabled |

## Tools

```
Read-Only (Always On)          Execution (Opt-In)
─────────────────────          ──────────────────
elsa_search_token              elsa_execute_swap_confirmed
elsa_get_token_price           elsa_pipeline_get_status
elsa_get_balances              elsa_pipeline_submit_tx_hash
elsa_get_portfolio             elsa_pipeline_run_and_wait
elsa_analyze_wallet
elsa_get_swap_quote
elsa_execute_swap_dry_run
elsa_budget_status
```

## How Swaps Work

```
Quote → Dry Run → User Confirms → Execute
  │        │           │            │
  │        │           │            └─ Signs & broadcasts txs
  │        │           └─ Human in the loop
  │        └─ Creates pipeline, no onchain action
  └─ Shows expected output
```

## Quick Start

```bash
# 1. Clone & install
git clone https://github.com/HeyElsa/elsa-openclaw.git
cd elsa-openclaw && npm install

# 2. Set payment wallet
export PAYMENT_PRIVATE_KEY=0x...

# 3. Test
npx tsx scripts/index.ts elsa_search_token '{"query": "USDC"}'
```

## Use Cases

- **Portfolio tracking** - Monitor wallet balances across chains
- **Token research** - Search and price tokens before trading
- **Automated swaps** - Execute trades with AI agent oversight
- **Risk analysis** - Analyze wallet behavior and risk profiles

## Pricing

| Endpoint | Cost |
|----------|------|
| Search/Price | ~$0.01 |
| Portfolio/Balances | ~$0.02 |
| Swap Quote | ~$0.03 |
| Execute Swap | ~$0.05 |

*Actual costs determined by x402 headers at request time*

## Links

- **GitHub**: [github.com/HeyElsa/elsa-openclaw](https://github.com/HeyElsa/elsa-openclaw)
- **Elsa API**: [x402.heyelsa.ai](https://x402.heyelsa.ai)
- **x402 Protocol**: [x402.org](https://x402.org)

---

*Built with x402 micropayments on Base*


path: README.md
content: <p align="center">
  <img src="assets/logo.svg" alt="Elsa + OpenClaw" width="400">
</p>

# elsa-openclaw

OpenClaw skill-pack for Elsa x402 DeFi API integration with micropayments.

This plugin enables OpenClaw agents to interact with the [Elsa DeFi API](https://x402.heyelsa.ai) using the x402 payment protocol for USDC micropayments on Base.

## Security Posture

- **Non-custodial**: Private keys never leave your machine
- **Local signing**: All transactions are signed locally using viem
- **Budget controls**: Per-call and daily USD limits enforced before any paid API call
- **Execution disabled by default**: Onchain execution tools require explicit opt-in
- **Confirmation tokens**: Dry-run required before confirmed execution (optional but recommended)
- **Separate wallets recommended**: Use different keys for API payments vs. trade execution

## Features

### Read-Only Tools (Always Available)
- `elsa_search_token` - Search tokens across blockchains
- `elsa_get_token_price` - Get real-time token pricing
- `elsa_get_balances` - Get wallet token balances
- `elsa_get_portfolio` - Comprehensive portfolio analysis
- `elsa_analyze_wallet` - Wallet behavior and risk assessment
- `elsa_get_swap_quote` - Get swap quotes and routing
- `elsa_execute_swap_dry_run` - Simulate swap execution (no onchain action)
- `elsa_budget_status` - Check current budget usage

### Execution Tools (Opt-In)
When `ELSA_ENABLE_EXECUTION_TOOLS=true`:
- `elsa_execute_swap_confirmed` - Execute swap with confirmation token
- `elsa_pipeline_get_status` - Check pipeline/transaction status
- `elsa_pipeline_submit_tx_hash` - Submit signed transaction hash
- `elsa_pipeline_run_and_wait` - Orchestrate full pipeline execution

## Installation

### 1. Clone and install

```bash
git clone https://github.com/HeyElsa/elsa-openclaw.git
cd elsa-openclaw
npm install
```

### 2. Configure OpenClaw

Edit `~/.openclaw/openclaw.json`:

```json
{
  "skills": {
    "load": {
      "extraDirs": ["/path/to/elsa-openclaw"]
    },
    "entries": {
      "openclaw-elsa-x402": {
        "env": {
          "PAYMENT_PRIVATE_KEY": "0x_YOUR_WALLET_PRIVATE_KEY"
        }
      }
    }
  }
}
```

**Minimal config** - only `PAYMENT_PRIVATE_KEY` is required. All other settings have sensible defaults.

**With execution enabled** (for swaps):
```json
{
  "skills": {
    "load": {
      "extraDirs": ["/path/to/elsa-openclaw"]
    },
    "entries": {
      "openclaw-elsa-x402": {
        "env": {
          "PAYMENT_PRIVATE_KEY": "0x...",
          "TRADE_PRIVATE_KEY": "0x...",
          "ELSA_ENABLE_EXECUTION_TOOLS": "true"
        }
      }
    }
  }
}
```

### 3. Fund your payment wallet

The payment wallet needs USDC on Base to pay for API calls (~$0.01-0.05 per call).

### 4. Restart OpenClaw

The skill will load automatically. Verify with `/skills` in chat.

## Usage in OpenClaw

Once configured, just talk naturally:

| You say | Tool used |
|---------|-----------|
| "What's the price of WETH?" | `elsa_get_token_price` |
| "Search for PEPE token" | `elsa_search_token` |
| "Show portfolio for 0xd8dA..." | `elsa_get_portfolio` |
| "Get a quote to swap 10 USDC to WETH" | `elsa_get_swap_quote` |
| "How much have I spent on Elsa API?" | `elsa_budget_status` |
| "Swap 10 USDC to WETH on Base" | Full swap flow (with execution enabled) |

## Smoke Test

After installation, verify the setup:

```bash
# Search for a token
npx tsx scripts/index.ts elsa_search_token '{"query": "USDC", "limit": 3}'

# Get portfolio for a sample address
npx tsx scripts/index.ts elsa_get_portfolio '{"wallet_address": [REDACTED_SECRET]}'

# Check budget status
npx tsx scripts/index.ts elsa_budget_status '{}'
```

> **Field Mapping Note**: This skill uses `wallet_address` in tool inputs, which maps internally to Elsa API's `evm_address` field.

Expected output includes:
- `ok: true` for successful calls
- `billing: { estimated_cost_usd, ... }` showing API cost
- `meta: { latency_ms, ... }` with request metadata

## Enabling Execution Tools

**WARNING**: Execution tools perform real onchain transactions.

```bash
# Required for x402 API payments
PAYMENT_PRIVATE_KEY=0x...

# Required for signing swap transactions (falls back to PAYMENT_PRIVATE_KEY if not set)
TRADE_PRIVATE_KEY=0x...

# Enable execution tools
ELSA_ENABLE_EXECUTION_TOOLS=true
```

1. Set `ELSA_ENABLE_EXECUTION_TOOLS=true`
2. Ensure `TRADE_PRIVATE_KEY` has sufficient funds for gas and swaps
3. Recommended: Use separate wallets for payments vs. trading

### Recommended Swap Flow

```
┌─────────────────────┐     ┌─────────────────────┐     ┌─────────────┐     ┌─────────────────────┐
│ 1. Get Quote        │ ──▶ │ 2. Dry Run          │ ──▶ │ 3. Confirm  │ ──▶ │ 4. Execute Pipeline │
│ elsa_get_swap_quote │     │ elsa_execute_swap_  │     │ [User says  │     │ elsa_pipeline_run_  │
│                     │     │ dry_run             │     │  "yes"]     │     │ and_wait            │
└─────────────────────┘     └─────────────────────┘     └─────────────┘     └─────────────────────┘
```

**Step 1: Get Quote** - Show user what they'll receive
```bash
npx tsx scripts/index.ts elsa_get_swap_quote '{
  "from_chain": "base", "from_token": [REDACTED_SECRET],
  "from_amount": "10", "to_chain": "base", "to_token": "0x4200000000000000000000000000000000000006",
  "wallet_address": "0x...", "slippage": 0.5
}'
```

**Step 2: Dry Run** - Create pipeline, get `pipeline_id`
```bash
npx tsx scripts/index.ts elsa_execute_swap_dry_run '{...same params...}'
# Returns: { "pipeline_id": "abc-123", ... }
```

**Step 3: User Confirmation** - Present results and wait for explicit "yes"

**Step 4: Execute Pipeline** - Sign and broadcast transactions
```bash
ELSA_ENABLE_EXECUTION_TOOLS=true npx tsx scripts/index.ts elsa_pipeline_run_and_wait '{
  "pipeline_id": "abc-123",
  "timeout_seconds": 180,
  "poll_interval_seconds": 3,
  "mode": "local_signer"
}'
# Automatically: signs approve tx → submits → signs swap tx → submits → returns tx hashes
```

### Critical Rules

- **NEVER** execute swaps without showing the user the quote first
- **NEVER** call execution tools in a loop
- **NEVER** proceed if budget limits are exceeded
- **ALWAYS** check `elsa_budget_status` if unsure about remaining budget
- **ALWAYS** use dry-run mode first for any swap operation

## Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `PAYMENT_PRIVATE_KEY` | Yes | - | Wallet for x402 API payments (hex with 0x prefix) |
| `TRADE_PRIVATE_KEY` | No | PAYMENT_PRIVATE_KEY | Wallet for tx signing (recommend separate) |
| `BASE_RPC_URL` | No | https://mainnet.base.org | Base RPC endpoint |
| `ELSA_API_URL` | No | https://x402-api.heyelsa.ai | Elsa API base URL |
| `ELSA_MAX_USD_PER_CALL` | No | 0.05 | Max USD per single API call |
| `ELSA_MAX_USD_PER_DAY` | No | 2.00 | Max USD spend per day |
| `ELSA_MAX_CALLS_PER_MINUTE` | No | 30 | Rate limit for API calls |
| `ELSA_TZ` | No | UTC | Timezone for daily budget reset (e.g., "Asia/Jakarta") |
| `ELSA_ENABLE_EXECUTION_TOOLS` | No | false | Enable onchain execution tools |
| `ELSA_REQUIRE_CONFIRMATION_TOKEN` | No | true | Require dry-run before execution |
| `ELSA_CONFIRMATION_TTL_SECONDS` | No | 600 | Token validity period |
| `ELSA_AUDIT_LOG_PATH` | No | - | Path for JSONL audit logs |
| `LOG_LEVEL` | No | info | Logging level (debug/info/warn/error) |

## API Pricing

Pricing may change. Actual costs are determined by x402 payment headers at request time.

See current pricing at [x402.heyelsa.ai](https://x402.heyelsa.ai).

## Supported Chains

- base (default)
- ethereum
- arbitrum
- optimism
- polygon
- bsc
- avalanche
- zksync

## Coming Soon

- **Hyperliquid Perp Endpoints** - Perpetual futures trading on Hyperliquid L1
- **Polymarket APIs** - Prediction market trading and data

## License

MIT


path: package.json
content: {
  "name": "openclaw-elsa-x402",
  "version": "1.0.0",
  "description": "OpenClaw skill-pack for Elsa x402 DeFi API with micropayments",
  "type": "module",
  "main": "scripts/index.js",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "typecheck": "tsc --noEmit",
    "lint": "eslint scripts --ext .ts"
  },
  "keywords": [
    "openclaw",
    "elsa",
    "x402",
    "defi",
    "micropayments",
    "base",
    "crypto"
  ],
  "author": "Elsa Team",
  "license": "MIT",
  "dependencies": {
    "@sinclair/typebox": "^0.32.0",
    "axios": "^1.6.0",
    "dotenv": "^17.2.3",
    "luxon": "^3.4.0",
    "pino": "^8.17.0",
    "viem": "^2.21.0",
    "x402-axios": "^1.1.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "@types/luxon": "^3.4.0",
    "@types/node": "^20.10.0",
    "tsx": "^4.7.0",
    "typescript": "^5.3.0",
    "vitest": "^1.2.0"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./scripts",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "noEmit": true
  },
  "include": ["scripts/**/*.ts"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}


path: assets/logo.svg
content: <svg width="315" height="233" viewBox="0 0 315 233" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3606_765)">
<path d="M49.1416 39.4966C48.6137 39.9038 48.137 40.1923 47.6377 40.4357C47.6357 40.4378 47.6316 40.4378 47.6296 40.4398C47.4556 40.5258 47.2797 40.6056 47.0955 40.6854C47.0935 40.6854 47.0915 40.6854 47.0874 40.6854C46.7539 40.8286 46.3999 40.9657 46.0091 41.1171C45.4464 39.6848 46.0377 34.4368 47.0853 29.8741C47.8403 26.578 48.8326 23.6399 49.7963 22.756C49.5283 26.0808 49.1682 29.1662 49.0147 32.2823C48.8961 34.6311 48.8981 36.9964 49.1416 39.4946V39.4966Z" fill="#DC2626"/>
<path d="M303.866 87.9875C299.373 87.6356 295.285 89.1231 291.263 91.2325C286.117 93.9313 281.317 97.1047 276.922 100.661C276.038 101.375 275.355 101.931 274.15 102.242C272.331 102.711 270.41 103.4 268.746 104.305C268.016 104.7 267.486 104.863 266.686 105.201L250.367 112.131C250.285 112.165 250.203 112.202 250.123 112.239C248.108 113.16 246.293 114.758 243.715 114.936C245.589 113.266 247.66 112.2 249.602 110.885C254.821 107.355 260.761 105.18 266.71 103.161C266.972 102.265 272.922 101.657 273.43 101.072C268.656 98.3671 261.741 99.8136 257.704 101.907L256.851 101.878C251.801 104.045 247.056 106.852 242.913 111.042C228.329 105.944 223.015 116.485 206.17 120.319C199.476 121.841 189.767 121.54 184.038 117.962C184.043 117.958 184.047 117.954 184.049 117.95C187.378 117.632 190.322 117.939 193.42 117.72L197.577 117.424C208.554 116.646 229.237 104.679 232.493 91.4638C227.492 91.0811 222.743 90.9297 217.9 89.074C228.204 87.7257 232.632 87.9057 242.6 83.564C246.539 81.8495 250.193 80.0203 253.229 76.9676C256.544 73.6367 259.238 69.8863 261.387 65.8965C261.751 65.2193 262.135 64.6648 262.242 63.8505C262.328 63.1999 263.032 61.9068 262.387 61.2521C259.377 60.761 256.284 61.1804 253.125 60.004C258.183 58.1134 263.467 57.3585 268.392 54.7702C277.561 49.9518 285.057 42.4797 290.389 33.6552C291.68 31.5171 292.707 29.0557 292.732 26.6107C292.756 24.0777 289.949 22.0092 287.486 21.8455C277.499 21.1867 267.056 21.7984 257.356 24.6772C229.291 33.0045 205.812 47.0833 184.918 67.4454C175.875 76.2576 166.031 86.7763 161.424 98.4407C157.99 107.126 157.639 116.309 160.722 124.822C160.879 125.211 161.506 125.618 161.544 126.052C161.741 128.206 163.603 129.581 164.337 132.055C162.228 130.492 160.622 128.272 159.484 126.031C158.447 125.45 158.346 123.918 157.461 123.196C156.832 122.684 156.571 121.956 156.589 121.135C154.565 117.156 153.915 112.695 154.224 108.024C154.324 106.533 154.997 105.121 154.353 103.445C151.294 109.303 150.496 116.217 152.094 122.737C152.908 126.062 154.711 128.72 155.818 131.699C158.829 136.397 162.166 140.734 167.026 143.901C167.39 144.139 167.513 144.648 167.715 144.843C175.588 149.909 183.783 153.945 193.111 156.29C208.953 160.274 218.271 160.834 234.406 158.058C242.545 156.658 250.266 154.608 257.692 151.451C257.966 150.727 258.786 150.819 259.379 150.6C273.824 145.268 289.571 132.614 297.767 119.509C297.937 118.684 298.215 117.772 299.054 117.45C302.072 112.626 316.883 89.0024 303.866 87.9875ZM279.801 104.722C278.921 105.778 277.591 106.306 276.535 107.173C276.171 107.472 275.484 108.151 274.974 107.697C274.948 107.169 275.598 106.907 275.821 106.78C276.149 105.559 277.432 105.121 278.209 104.362C283.22 99.4678 292.853 93.1722 300.265 93.6346C292.771 96.81 285.988 100.141 279.801 104.722Z" fill="#DC2626"/>
<path d="M156.589 121.135L157.461 123.196C156.832 122.684 156.571 121.956 156.589 121.135Z" fill="#DC2626"/>
<path d="M160.722 124.822C160.879 125.211 161.506 125.618 161.544 126.052C161.231 126.029 160.982 125.704 160.791 125.076C160.767 124.996 160.744 124.912 160.722 124.822Z" fill="#DC2626"/>
<path d="M159.484 126.031C160.237 127.445 160.922 128.094 161.54 127.973C161.555 127.973 161.567 127.971 161.581 127.965C161.663 127.947 161.745 127.914 161.825 127.865C161.278 127.112 161.186 126.506 161.544 126.052C161.741 128.206 163.603 129.581 164.337 132.055C162.228 130.492 160.622 128.272 159.484 126.031Z" fill="#DC2626"/>
<path d="M167.715 144.843C163.914 142.312 160.601 139.29 157.774 135.775C157.772 135.771 157.767 135.769 157.765 135.764C157.739 135.734 157.714 135.703 157.69 135.672C157.69 135.631 157.69 135.591 157.686 135.55C157.682 135.419 157.663 135.288 157.636 135.159C157.514 134.568 157.174 133.978 156.62 133.395L155.818 131.699C158.829 136.397 162.166 140.734 167.026 143.902C167.39 144.139 167.513 144.648 167.715 144.843Z" fill="#DC2626"/>
<path d="M248.832 112.712C249.256 112.569 249.773 112.37 250.367 112.131C250.285 112.165 250.203 112.202 250.123 112.239C248.108 113.16 246.293 114.758 243.715 114.936C245.589 113.266 247.66 112.2 249.601 110.885C248.797 111.515 248.245 111.987 247.911 112.325C247.713 112.524 247.592 112.675 247.543 112.783C247.529 112.81 247.521 112.832 247.519 112.855C247.506 112.914 247.529 112.955 247.582 112.976C247.74 113.041 248.175 112.939 248.832 112.712Z" fill="#DC2626"/>
<path d="M257.704 101.907L256.851 101.878C257.217 101.727 257.503 101.737 257.704 101.907Z" fill="#DC2626"/>
<path d="M259.379 150.6L257.691 151.451C257.966 150.727 258.786 150.819 259.379 150.6Z" fill="#DC2626"/>
<path d="M251.353 110.725L249.602 110.885C254.821 107.355 260.761 105.18 266.71 103.161L266.672 103.202L265.491 104.442C260.607 106.121 255.764 108.002 251.353 110.725Z" fill="#DC2626"/>
<path d="M266.686 105.201L250.367 112.131C250.291 111.955 250.23 111.779 250.219 111.601C250.176 110.815 262.913 105.825 264.511 105.236C265.45 104.581 266.174 104.569 266.686 105.201Z" fill="#DC2626"/>
<path d="M266.686 105.201L268.746 104.305C268.016 104.7 267.486 104.863 266.686 105.201Z" fill="#DC2626"/>
<path d="M273.81 101.373C271.406 101.862 269.039 102.459 266.71 103.161C266.972 102.265 272.922 101.657 273.43 101.072C268.656 98.367 261.741 99.8136 257.704 101.907C260.394 99.3307 271.302 98.2279 273.689 100.935C273.818 101.08 273.874 101.248 273.81 101.373Z" fill="#DC2626"/>
<path d="M274.15 102.242C272.331 102.711 270.41 103.4 268.746 104.305C268.769 104.047 268.812 103.799 268.926 103.593C269.022 103.419 269.389 103.226 269.892 103.042C271.195 102.561 273.409 102.134 274.15 102.242Z" fill="#DC2626"/>
<path d="M276.922 100.661C276.038 101.375 275.355 101.931 274.15 102.242L274.182 101.941L274.215 101.657C275.103 101.62 276.006 101.289 276.922 100.661Z" fill="#DC2626"/>
<path d="M299.054 117.45C298.414 117.986 298.066 118.75 297.767 119.509C297.937 118.684 298.215 117.772 299.054 117.45Z" fill="#DC2626"/>
<path d="M300.265 93.6346C292.771 96.81 285.988 100.141 279.801 104.722C278.921 105.778 277.591 106.306 276.535 107.173C276.171 107.472 275.484 108.151 274.974 107.697C274.948 107.169 275.598 106.907 275.821 106.78C276.149 105.559 277.432 105.121 278.209 104.362C283.22 99.4678 292.853 93.1722 300.265 93.6346Z" fill="#DC2626"/>
<path d="M110.799 45.8925C109.982 45.7043 109.049 45.4383 108.74 44.5912C100.374 39.6009 90.3466 37.5385 80.4193 36.9616C77.7369 36.8061 75.2203 37.1846 72.497 37.4096C55.1712 38.8357 39.4004 46.5124 29.1519 60.8183C24.8797 66.7825 22.1831 73.0679 20.2209 80.1471C16.1146 94.9768 14.8317 113.356 20.0368 128.041C24.1002 139.503 36.804 146.277 47.9651 149.248C70.3773 155.216 95.6438 152.519 116.204 142.183C121.297 139.623 125.673 136.499 129.735 132.742C136.159 126.801 139.66 119.147 139.705 110.267C139.822 87.431 131.034 59.0751 110.799 45.8925ZM62.449 57.2316C63.8137 52.9083 67.4126 49.8904 71.5006 49.2193C76.2495 48.4378 80.7384 50.5718 83.296 54.5001C86.7026 59.7298 85.8699 66.8152 80.6198 70.5779C76.1778 73.7635 70.5042 73.5692 66.4653 70.449C62.5247 67.4045 60.8797 62.2075 62.449 57.2316ZM118.85 134.207C110.619 141.195 100.581 145.19 89.8883 147.322C76.7303 149.945 63.5436 149.544 50.6454 145.027C45.3257 143.163 40.8408 140.564 36.9636 136.884C26.1381 126.604 29.2214 109.047 31.0158 95.7952C31.2879 93.7839 32.7611 92.7425 34.7887 92.6586C45.6245 92.2187 50.6086 92.3681 59.8219 86.0786C62.4469 84.2883 64.7651 82.3671 67.5088 80.7814C73.1763 77.5078 79.6827 76.9758 85.7042 79.5927L93.964 83.1835C102.443 86.8704 109.898 85.9497 118.917 84.319C121.471 83.8586 122.441 85.428 123.177 87.4249C126.052 95.2141 128.341 102.942 128.804 111.302C129.307 120.403 125.796 128.309 118.85 134.207Z" fill="#DC2626"/>
<path d="M58.3078 227.259C57.7738 229.726 44.2495 229.319 41.1518 228.814C31.3186 227.212 22.0112 222.792 14.8747 215.916C10.9401 212.122 8.36009 207.672 6.39386 202.684C3.67059 195.781 4.15959 188.503 5.4711 181.258C6.76214 174.132 11.5233 167.973 18.7723 166.737C26.1401 165.481 32.8859 169.141 36.405 175.451C34.2158 176.086 32.4705 176.636 30.9667 177.892C33.4854 177.737 35.8035 177.19 38.2853 177.696C49.9211 180.065 56.5134 191.269 53.0659 202.897C51.8874 202.385 51.4434 201.356 50.5227 200.636C42.6864 194.504 37.5324 195.787 30.934 187.768C30.8767 193.755 36.2148 195.175 37.4935 200.294C39.157 206.964 42.6107 212.746 47.736 217.303C51.0649 220.263 54.228 223.044 57.7226 225.765C58.1584 226.105 58.3856 226.901 58.3078 227.259Z" fill="#DC2626"/>
<path d="M117.514 168.609C115.081 171.482 112.239 173.319 108.947 174.981C96.0653 181.479 75.8055 184.315 62.0541 178.07C53.2398 172.691 48.9963 162.995 46.8664 152.951C53.1252 153.958 58.8091 155.441 64.9554 155.766C82.7108 156.701 96.1328 154.447 112.869 147.996C111.805 154.905 113.037 163.359 117.514 168.609Z" fill="#DC2626"/>
<path d="M129.933 187.14C126.502 190.459 122.4 192.401 118.275 194.567C112.407 197.649 106.375 199.752 99.7645 200.597C95.6213 201.127 91.7256 201.624 87.3799 201.203C78.6863 200.356 71.294 196.306 66.0541 189.628C63.7809 186.733 61.3339 183.903 61.162 179.789C77.2111 186.488 94.2995 184.055 109.51 177.544C113.502 175.836 117.121 174.05 120.601 171.371C122.009 172.482 123.253 173.26 124.976 173.808C126.078 178.424 127.519 182.751 129.933 187.14Z" fill="#DC2626"/>
<path d="M143.713 202.199C138.651 208.562 131.971 212.511 124.448 215.075C112.517 219.142 99.4821 219.744 88.7037 213.154C87.5804 212.073 79.2223 205.151 80.5973 203.779C96.6791 206.983 116.755 200.362 129.962 191.087C130.537 190.682 130.815 190.14 131.402 190.32C132.814 190.754 133.109 192.378 134.469 193.166C135.564 195.762 141.172 199.711 143.707 202.179C143.707 202.187 143.711 202.193 143.713 202.199Z" fill="#DC2626"/>
<path d="M153.622 217.939C151.181 221.821 147.533 224.55 143.128 225.915C129.817 230.035 118.277 228.274 105.954 223.026C104.616 221.741 102.954 221.317 101.843 219.952C104.198 219.506 106.422 219.969 108.781 219.754C116.174 219.083 123.228 217.291 130.001 214.44C135.511 212.122 139.879 208.695 143.936 204.652C144.147 204.495 144.325 204.352 144.472 204.223C145.497 203.594 146.4 204.192 147.695 204.611C149.675 205.254 151.232 206.465 152.611 207.815C155.404 210.551 155.685 214.659 153.622 217.939Z" fill="#DC2626"/>
<path d="M167.396 161.745C164.646 163.66 161.741 165.491 158.394 166.263C147.124 168.859 136.593 158.619 133.671 148.546C132.538 144.64 131.621 138.387 133.731 134.754C136.352 130.236 139.973 127.014 141.481 121.827C141.927 120.298 144.816 119.505 146.005 118.791C144.145 131.969 147.478 144.642 156.646 154.279C151.2 156.515 145.782 155.363 141.493 152.282C140.573 150.604 140.382 151.787 138.316 148.961C138.05 149.647 138.375 150.891 139.024 150.997C139.042 151.11 138.977 151.247 139.02 151.353C139.093 151.541 139.157 152.004 139.449 151.848C143.226 159.198 150.956 163.777 159.386 163.144C162.181 162.934 164.515 162.183 167.396 161.745Z" fill="#DC2626"/>
<path d="M192.098 211.447C187.122 215.019 176.626 213.97 170.79 212.161C165.465 210.508 160.877 207.867 157.109 203.758C156.599 203.202 155.828 203.114 155.768 202.355C155.703 201.516 156.286 201.221 156.73 200.642C160.972 195.118 167.443 192.941 174.244 194.353C181.616 195.883 188.01 200.131 191.531 206.887C192.272 208.306 192.495 209.927 192.098 211.447Z" fill="#DC2626"/>
<path d="M15.9263 126.606C10.2486 122.819 7.43324 116.855 5.89462 110.559C3.56828 101.045 4.58925 91.1118 9.88234 82.7599C11.7176 79.8648 14.3304 77.4218 17.6961 75.8648C15.2307 85.1497 13.3954 94.0561 13.469 103.529C13.0169 111.269 14.0849 118.322 15.9263 126.606Z" fill="#DC2626"/>
<path d="M142.901 114.936C142.907 110.6 143.31 107.061 142.815 103.14C140.943 88.3333 139.374 79.6356 132.906 65.8188C150.905 71.2878 156.063 102.185 142.901 114.936Z" fill="#DC2626"/>
<path d="M213.814 200.845C208.707 204.624 202.997 206.831 196.384 208.626C193.945 202.105 190.999 202.623 193.205 199.472C196.759 194.398 201.845 190.946 207.664 188.785C212.047 187.159 217.313 189.638 217.853 194.479C218.162 197.246 215.903 199.351 213.814 200.845Z" fill="#DC2626"/>
<path d="M41.2828 169.019C40.8327 168.63 40.0838 172.141 39.3125 172.307C39.1488 172.18 38.9871 172.047 38.8275 171.908C38.7846 171.818 38.7396 171.713 38.6946 171.588C38.2178 170.263 37.0291 169.78 36.27 169.021C32.1329 164.873 26.9094 162.318 20.8347 161.659C22.537 155.723 26.6905 150.467 32.8368 149.125C36.0593 148.421 39.5784 150.87 40.8695 153.796C43.0321 158.702 42.9442 164.178 41.2828 169.019Z" fill="#DC2626"/>
<path d="M127.03 162.895C127.271 164.464 128.288 165.454 129.141 166.559C130.078 167.773 131.189 168.783 132.515 169.708C129.299 169.972 128.802 168.546 126.688 168.204C119.091 162.617 116.18 154.14 119.057 144.998L127.938 139.202C127.339 147.901 129.722 155.836 134.915 162.545C135.55 163.365 136.556 163.846 136.575 164.843C132.431 165.315 131.386 165.868 127.03 162.895Z" fill="#DC2626"/>
<path d="M206.747 185.661C201.325 187.865 197.293 191.202 193.473 195.173C192.306 195.146 191.148 197.219 189.733 198.258C189.156 197.276 188.372 197.164 187.711 196.824C185.643 195.762 183.828 194.252 181.561 193.338C183.419 188.658 187.378 182.146 192.169 180.881C197.303 179.525 202.893 181.587 206.747 185.661Z" fill="#DC2626"/>
<path d="M168.9 190.749C162.32 191.206 156.217 194.686 152.879 200.564C149.538 199.693 143.646 194.05 145.342 190.49C151.241 185.221 163.161 184.214 168.9 190.749Z" fill="#DC2626"/>
<path d="M73.7165 208.959C73.7042 209.88 73.1436 210.839 72.2126 211.494C67.6398 214.698 63.4024 217.927 59.5088 222.463C58.0746 222.054 56.5012 220.621 56.2065 219.259C56.9595 214.755 59.2285 210.631 62.1359 207.146C63.3615 205.677 65.2459 205.671 66.8745 205.258C69.1231 204.687 70.7599 204.937 72.2699 206.79C72.8244 207.47 73.7287 207.999 73.7165 208.959Z" fill="#DC2626"/>
<path d="M187.443 229.346C182.522 227.86 179.163 224.319 174.218 221.46C171.676 219.991 168.988 218.786 167.302 216.495C167.126 215.308 166.062 215.022 166.22 213.753C167.926 214.86 169.289 214.846 170.578 215.224C174.672 216.425 178.692 216.004 182.629 216.82C186.649 217.655 188.38 226.048 187.443 229.346Z" fill="#DC2626"/>
<path d="M178.279 157.855C176.493 157.767 175.3 156.634 173.78 156.763C164.706 153.051 154.702 143.764 153.407 133.383C153.346 132.884 154.271 133.041 154.549 133.043L163.204 143.206C164.425 144.64 165.751 145.698 166.88 146.936C168.667 148.894 170.428 150.78 172.732 152.509C173.548 153.123 174.608 153.819 175.048 154.645C175.752 155.977 177.203 156.497 178.279 157.855Z" fill="#DC2626"/>
<path d="M82.6658 219.502C79.4556 222.26 76.4029 225.078 73.3932 228.325C72.896 228.863 72.1349 229.11 71.7032 229.17C68.6853 229.587 70.3487 214.813 76.1553 213.424C78.4448 212.877 80.0387 213.004 81.7185 214.506C83.0566 215.701 84.4561 217.966 82.6658 219.502Z" fill="#DC2626"/>
<path d="M98.9318 5.26648L95.1937 14.7417C95.1712 14.7887 95.1486 14.8337 95.1282 14.8808C92.2781 20.7386 90.8786 27.1222 89.7492 33.7104C88.6464 34.1012 85.9783 34.0378 85.8924 32.9043C85.6919 30.1871 87.3083 28.1043 87.5108 25.8291C87.5967 25.6634 87.6745 25.4895 87.742 25.3094C87.7441 25.3053 87.7461 25.2992 87.7481 25.2951C90.1727 19.7483 93.7676 9.00458 98.9318 5.26648Z" fill="#DC2626"/>
<path d="M185.575 181.458C182.461 184.466 180.943 187.953 179.566 192.243L173.894 191.011C173.43 187.418 174.842 181.976 178.586 180.742C180.822 180.008 183.144 180.255 185.575 181.458Z" fill="#DC2626"/>
<path d="M147.787 185.266C145.264 187.069 142.899 188.063 140.699 190.494C138.424 189.233 137.321 187.081 136.204 184.783C139.079 184.581 141.225 184.034 143.654 182.968C145.021 183.762 146.604 184.165 147.787 185.266Z" fill="#DC2626"/>
<path d="M164.916 221.514C163.251 222.856 161.51 223.59 159.245 224.343C159.938 222.67 161.235 221.645 162.404 220.349C163.255 220.675 163.965 220.583 164.916 221.514Z" fill="#DC2626"/>
<path d="M79.8361 55.3677C77.0208 52.0797 72.3497 51.5416 69.0065 53.8884C64.8571 56.7999 64.006 62.0582 66.7907 65.9907C69.5589 69.8966 75.0341 70.6597 78.809 67.6971C82.5492 64.761 83.1835 59.2797 79.8361 55.3677ZM74.8807 65.3339C72.4418 65.9825 70.183 64.8388 69.3339 62.5431C68.4889 60.2536 69.5221 57.469 72.2945 56.7467C74.9973 56.0408 77.2847 57.4853 77.8494 59.7339C78.4551 62.1503 77.1886 64.7221 74.8807 65.3339Z" fill="#DC2626"/>
<path d="M314.373 93.9067C313.816 89.7921 311.177 86.0765 307.291 84.7323C297.354 81.297 284.629 88.7753 276.717 94.3507C275.584 95.1486 274.69 95.4944 273.233 95.3021C261.728 93.786 251.862 97.9374 242.485 104.268C241.125 103.922 240.022 103.498 238.753 102.823C240.879 99.4617 242.084 96.1962 242.845 92.4008C251.95 90.7333 259.93 85.4689 264.744 77.4791C267.519 72.8714 268.971 67.9364 270.624 62.8193C272.621 61.473 274.774 60.6423 276.967 59.4106C286.59 53.9989 294.334 45.4342 297.753 34.8889C299.175 30.5043 299.191 25.293 295.93 21.9089C293.452 19.3391 289.237 18.533 285.667 18.2567C273.738 17.3299 262.015 18.9483 250.506 22.2915C242.201 24.7038 234.348 27.335 226.514 30.981C204.488 41.2296 182.103 61.6101 165.591 79.3185C161.399 83.8136 157.839 88.3128 154.048 93.3768C153.46 80.4029 147.865 68.319 135.758 62.6781C134.164 62.0009 132.663 61.6797 130.864 61.8229C122.083 47.0464 109.307 38.2014 92.413 34.3815C93.7286 27.0935 95.0606 19.9815 97.7736 13.4915L103.413 0C95.3225 3.08951 89.0616 16.0491 86.2975 23.2982C85.076 26.5002 83.8258 29.69 83.0811 33.3912C83.075 33.4178 76.5829 33.3012 75.9752 33.3258C73.6059 33.4158 71.2407 33.6183 68.8898 33.9416C65.7205 34.3754 62.5799 35.0219 59.4965 35.8772C57.9395 36.3089 56.3988 36.7938 54.8745 37.3278C54.1052 37.5979 53.34 37.8802 52.5809 38.1749C51.9384 38.4245 51.3471 38.8991 51.1855 37.915C51.1139 37.4751 51.0484 37.0332 50.9891 36.5892C50.1379 30.1012 50.8786 23.38 51.8055 16.937C47.7052 21.3769 45.5057 27.4577 44.3211 33.2951C43.8955 35.3902 43.5497 37.5672 43.5497 39.7073C43.5497 40.538 43.5906 41.4117 43.652 42.3508C32.3416 49.1273 23.8485 59.3676 18.9442 71.7605C16.6403 71.9426 14.8603 72.851 13.0434 74.1625C2.18511 82.0111 -0.544294 96.7098 1.68997 109.442C2.9892 116.843 6.20556 123.439 11.7503 128.542C13.2296 129.905 14.8419 130.856 17.0577 131.007C19.8731 136.957 24.0961 141.808 29.9088 145.436C23.7769 148.2 17.2787 156.352 15.9078 162.97C-2.55964 167.869 -2.80311 194.977 4.37436 209.053C9.69405 219.488 18.7559 226.651 29.9252 229.81C37.3155 231.901 44.5666 232.971 52.2801 232.566C55.5906 232.392 61.5753 232.951 62.9175 228.869C63.4883 227.132 63.1671 225.493 62.0008 223.862C63.5845 222.528 64.9308 221.321 66.7722 220.108C66.7579 222.51 65.9763 224.66 66.7988 227.007C67.4535 228.881 68.7261 230.858 70.408 231.946C74.1931 234.395 76.7855 226.913 84.9184 223.247C91.1486 220.439 91.9854 222.101 98.2872 225.374C107.703 230.264 117.731 232.603 128.372 232.898C139.054 233.192 147.063 233.072 157.203 229.368C161.237 227.895 164.965 226.316 168.603 223.703C175.801 227.177 183.03 230.16 190.886 232.135C193.945 226.852 191.427 221.376 188.722 216.366C192.227 216.259 194.039 213.579 196.411 212.331C201.305 209.753 212.362 208.399 218.913 201.818C220.521 200.2 221.982 198.052 222.078 195.625C222.277 190.684 218.713 186.479 214.101 185.258L210.172 184.221C206.811 177.522 194.111 174.111 188.045 178.35C184.648 177.158 181.129 176.656 177.577 177.276C172.263 178.205 171.167 183.852 171.249 188.912C163.981 181.937 158.995 184.096 152.791 182.639C148.501 181.632 144.861 179.425 142.866 174.911L155.296 171.5C164.2 169.057 173.634 169.203 181.968 164.472C199.083 169.46 214.365 170.805 231.887 168.171C262.295 163.601 290.841 148.976 306.8 121.98C311.377 114.238 315.586 102.881 314.373 93.9067ZM142.901 114.936C142.907 110.6 143.31 107.061 142.815 103.14C140.943 88.3333 139.374 79.6356 132.906 65.8188C150.905 71.2878 156.063 102.185 142.901 114.936ZM87.5107 25.8291C87.5967 25.6634 87.6744 25.4895 87.742 25.3094C87.744 25.3053 87.746 25.2992 87.7481 25.2951C90.1726 19.7483 93.7675 9.00458 98.9317 5.26648L95.1936 14.7417C95.1711 14.7887 95.1486 14.8337 95.1281 14.8808C92.278 20.7386 90.8785 27.1222 89.7491 33.7104C88.6463 34.1012 85.9783 34.0378 85.8923 32.9043C85.6918 30.1871 87.3082 28.1043 87.5107 25.8291ZM47.0853 29.8761C47.8403 26.58 48.8326 23.6419 49.7963 22.758C49.5282 26.0828 49.1681 29.1682 49.0147 32.2843C48.896 34.6332 48.898 36.9984 49.1415 39.4966C48.6136 39.9037 48.1369 40.1922 47.6377 40.4357C47.6356 40.4378 47.6316 40.4378 47.6295 40.4398C47.4556 40.5257 47.2796 40.6055 47.0955 40.6853C47.0934 40.6853 47.0914 40.6853 47.0873 40.6853C46.7538 40.8286 46.3998 40.9656 46.0091 41.117C45.4464 39.6848 46.0377 34.4367 47.0853 29.8741V29.8761ZM5.89457 110.559C3.56823 101.045 4.5892 91.1118 9.88228 82.7599C11.7176 79.8648 14.3304 77.4218 17.6961 75.8648C15.2306 85.1497 13.3953 94.0561 13.469 103.529C13.0168 111.269 14.0848 118.322 15.9263 126.606C10.2485 122.819 7.43318 116.855 5.89457 110.559ZM20.0367 128.041C14.8316 113.356 16.1145 94.9768 20.2209 80.1471C22.183 73.0679 24.8797 66.7825 29.1518 60.8183C39.4004 46.5124 55.1712 38.8357 72.497 37.4096C75.2202 37.1846 77.7369 36.8061 80.4192 36.9616C90.3465 37.5385 100.374 39.6009 108.74 44.5912C109.049 45.4383 109.982 45.7042 110.799 45.8925C131.034 59.0751 139.822 87.431 139.705 110.267C139.66 119.147 136.159 126.801 129.735 132.742C125.673 136.499 121.297 139.623 116.204 142.183C95.6437 152.519 70.3773 155.216 47.9651 149.248C36.804 146.277 24.1002 139.502 20.0367 128.041ZM129.141 166.559C130.078 167.773 131.189 168.783 132.515 169.708C129.299 169.972 128.802 168.546 126.688 168.204C119.091 162.617 116.18 154.14 119.056 144.998L127.938 139.202C127.339 147.901 129.722 155.836 134.915 162.545C135.55 163.365 136.556 163.846 136.575 164.843C132.431 165.315 131.386 165.868 127.03 162.895C127.271 164.464 128.288 165.454 129.141 166.559ZM64.4765 196.364C64.3517 196.268 64.231 196.167 64.1164 196.063C63.4351 196.069 62.7456 195.369 62.1134 195.24C61.209 195.056 60.7609 194.496 60.0592 193.98C58.4469 192.798 56.4561 191.558 55.5333 189.575L51.2448 180.37C53.6858 182.614 55.5006 184.787 57.2397 187.017C57.8801 187.13 58.0909 187.744 58.4285 188.182C58.5246 188.251 58.9216 188.198 59.1712 188.3C62.5451 192.173 66.4039 195.118 70.4428 198.377C68.1737 198.271 66.0029 197.567 64.4786 196.366L64.4765 196.364ZM66.1031 201.763C60.1001 204.806 55.4044 209.597 53.5425 215.991C49.8617 212.849 47.6868 208.605 46.629 203.834C49.4382 205.014 51.6152 206.803 54.1625 208.351C56.6934 205.096 57.6285 201.424 58.2709 197.561C61.1272 198.758 63.5415 199.948 66.1031 201.763ZM117.514 168.609C115.081 171.482 112.239 173.319 108.947 174.981C96.0652 181.479 75.8054 184.315 62.054 178.07C53.2397 172.691 48.9963 162.995 46.8663 152.951C53.1251 153.958 58.809 155.441 64.9553 155.766C82.7108 156.701 96.1327 154.447 112.869 147.996C111.805 154.905 113.037 163.359 117.514 168.609ZM66.054 189.628C63.7809 186.733 61.3338 183.903 61.162 179.789C77.211 186.488 94.2995 184.055 109.51 177.544C113.501 175.836 117.121 174.05 120.601 171.371C122.009 172.482 123.253 173.26 124.976 173.808C126.078 178.424 127.519 182.752 129.933 187.14C126.502 190.459 122.4 192.401 118.275 194.567C112.407 197.649 106.375 199.752 99.7644 200.597C95.6212 201.127 91.7256 201.624 87.3798 201.203C78.6862 200.356 71.2939 196.306 66.054 189.628ZM32.8367 149.125C36.0592 148.421 39.5784 150.87 40.8694 153.796C43.0321 158.702 42.9441 164.178 41.2827 169.019C40.8326 168.63 40.0837 172.141 39.3124 172.307C39.1487 172.18 38.9871 172.047 38.8275 171.908C38.7845 171.818 38.7395 171.713 38.6945 171.588C38.2178 170.263 37.029 169.78 36.2699 169.021C32.1329 164.873 26.9094 162.318 20.8347 161.659C22.537 155.723 26.6904 150.467 32.8367 149.125ZM58.3077 227.259C57.7737 229.726 44.2495 229.319 41.1518 228.814C31.3185 227.212 22.0112 222.792 14.8746 215.916C10.9401 212.122 8.36003 207.672 6.3938 202.684C3.67053 195.781 4.15953 188.503 5.47104 181.258C6.76208 174.132 11.5232 167.973 18.7723 166.737C26.14 165.481 32.8858 169.141 36.405 175.451C34.2157 176.086 32.4705 176.636 30.9666 177.892C33.4853 177.737 35.8034 177.19 38.2853 177.696C49.9211 180.065 56.5134 191.269 53.0658 202.897C51.8873 202.385 51.4433 201.356 50.5226 200.636C42.6863 194.504 37.5323 195.787 30.9339 187.768C30.8766 193.755 36.2147 195.175 37.4935 200.294C39.1569 206.964 42.6106 212.746 47.7359 217.303C51.0648 220.263 54.228 223.044 57.7226 225.765C58.1584 226.105 58.3855 226.901 58.3077 227.259ZM59.5088 222.463C58.0745 222.054 56.5011 220.621 56.2065 219.259C56.9594 214.756 59.2285 210.631 62.1359 207.146C63.3614 205.677 65.2458 205.671 66.8745 205.258C69.1231 204.687 70.7599 204.937 72.2699 206.79C72.8243 207.47 73.7287 208 73.7164 208.959C73.7041 209.88 73.1435 210.839 72.2126 211.494C67.6397 214.698 63.4024 217.927 59.5088 222.463ZM82.6657 219.502C79.4555 222.26 76.4028 225.078 73.3931 228.325C72.8959 228.863 72.1348 229.11 71.7031 229.17C68.6852 229.587 70.3486 214.813 76.1553 213.424C78.4448 212.877 80.0386 213.004 81.7184 214.506C83.0565 215.701 84.456 217.966 82.6657 219.502ZM88.7036 213.153C87.5803 212.073 79.2223 205.151 80.5972 203.779C96.679 206.983 116.755 200.362 129.962 191.087C130.537 190.682 130.815 190.14 131.402 190.32C132.814 190.754 133.109 192.378 134.469 193.166C135.564 195.762 141.172 199.711 143.707 202.179C143.707 202.187 143.711 202.193 143.713 202.199C138.651 208.562 131.971 212.511 124.448 215.075C112.517 219.142 99.4821 219.744 88.7036 213.153ZM153.622 217.939C151.181 221.82 147.533 224.55 143.128 225.915C129.817 230.035 118.277 228.274 105.954 223.026C104.616 221.741 102.954 221.317 101.843 219.952C104.198 219.506 106.422 219.969 108.781 219.754C116.174 219.083 123.228 217.291 130.001 214.44C135.511 212.122 139.879 208.695 143.936 204.652C144.147 204.495 144.325 204.351 144.472 204.223C145.497 203.594 146.4 204.192 147.695 204.611C149.675 205.254 151.232 206.465 152.611 207.815C155.404 210.551 155.685 214.659 153.622 217.939ZM159.245 224.343C159.938 222.67 161.235 221.645 162.404 220.349C163.255 220.675 163.965 220.583 164.916 221.514C163.251 222.856 161.51 223.59 159.245 224.343ZM187.443 229.346C182.522 227.86 179.163 224.319 174.217 221.46C171.676 219.991 168.988 218.786 167.302 216.495C167.126 215.308 166.062 215.022 166.22 213.753C167.926 214.86 169.289 214.846 170.578 215.224C174.672 216.425 178.692 216.004 182.629 216.82C186.649 217.655 188.38 226.048 187.443 229.346ZM217.853 194.479C218.162 197.246 215.903 199.351 213.814 200.845C208.707 204.624 202.997 206.831 196.384 208.626C193.945 202.105 190.999 202.623 193.205 199.472C196.759 194.398 201.845 190.946 207.664 188.785C212.047 187.159 217.313 189.638 217.853 194.479ZM192.169 180.881C197.303 179.525 202.893 181.587 206.747 185.661C201.325 187.865 197.293 191.202 193.473 195.173C192.306 195.146 191.148 197.219 189.733 198.258C189.156 197.276 188.372 197.164 187.711 196.824C185.642 195.762 183.828 194.252 181.561 193.338C183.418 188.658 187.378 182.146 192.169 180.881ZM178.586 180.742C180.822 180.008 183.144 180.255 185.575 181.458C182.461 184.466 180.943 187.953 179.566 192.243L173.894 191.011C173.43 187.419 174.842 181.976 178.586 180.742ZM174.244 194.353C181.616 195.883 188.01 200.13 191.531 206.886C192.272 208.306 192.495 209.927 192.098 211.447C187.122 215.019 176.626 213.97 170.79 212.161C165.465 210.508 160.877 207.867 157.109 203.758C156.599 203.202 155.828 203.114 155.768 202.355C155.703 201.516 156.286 201.221 156.73 200.642C160.971 195.118 167.443 192.941 174.244 194.353ZM168.9 190.749C162.32 191.206 156.216 194.686 152.879 200.564C149.538 199.693 143.646 194.05 145.342 190.49C151.241 185.221 163.161 184.214 168.9 190.749ZM143.654 182.968C145.021 183.762 146.604 184.165 147.787 185.266C145.264 187.069 142.899 188.063 140.699 190.494C138.424 189.233 137.321 187.081 136.204 184.783C139.079 184.581 141.225 184.034 143.654 182.968ZM158.393 166.263C147.124 168.859 136.593 158.619 133.671 148.546C132.538 144.64 131.621 138.387 133.731 134.754C136.352 130.236 139.973 127.014 141.481 121.827C141.927 120.298 144.816 119.505 146.005 118.791C144.145 131.969 147.478 144.642 156.646 154.279C151.2 156.515 145.782 155.363 141.493 152.282C140.573 150.604 140.382 151.787 138.316 148.961C138.05 149.647 138.375 150.891 139.024 150.997C139.042 151.11 138.977 151.247 139.02 151.353C139.093 151.541 139.157 152.004 139.449 151.848C143.226 159.198 150.956 163.777 159.386 163.144C162.181 162.934 164.515 162.183 167.396 161.745C164.646 163.66 161.741 165.491 158.393 166.263ZM173.78 156.763C164.705 153.051 154.702 143.764 153.407 133.383C153.346 132.884 154.271 133.041 154.549 133.043L163.204 143.206C164.425 144.64 165.751 145.698 166.88 146.936C168.667 148.894 170.428 150.78 172.732 152.509C173.548 153.123 174.608 153.819 175.048 154.645C175.752 155.977 177.203 156.497 178.279 157.855C176.493 157.767 175.3 156.634 173.78 156.763ZM299.054 117.45C298.215 117.772 297.937 118.684 297.767 119.509C289.571 132.614 273.824 145.268 259.379 150.6C258.786 150.819 257.966 150.727 257.691 151.451C250.266 154.608 242.545 156.658 234.406 158.058C218.271 160.834 208.953 160.274 193.111 156.29C183.783 153.945 175.588 149.909 167.715 144.843C167.513 144.648 167.39 144.139 167.026 143.901C162.166 140.734 158.829 136.397 155.818 131.699C154.711 128.72 152.908 126.062 152.094 122.737C150.496 116.217 151.294 109.303 154.353 103.445C154.997 105.121 154.324 106.533 154.224 108.024C153.933 112.339 154.59 116.712 156.276 120.702C157.135 122.735 158.263 124.603 159.566 126.379C160.843 128.12 161.851 129.935 163.511 131.386C163.779 131.619 164.055 131.844 164.339 132.055C159.486 123.766 156.591 109.72 161.426 98.4407C166.367 86.9134 175.877 76.2576 184.92 67.4454C205.814 47.0833 229.293 33.0045 257.358 24.6772C267.058 21.7984 277.501 21.1867 287.488 21.8455C289.951 22.0092 292.758 24.0777 292.734 26.6107C292.709 29.0557 291.682 31.5171 290.391 33.6552C285.059 42.4797 277.563 49.9518 268.394 54.7702C263.469 57.3584 258.185 58.1134 253.127 60.004C256.286 61.1804 259.379 60.761 262.389 61.252C263.034 61.9068 262.33 63.1999 262.244 63.8505C262.137 64.6648 261.753 65.2193 261.389 65.8965C259.24 69.8863 256.546 73.6367 253.231 76.9676C250.195 80.0203 246.541 81.8494 242.602 83.564C232.634 87.9057 228.206 87.7256 217.902 89.074C222.745 90.9297 227.494 91.0811 232.495 91.4637C229.239 104.679 208.556 116.646 197.579 117.424L193.422 117.72C190.324 117.939 187.38 117.632 184.051 117.95C184.049 117.954 184.045 117.958 184.04 117.962C189.769 121.54 199.478 121.841 206.172 120.319C223.017 116.485 228.331 105.943 242.915 111.042C247.058 106.852 251.803 104.045 256.853 101.878L257.706 101.907C261.743 99.8136 268.658 98.367 273.432 101.072C272.924 101.657 266.974 102.265 266.712 103.161C266.694 103.167 266.678 103.173 266.661 103.179C266.604 103.198 266.549 103.218 266.491 103.237C266.379 103.275 266.266 103.312 266.154 103.351C266.111 103.365 266.068 103.382 266.025 103.396C265.378 103.617 264.732 103.84 264.085 104.067C264.083 104.069 264.079 104.069 264.077 104.071C263.942 104.118 263.807 104.165 263.672 104.215C263.662 104.219 263.652 104.223 263.639 104.227C263.439 104.298 263.236 104.37 263.036 104.444C262.962 104.47 262.89 104.495 262.817 104.522C262.772 104.538 262.727 104.554 262.682 104.571C262.631 104.589 262.577 104.609 262.526 104.628C262.367 104.687 262.207 104.745 262.047 104.804C261.97 104.832 261.892 104.861 261.814 104.89C261.745 104.916 261.673 104.941 261.603 104.97C261.563 104.984 261.522 105 261.479 105.017C260.848 105.254 260.22 105.497 259.594 105.749C259.537 105.772 259.48 105.794 259.422 105.817C259.328 105.856 259.232 105.894 259.138 105.933C259.019 105.982 258.901 106.031 258.78 106.081C258.661 106.13 258.545 106.179 258.426 106.228C258.295 106.283 258.166 106.338 258.035 106.394C257.755 106.512 257.475 106.637 257.194 106.76C257.065 106.817 256.936 106.872 256.808 106.93C256.54 107.05 256.274 107.173 256.008 107.298C255.971 107.316 255.932 107.333 255.895 107.351C255.785 107.404 255.672 107.458 255.562 107.511C255.49 107.543 255.416 107.578 255.345 107.613C255.136 107.713 254.931 107.816 254.725 107.918C254.581 107.987 254.442 108.063 254.301 108.135C254.08 108.247 253.861 108.358 253.642 108.472C253.507 108.544 253.374 108.616 253.239 108.689C253.104 108.761 252.971 108.835 252.838 108.91C252.834 108.912 252.83 108.914 252.828 108.916C252.683 108.996 252.542 109.076 252.398 109.158C252.378 109.17 252.355 109.18 252.335 109.193C252.212 109.262 252.087 109.334 251.965 109.405C251.909 109.436 251.856 109.471 251.801 109.502C251.69 109.567 251.58 109.632 251.469 109.698C251.461 109.702 251.453 109.706 251.445 109.712C251.326 109.782 251.208 109.856 251.091 109.927C250.958 110.007 250.827 110.089 250.696 110.173C250.682 110.181 250.667 110.191 250.653 110.199C250.608 110.228 250.563 110.257 250.518 110.285C250.461 110.322 250.399 110.361 250.342 110.398C250.217 110.478 250.095 110.557 249.972 110.639C249.849 110.721 249.726 110.803 249.603 110.885C247.662 112.2 245.591 113.266 243.717 114.936C246.295 114.758 248.11 113.16 250.125 112.239C250.205 112.202 250.287 112.165 250.369 112.131L266.688 105.201C267.488 104.863 268.018 104.7 268.748 104.305C270.412 103.4 272.333 102.711 274.152 102.242C275.357 101.931 276.04 101.375 276.924 100.661C281.319 97.1046 286.119 93.9312 291.265 91.2325C295.287 89.1231 299.375 87.6356 303.868 87.9875C316.885 89.0024 302.074 112.626 299.056 117.45H299.054Z" fill="#010202"/>
<path d="M66.4653 70.449C70.5042 73.5692 76.1778 73.7635 80.6198 70.5779C85.8699 66.8152 86.7026 59.7298 83.296 54.5001C80.7384 50.5718 76.2495 48.4378 71.5006 49.2193C67.4126 49.8904 63.8137 52.9083 62.449 57.2316C60.8797 62.2075 62.5247 67.4045 66.4653 70.449ZM69.0065 53.8884C72.3497 51.5416 77.0208 52.0797 79.8362 55.3677C83.1835 59.2797 82.5492 64.761 78.809 67.697C75.0341 70.6597 69.5589 69.8965 66.7907 65.9907C64.006 62.0582 64.8572 56.7999 69.0065 53.8884Z" fill="#606060"/>
<path d="M89.8883 147.322C100.581 145.19 110.619 141.195 118.85 134.207C125.796 128.309 129.307 120.403 128.804 111.302C128.341 102.942 126.052 95.2141 123.177 87.4249C122.441 85.428 121.471 83.8587 118.917 84.319C109.898 85.9497 102.443 86.8704 93.964 83.1835L85.7042 79.5927C79.6827 76.9758 73.1763 77.5078 67.5088 80.7814C64.7651 82.3671 62.4469 84.2883 59.8219 86.0786C50.6086 92.3681 45.6245 92.2187 34.7887 92.6586C32.7611 92.7425 31.2879 93.784 31.0158 95.7952C29.2214 109.047 26.1381 126.604 36.9636 136.884C40.8408 140.564 45.3257 143.163 50.6454 145.027C63.5436 149.544 76.7303 149.945 89.8883 147.322Z" fill="#606060"/>
<path d="M74.8807 65.3338C77.1886 64.7221 78.4551 62.1502 77.8495 59.7339C77.2848 57.4853 74.9973 56.0408 72.2945 56.7467C69.5221 57.4689 68.4889 60.2535 69.3339 62.5431C70.183 64.8387 72.4418 65.9824 74.8807 65.3338Z" fill="#606060"/>
<path d="M118.85 134.207C110.619 141.195 100.581 145.19 89.8883 147.322C76.7303 149.945 63.5436 149.544 50.6454 145.027C45.3257 143.163 40.8408 140.564 36.9636 136.884C26.138 126.604 29.2214 109.047 31.0158 95.7952C31.2879 93.784 32.7611 92.7425 34.7887 92.6586C45.6244 92.2187 50.6086 92.3681 59.8219 86.0786C62.4469 84.2883 64.7651 82.3671 67.5088 80.7814C73.1763 77.5078 79.6827 76.9758 85.7042 79.5927L93.964 83.1835C102.443 86.8704 109.898 85.9497 118.917 84.319C121.471 83.8587 122.441 85.428 123.177 87.4249C126.052 95.2141 128.341 102.942 128.804 111.302C129.307 120.403 125.796 128.309 118.85 134.207Z" fill="#FEFEFE"/>
<path d="M83.296 54.5001C80.7384 50.5718 76.2494 48.4378 71.5006 49.2193C67.4126 49.8904 63.8137 52.9083 62.449 57.2316C60.8797 62.2075 62.5247 67.4045 66.4653 70.449C70.5042 73.5692 76.1778 73.7635 80.6198 70.5779C85.8699 66.8152 86.7026 59.7298 83.296 54.5001ZM78.809 67.697C75.0341 70.6597 69.5589 69.8965 66.7906 65.9907C64.006 62.0582 64.8571 56.7999 69.0065 53.8884C72.3497 51.5416 77.0208 52.0797 79.8361 55.3677C83.1834 59.2797 82.5492 64.761 78.809 67.697Z" fill="#F9FCFB"/>
<path d="M74.8807 65.3338C72.4418 65.9824 70.183 64.8387 69.3339 62.5431C68.4888 60.2535 69.5221 57.4689 72.2945 56.7467C74.9973 56.0408 77.2847 57.4853 77.8494 59.7339C78.4551 62.1502 77.1886 64.7221 74.8807 65.3338Z" fill="#FAFBFA"/>
<path d="M118.85 134.207C110.619 141.195 100.581 145.19 89.8883 147.322C76.7303 149.945 63.5436 149.544 50.6454 145.027C45.3257 143.163 40.8408 140.564 36.9636 136.884C26.138 126.604 29.2214 109.047 31.0158 95.7952C31.2879 93.784 32.7611 92.7425 34.7887 92.6586C45.6244 92.2187 50.6086 92.3681 59.8219 86.0786C62.4469 84.2883 64.7651 82.3671 67.5088 80.7814C73.1763 77.5078 79.6827 76.9758 85.7042 79.5927L93.964 83.1835C102.443 86.8704 109.898 85.9497 118.917 84.319C121.471 83.8587 122.441 85.428 123.177 87.4249C126.052 95.2141 128.341 102.942 128.804 111.302C129.307 120.403 125.796 128.309 118.85 134.207Z" fill="#FEFEFE"/>
<path d="M83.296 54.5002C80.7384 50.5718 76.2494 48.4378 71.5006 49.2194C67.4126 49.8905 63.8137 52.9084 62.449 57.2317C60.8797 62.2076 62.5247 67.4045 66.4653 70.449C70.5042 73.5692 76.1778 73.7636 80.6198 70.5779C85.8699 66.8153 86.7026 59.7299 83.296 54.5002ZM78.809 67.6971C75.0341 70.6598 69.5589 69.8966 66.7906 65.9907C64.006 62.0582 64.8571 56.7999 69.0065 53.8884C72.3497 51.5416 77.0208 52.0797 79.8361 55.3677C83.1834 59.2797 82.5492 64.761 78.809 67.6971Z" fill="#F9FCFB"/>
<path d="M74.8807 65.3339C72.4418 65.9825 70.183 64.8388 69.3339 62.5431C68.4888 60.2536 69.5221 57.469 72.2945 56.7467C74.9973 56.0408 77.2847 57.4853 77.8494 59.7339C78.4551 62.1503 77.1886 64.7221 74.8807 65.3339Z" fill="#FAFBFA"/>
<path d="M278.209 104.362C277.432 105.121 276.149 105.559 275.821 106.78C275.598 106.907 274.948 107.169 274.974 107.697C275.484 108.151 276.171 107.472 276.535 107.173C277.591 106.306 278.921 105.778 279.801 104.722C285.988 100.141 292.771 96.81 300.265 93.6346C292.853 93.1722 283.22 99.4678 278.209 104.362Z" fill="#010202"/>
<path d="M54.717 120.485C50.6556 121.933 46.5779 120.253 44.583 117.141C42.3467 113.653 43.0751 109.381 45.8168 106.686C48.7876 103.766 53.4219 103.392 56.7221 106.036C59.1139 107.953 60.2638 110.811 59.9098 113.817C59.5907 116.544 57.7881 119.388 54.717 120.483V120.485Z" fill="#010202"/>
<path d="M104.697 98.8765C109.485 98.1952 113.256 101.876 113.87 106.034C114.557 110.688 111.149 114.799 106.827 115.417C102.189 116.077 98.1277 112.845 97.4054 108.519C96.6361 103.922 99.6622 99.5926 104.697 98.8765Z" fill="#020302"/>
<path d="M77.6285 138.441C72.7487 137.254 69.7083 132.97 69.2622 127.91L91.0504 125.649C91.7972 129.508 90.5635 133.223 87.9916 135.783C85.4013 138.361 81.6305 139.21 77.6264 138.441H77.6285Z" fill="#020302"/>
<path d="M70.1789 198.428C70.1871 198.475 70.1912 198.52 70.1912 198.567C63.4536 197.583 57.2684 193.332 53.9395 187.392C53.3216 186.289 52.7978 185.133 52.3436 183.953C51.9365 182.895 51.3554 181.647 51.2408 180.519C51.7216 180.489 52.6198 181.685 52.9697 182.046C53.5058 182.594 54.0255 183.157 54.5329 183.732C55.0403 184.306 55.4945 184.845 55.961 185.413C56.184 185.685 56.9247 186.962 57.2377 187.017C57.8781 187.13 58.0889 187.744 58.4265 188.182C58.5226 188.251 58.9196 188.198 59.1692 188.3C59.6009 188.476 60.1533 189.393 60.4868 189.743C60.9308 190.207 61.385 190.663 61.8474 191.11C62.7948 192.024 63.7768 192.902 64.7774 193.757C65.7779 194.612 66.8111 195.455 67.8423 196.288C68.4377 196.769 70.0254 197.575 70.1789 198.43V198.428Z" fill="#DC2626"/>
</g>
<defs>
<clipPath id="clip0_3606_765">
<rect width="314.583" height="233" fill="white"/>
</clipPath>
</defs>
</svg>


path: scripts/x402Client.ts
content: import axios, { AxiosInstance, AxiosError } from 'axios';
import { withPaymentInterceptor } from 'x402-axios';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import { getConfig } from './env.js';
import { budgetTracker, getEndpointCost } from './budgets.js';
import { UpstreamHttpError } from './errors.js';
import { getLogger, writeAuditLog, nowMs } from './util.js';
import type { BillingInfo, MetaInfo } from './types.js';

// ============================================================================
// X402 Client Setup
// ============================================================================

let clientInstance: AxiosInstance | null = null;
let paymentAddress: string | null = null;

export function getX402Client(): AxiosInstance {
  if (clientInstance) {
    return clientInstance;
  }

  const config = getConfig();

  // Create account from private key
  const account = privateKeyToAccount(config.PAYMENT_PRIVATE_KEY as `0x${string}`);
  paymentAddress = account.address;

  // Create wallet client with Base mainnet
  const walletClient = createWalletClient({
    account,
    chain: base,
    transport: http(config.BASE_RPC_URL),
  });

  // Create base axios instance
  const baseAxios = axios.create({
    baseURL: config.ELSA_API_URL,
    headers: { 'Content-Type': 'application/json' },
    timeout: 90_000,
  });

  // Wrap with x402 payment interceptor
  clientInstance = withPaymentInterceptor(baseAxios, walletClient as any);
  return clientInstance;
}

export function getPaymentAddress(): string {
  if (!paymentAddress) {
    getX402Client(); // Initialize to get address
  }
  return paymentAddress!;
}

// ============================================================================
// API Call Wrapper with Budget Enforcement
// ============================================================================

export interface ApiCallResult<T> {
  data: T;
  billing: BillingInfo;
  meta: MetaInfo;
}

export async function callElsaApi<T>(
  endpoint: string,
  payload: unknown
): Promise<ApiCallResult<T>> {
  const logger = getLogger();
  const startTime = nowMs();
  const estimatedCost = getEndpointCost(endpoint);

  // Enforce budget before attempting call
  budgetTracker.checkBudget(endpoint);

  logger.debug({ endpoint, payload }, 'Calling Elsa API');

  try {
    const client = getX402Client();
    const response = await client.post<T>(endpoint, payload);

    const latencyMs = nowMs() - startTime;

    // Record successful call
    budgetTracker.recordCall(endpoint, estimatedCost);

    // Build billing info
    const billing: BillingInfo = {
      estimated_cost_usd: estimatedCost,
      payment_required: true,
      receipt: null,
      protocol: 'x402',
    };

    // Build meta info
    const meta: MetaInfo = {
      latency_ms: latencyMs,
      endpoint,
      timestamp: new Date().toISOString(),
    };

    // Write audit log
    writeAuditLog({
      type: 'api_call',
      endpoint,
      estimated_cost_usd: estimatedCost,
      ok: true,
      latency_ms: latencyMs,
    });

    logger.info({ endpoint, latencyMs, estimatedCost }, 'API call successful');

    return {
      data: response.data,
      billing,
      meta,
    };
  } catch (error) {
    const latencyMs = nowMs() - startTime;

    // Write audit log for failed call
    writeAuditLog({
      type: 'api_call',
      endpoint,
      estimated_cost_usd: estimatedCost,
      ok: false,
      latency_ms: latencyMs,
      error: error instanceof Error ? error.message : String(error),
    });

    if (error instanceof AxiosError) {
      const status = error.response?.status ?? 0;
      const statusText = error.response?.statusText ?? 'Unknown';

      logger.error(
        { endpoint, status, statusText, latencyMs },
        'Elsa API request failed'
      );

      throw new UpstreamHttpError(
        `Elsa API error: ${status} ${statusText}`,
        { status, statusText, url: endpoint }
      );
    }

    throw error;
  }
}


path: scripts/elsaClient.ts
content: import { callElsaApi } from './x402Client.js';
import { getLogger } from './util.js';
import type {
  ElsaSearchTokenResponse,
  ElsaTokenPriceResponse,
  ElsaBalancesResponse,
  ElsaPortfolioResponse,
  ElsaAnalyzeWalletResponse,
  ElsaSwapQuoteResponse,
  ElsaExecuteSwapResponse,
  ElsaPipelineStatusResponse,
  ElsaSubmitTxHashResponse,
  ElsaGetLimitOrdersResponse,
  ElsaCreateLimitOrderResponse,
  ElsaCancelLimitOrderResponse,
  ElsaGetPerpPositionsResponse,
  ElsaOpenPerpPositionResponse,
  ElsaClosePerpPositionResponse,
  ElsaGetTransactionHistoryResponse,
} from './types.js';

// ============================================================================
// Elsa API Methods
// ============================================================================

export async function searchToken(query: string, limit: number = 10) {
  return callElsaApi<ElsaSearchTokenResponse>('/api/search_token', {
    symbol_or_address: query,
    limit,
  });
}

export async function getTokenPrice(tokenAddress: string, chain: string = 'base') {
  const logger = getLogger();

  // Try the dedicated price endpoint first
  const result = await callElsaApi<ElsaTokenPriceResponse>('/api/get_token_price', {
    token_address: tokenAddress,
    chain,
  });

  // Check if response has valid price data
  const data = result.data as unknown as Record<string, unknown>;
  const hasPrice = data.price_usd !== undefined && data.price_usd !== null && data.price_usd !== '';
  const hasSymbol = typeof data.symbol === 'string' && data.symbol.length > 0;

  if (hasPrice && hasSymbol) {
    return result;
  }

  // Fallback: Use search_token to get price data
  logger.info({ token_address: tokenAddress, chain }, 'get_token_price returned empty data, falling back to search_token');

  const searchResult = await callElsaApi<ElsaSearchTokenResponse>('/api/search_token', {
    symbol_or_address: tokenAddress,
    limit: 20,
  });

  // Find matching token by address (case-insensitive)
  const searchData = searchResult.data as { results?: Array<{ address: string; symbol: string; name: string; priceUSD?: string; decimals?: number }> };
  const normalizedAddress = tokenAddress.toLowerCase();
  const matchingToken = searchData.results?.find(
    (t) => t.address.toLowerCase() === normalizedAddress
  );

  if (matchingToken) {
    // Merge search data into price response format
    const enrichedData: ElsaTokenPriceResponse = {
      token_address: tokenAddress,
      chain,
      symbol: matchingToken.symbol,
      name: matchingToken.name,
      price_usd: matchingToken.priceUSD || '0',
      decimals: matchingToken.decimals,
      _fallback_used: true,
    };

    return {
      data: enrichedData,
      billing: {
        estimated_cost_usd: result.billing.estimated_cost_usd + searchResult.billing.estimated_cost_usd,
        payment_required: true,
        receipt: null,
        protocol: 'x402',
      },
      meta: {
        ...result.meta,
        fallback_used: true,
      },
    };
  }

  // No match found, return original (empty) result
  logger.warn({ token_address: tokenAddress, chain }, 'Fallback search did not find matching token');
  return result;
}

export async function getBalances(walletAddress: string) {
  return callElsaApi<ElsaBalancesResponse>('/api/get_balances', {
    wallet_address: walletAddress,
  });
}

export async function getPortfolio(walletAddress: string) {
  return callElsaApi<ElsaPortfolioResponse>('/api/get_portfolio', {
    wallet_address: walletAddress,
  });
}

export async function analyzeWallet(walletAddress: string) {
  return callElsaApi<ElsaAnalyzeWalletResponse>('/api/analyze_wallet', {
    wallet_address: walletAddress,
  });
}

export async function getSwapQuote(params: {
  from_chain: string;
  from_token: string;
  from_amount: string;
  to_chain: string;
  to_token: string;
  wallet_address: string;
  slippage: number;
}) {
  return callElsaApi<ElsaSwapQuoteResponse>('/api/get_swap_quote', params);
}

export async function executeSwap(params: {
  from_chain: string;
  from_token: string;
  from_amount: string;
  to_chain: string;
  to_token: string;
  wallet_address: string;
  slippage: number;
  dry_run: boolean;
}) {
  return callElsaApi<ElsaExecuteSwapResponse>('/api/execute_swap', params);
}

export async function getPipelineStatus(pipelineId: string) {
  return callElsaApi<ElsaPipelineStatusResponse>('/api/get_transaction_status', {
    pipeline_id: pipelineId,
  });
}

export async function submitTransactionHash(taskId: string, txHash: string) {
  return callElsaApi<ElsaSubmitTxHashResponse>('/api/submit_transaction_hash', {
    task_id: taskId,
    tx_hash: txHash,
  });
}

// ============================================================================
// Limit Order Methods
// ============================================================================

export async function getLimitOrders(walletAddress: string, chain: string = 'base') {
  return callElsaApi<ElsaGetLimitOrdersResponse>('/api/get_limit_orders', {
    wallet_address: walletAddress,
    chain,
  });
}

export async function createLimitOrder(params: {
  wallet_address: string;
  chain: string;
  from_token: string;
  to_token: string;
  from_amount: string;
  limit_price: string;
  expiry_hours: number;
}) {
  return callElsaApi<ElsaCreateLimitOrderResponse>('/api/create_limit_order', params);
}

export async function cancelLimitOrder(walletAddress: string, orderId: string, chain: string = 'base') {
  return callElsaApi<ElsaCancelLimitOrderResponse>('/api/cancel_limit_order', {
    wallet_address: walletAddress,
    order_id: orderId,
    chain,
  });
}

// ============================================================================
// Perpetuals Methods
// ============================================================================

export async function getPerpPositions(walletAddress: string) {
  return callElsaApi<ElsaGetPerpPositionsResponse>('/api/get_perp_positions', {
    wallet_address: walletAddress,
  });
}

export async function openPerpPosition(params: {
  wallet_address: string;
  market: string;
  side: 'long' | 'short';
  size_usd: string;
  leverage: number;
  take_profit?: string;
  stop_loss?: string;
}) {
  return callElsaApi<ElsaOpenPerpPositionResponse>('/api/open_perp_position', params);
}

export async function closePerpPosition(params: {
  wallet_address: string;
  position_id: string;
  close_percentage: number;
}) {
  return callElsaApi<ElsaClosePerpPositionResponse>('/api/close_perp_position', params);
}

// ============================================================================
// Transaction History Methods
// ============================================================================

export async function getTransactionHistory(walletAddress: string, chain?: string, limit: number = 20) {
  return callElsaApi<ElsaGetTransactionHistoryResponse>('/api/get_transaction_history', {
    wallet_address: walletAddress,
    chain,
    limit,
  });
}


path: scripts/confirm.ts
content: import { getConfig, getConfirmationTtlMs, isConfirmationRequired } from './env.js';
import { ConfirmationInvalidError, ConfirmationRequiredError } from './errors.js';
import { generateToken, hashParams, normalizeSwapParams, nowMs } from './util.js';
import type { ConfirmationTokenData, NormalizedSwapParams } from './types.js';

// ============================================================================
// In-Memory Confirmation Token Store
// ============================================================================

class ConfirmationStore {
  private tokens: Map<string, ConfirmationTokenData> = new Map();

  /**
   * Issue a new confirmation token for swap parameters
   */
  issueToken(params: {
    from_chain: string;
    from_token: string;
    from_amount: string;
    to_chain: string;
    to_token: string;
    wallet_address: string;
    slippage: number;
  }): string {
    // Cleanup expired tokens first
    this.cleanup();

    const normalized = normalizeSwapParams(params);
    const [REDACTED_SECRET]);
    const now = nowMs();
    const ttl = getConfirmationTtlMs();

    this.tokens.set(token, {
      createdAt: now,
      expiresAt: now + ttl,
      normalizedParamsHash: hashParams(normalized),
    });

    return token;
  }

  /**
   * Validate and consume a confirmation token
   * Returns true if valid, throws if invalid
   */
  validateAndConsume(
    [REDACTED_SECRET],
    params: {
      from_chain: string;
      from_token: string;
      from_amount: string;
      to_chain: string;
      to_token: string;
      wallet_address: string;
      slippage: number;
    }
  ): boolean {
    const data = this.tokens.get(token);

    if (!data) {
      throw new ConfirmationInvalidError(
        'Confirmation token not found. Run elsa_execute_swap_dry_run first.',
        { reason: 'not_found' }
      );
    }

    // Check expiry
    if (nowMs() > data.expiresAt) {
      this.tokens.delete(token);
      throw new ConfirmationInvalidError(
        'Confirmation token has expired. Run elsa_execute_swap_dry_run again.',
        { reason: 'expired' }
      );
    }

    // Check params match
    const normalized = normalizeSwapParams(params);
    const expectedHash = hashParams(normalized);

    if (data.normalizedParamsHash !== expectedHash) {
      throw new ConfirmationInvalidError(
        'Swap parameters do not match the dry-run. Run elsa_execute_swap_dry_run with the same parameters.',
        { reason: 'params_mismatch' }
      );
    }

    // One-time use: delete after successful validation
    this.tokens.delete(token);

    return true;
  }

  /**
   * Check if confirmation is required and token is present
   */
  requireConfirmation([REDACTED_SECRET] | undefined, params: {
    from_chain: string;
    from_token: string;
    from_amount: string;
    to_chain: string;
    to_token: string;
    wallet_address: string;
    slippage: number;
  }): void {
    if (!isConfirmationRequired()) {
      return;
    }

    if (!token) {
      throw new ConfirmationRequiredError();
    }

    this.validateAndConsume(token, params);
  }

  /**
   * Remove expired tokens
   */
  private cleanup(): void {
    const now = nowMs();
    for (const [token, data] of this.tokens.entries()) {
      if (now > data.expiresAt) {
        this.tokens.delete(token);
      }
    }
  }

  /**
   * Reset store (for testing)
   */
  reset(): void {
    this.tokens.clear();
  }

  /**
   * Get token count (for testing)
   */
  size(): number {
    return this.tokens.size;
  }

  /**
   * Check if token exists (for testing)
   */
  has([REDACTED_SECRET]): boolean {
    return this.tokens.has(token);
  }
}

// Singleton instance
export const confirmationStore = new ConfirmationStore();


path: scripts/errors.ts
content: // ============================================================================
// Custom Error Classes
// ============================================================================

export class ElsaToolError extends Error {
  constructor(
    public readonly code: string,
    message: string,
    public readonly details?: unknown
  ) {
    super(message);
    this.name = 'ElsaToolError';
  }

  toJSON() {
    return {
      code: this.code,
      message: this.message,
      details: this.details,
    };
  }
}

export class BudgetExceededError extends ElsaToolError {
  constructor(
    message: string,
    details?: { limit: number; requested: number; spent: number }
  ) {
    super('BUDGET_EXCEEDED', message, details);
    this.name = 'BudgetExceededError';
  }
}

export class RateLimitError extends ElsaToolError {
  constructor(
    message: string,
    details?: { limit: number; current: number; reset_in_seconds: number }
  ) {
    super('RATE_LIMIT_EXCEEDED', message, details);
    this.name = 'RateLimitError';
  }
}

export class ExecutionDisabledError extends ElsaToolError {
  constructor(message: string = 'Execution tools are disabled. Set ELSA_ENABLE_EXECUTION_TOOLS=true to enable.') {
    super('EXECUTION_DISABLED', message);
    this.name = 'ExecutionDisabledError';
  }
}

export class ConfirmationRequiredError extends ElsaToolError {
  constructor(message: string = 'Confirmation token required. Run elsa_execute_swap_dry_run first.') {
    super('CONFIRMATION_REQUIRED', message);
    this.name = 'ConfirmationRequiredError';
  }
}

export class ConfirmationInvalidError extends ElsaToolError {
  constructor(
    message: string,
    details?: { reason: 'expired' | 'not_found' | 'params_mismatch' | 'already_used' }
  ) {
    super('CONFIRMATION_INVALID', message, details);
    this.name = 'ConfirmationInvalidError';
  }
}

export class UpstreamHttpError extends ElsaToolError {
  constructor(
    message: string,
    details?: { status: number; statusText: string; url?: string }
  ) {
    super('UPSTREAM_HTTP_ERROR', message, details);
    this.name = 'UpstreamHttpError';
  }
}

export class PipelineTimeoutError extends ElsaToolError {
  constructor(
    message: string,
    details?: { pipeline_id: string; timeout_seconds: number; last_status: string }
  ) {
    super('PIPELINE_TIMEOUT', message, details);
    this.name = 'PipelineTimeoutError';
  }
}

export class PipelineTaskError extends ElsaToolError {
  constructor(
    message: string,
    details?: { task_id: string; status: string; description?: string }
  ) {
    super('PIPELINE_TASK_ERROR', message, details);
    this.name = 'PipelineTaskError';
  }
}

export class ValidationError extends ElsaToolError {
  constructor(message: string, details?: unknown) {
    super('VALIDATION_ERROR', message, details);
    this.name = 'ValidationError';
  }
}

// ============================================================================
// Error Result Builder
// ============================================================================

export function buildErrorResult(error: unknown): {
  ok: false;
  error: { code: string; message: string; details?: unknown };
} {
  if (error instanceof ElsaToolError) {
    return {
      ok: false,
      error: error.toJSON(),
    };
  }

  if (error instanceof Error) {
    return {
      ok: false,
      error: {
        code: 'INTERNAL_ERROR',
        message: error.message,
      },
    };
  }

  return {
    ok: false,
    error: {
      code: 'UNKNOWN_ERROR',
      message: String(error),
    },
  };
}


path: scripts/budgets.ts
content: import { DateTime } from 'luxon';
import { getConfig } from './env.js';
import { BudgetExceededError, RateLimitError } from './errors.js';
import type { BudgetStatus, CallRecord } from './types.js';

// ============================================================================
// Endpoint Cost Table (USD)
// ============================================================================

export const ENDPOINT_COSTS: Record<string, number> = {
  '/api/search_token': 0.001,
  '/api/get_token_price': 0.002,
  '/api/get_balances': 0.005,
  '/api/get_portfolio': 0.01,
  '/api/analyze_wallet': 0.02,
  '/api/get_swap_quote': 0.01,
  '/api/execute_swap': 0.02,
  '/api/get_transaction_status': 0.005,
  '/api/submit_transaction_hash': 0.005,
};

export function getEndpointCost(endpoint: string): number {
  return ENDPOINT_COSTS[endpoint] ?? 0.01; // Default conservative estimate
}

// ============================================================================
// In-Memory Budget Tracker
// ============================================================================

class BudgetTracker {
  private calls: CallRecord[] = [];

  /**
   * Get all calls made today in the configured timezone
   */
  private getCallsToday(): CallRecord[] {
    const config = getConfig();
    const now = DateTime.now().setZone(config.ELSA_TZ);
    const startOfDay = now.startOf('day').toMillis();

    return this.calls.filter((call) => call.timestamp >= startOfDay);
  }

  /**
   * Get calls in the last minute
   */
  private getCallsLastMinute(): CallRecord[] {
    const oneMinuteAgo = Date.now() - 60_000;
    return this.calls.filter((call) => call.timestamp >= oneMinuteAgo);
  }

  /**
   * Clean up old call records (older than 24 hours)
   */
  private cleanup(): void {
    const cutoff = Date.now() - 24 * 60 * 60 * 1000;
    this.calls = this.calls.filter((call) => call.timestamp >= cutoff);
  }

  /**
   * Record a call
   */
  recordCall(endpoint: string, cost_usd: number): void {
    this.calls.push({
      timestamp: Date.now(),
      endpoint,
      cost_usd,
    });
    this.cleanup();
  }

  /**
   * Check if a call can proceed without exceeding budgets
   */
  checkBudget(endpoint: string): void {
    const config = getConfig();
    const estimatedCost = getEndpointCost(endpoint);

    // Check per-call limit
    if (estimatedCost > config.ELSA_MAX_USD_PER_CALL) {
      throw new BudgetExceededError(
        `Estimated cost $${estimatedCost.toFixed(4)} exceeds per-call limit of $${config.ELSA_MAX_USD_PER_CALL.toFixed(2)}`,
        {
          limit: config.ELSA_MAX_USD_PER_CALL,
          requested: estimatedCost,
          spent: 0,
        }
      );
    }

    // Check daily limit
    const spentToday = this.getSpentToday();
    if (spentToday + estimatedCost > config.ELSA_MAX_USD_PER_DAY) {
      throw new BudgetExceededError(
        `Daily budget limit reached. Spent: $${spentToday.toFixed(4)}, Limit: $${config.ELSA_MAX_USD_PER_DAY.toFixed(2)}`,
        {
          limit: config.ELSA_MAX_USD_PER_DAY,
          requested: estimatedCost,
          spent: spentToday,
        }
      );
    }

    // Check rate limit
    const callsLastMinute = this.getCallsLastMinute().length;
    if (callsLastMinute >= config.ELSA_MAX_CALLS_PER_MINUTE) {
      throw new RateLimitError(
        `Rate limit exceeded: ${callsLastMinute}/${config.ELSA_MAX_CALLS_PER_MINUTE} calls in last minute`,
        {
          limit: config.ELSA_MAX_CALLS_PER_MINUTE,
          current: callsLastMinute,
          reset_in_seconds: 60,
        }
      );
    }
  }

  /**
   * Get total spent today
   */
  getSpentToday(): number {
    return this.getCallsToday().reduce((sum, call) => sum + call.cost_usd, 0);
  }

  /**
   * Get current budget status
   */
  getStatus(): BudgetStatus {
    const config = getConfig();
    const callsToday = this.getCallsToday();
    const callsLastMinute = this.getCallsLastMinute();
    const spentToday = callsToday.reduce((sum, call) => sum + call.cost_usd, 0);

    return {
      spent_today_usd: spentToday,
      remaining_today_usd: Math.max(0, config.ELSA_MAX_USD_PER_DAY - spentToday),
      calls_last_minute: callsLastMinute.length,
      last_calls: callsLastMinute.slice(-10), // Last 10 calls
    };
  }

  /**
   * Reset tracker (for testing)
   */
  reset(): void {
    this.calls = [];
  }

  /**
   * Set calls directly (for testing)
   */
  setCalls(calls: CallRecord[]): void {
    this.calls = calls;
  }
}

// Singleton instance
export const budgetTracker = new BudgetTracker();


path: scripts/pipeline.ts
content: import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import { getConfig } from './env.js';
import { getPipelineStatus, submitTransactionHash } from './elsaClient.js';
import { PipelineTimeoutError, PipelineTaskError } from './errors.js';
import { getLogger, writeAuditLog, sleep, mapTransactionData, nowMs } from './util.js';
import type { PipelineRunResult, PipelineTask, TransactionData, MetaInfo } from './types.js';

// ============================================================================
// Trade Wallet Client
// ============================================================================

function getTradeWalletClient() {
  const config = getConfig();
  const privateKey = config.TRADE_PRIVATE_KEY ?? config.PAYMENT_PRIVATE_KEY;
  const account = privateKeyToAccount(privateKey as `0x${string}`);

  return createWalletClient({
    account,
    chain: base,
    transport: http(config.BASE_RPC_URL),
  });
}

// ============================================================================
// Pipeline Executor
// ============================================================================

export async function runPipelineAndWait(params: {
  pipeline_id: string;
  timeout_seconds: number;
  poll_interval_seconds: number;
  mode: 'local_signer' | 'external_signer';
}): Promise<PipelineRunResult> {
  const logger = getLogger();
  const startTime = nowMs();
  const timeoutMs = params.timeout_seconds * 1000;
  const pollIntervalMs = params.poll_interval_seconds * 1000;

  const txHashes: string[] = [];
  const taskResults: Array<{
    task_id: string;
    status: string;
    tx_hash?: string;
    tx_data?: TransactionData;
  }> = [];

  logger.info({ pipeline_id: params.pipeline_id, mode: params.mode }, 'Starting pipeline execution');

  while (true) {
    // Check timeout
    if (nowMs() - startTime > timeoutMs) {
      logger.warn({ pipeline_id: params.pipeline_id }, 'Pipeline timeout reached');
      throw new PipelineTimeoutError(
        'Pipeline execution timed out',
        {
          pipeline_id: params.pipeline_id,
          timeout_seconds: params.timeout_seconds,
          last_status: taskResults.length > 0 ? taskResults[taskResults.length - 1].status : 'unknown',
        }
      );
    }

    // Poll for status
    const statusResult = await getPipelineStatus(params.pipeline_id);
    const pipelineStatus = statusResult.data;

    logger.debug({ pipeline_id: params.pipeline_id, status: pipelineStatus.status }, 'Pipeline status');

    // Process tasks - API returns tasks as 'status' array
    const tasks = pipelineStatus.status || [];
    for (const task of tasks) {
      const existingResult = taskResults.find((r) => r.task_id === task.task_id);

      if (task.status === 'sign_pending') {
        // Task needs signing - use evm_tx_data from API
        const rawTxData = task.evm_tx_data || task.tx_data;

        if (params.mode === 'external_signer') {
          // Return for external signing
          if (!existingResult || existingResult.status !== 'sign_pending') {
            const txData = rawTxData ? mapTransactionData(rawTxData) : undefined;
            taskResults.push({
              task_id: task.task_id,
              status: 'sign_pending',
              tx_data: txData,
            });
          }
        } else {
          // Local signing
          if (!rawTxData) {
            throw new PipelineTaskError(
              'Task requires signing but no tx_data provided',
              { task_id: task.task_id, status: task.status }
            );
          }

          logger.info({ task_id: task.task_id }, 'Signing and broadcasting transaction');

          try {
            const txData = mapTransactionData(rawTxData);
            const walletClient = getTradeWalletClient();

            // Broadcast transaction
            const txHash = await walletClient.sendTransaction({
              to: txData.to as `0x${string}`,
              data: txData.data as `0x${string}`,
              value: txData.value ? BigInt(txData.value) : undefined,
              gas: txData.gas ? BigInt(txData.gas) : undefined,
              maxFeePerGas: txData.maxFeePerGas ? BigInt(txData.maxFeePerGas) : undefined,
              maxPriorityFeePerGas: txData.maxPriorityFeePerGas ? BigInt(txData.maxPriorityFeePerGas) : undefined,
            });

            logger.info({ task_id: task.task_id, tx_hash: txHash }, 'Transaction broadcast');
            txHashes.push(txHash);

            // Write audit log for signed transaction
            writeAuditLog({
              type: 'pipeline_tx_signed',
              pipeline_id: params.pipeline_id,
              task_id: task.task_id,
              tx_hash: txHash,
            });

            // Submit hash to Elsa
            await submitTransactionHash(task.task_id, txHash);

            // Update task result
            const idx = taskResults.findIndex((r) => r.task_id === task.task_id);
            if (idx >= 0) {
              taskResults[idx] = { task_id: task.task_id, status: 'submitted', tx_hash: txHash };
            } else {
              taskResults.push({ task_id: task.task_id, status: 'submitted', tx_hash: txHash });
            }
          } catch (error) {
            logger.error({ task_id: task.task_id, error }, 'Failed to sign/broadcast transaction');
            throw new PipelineTaskError(
              `Failed to sign transaction: ${error instanceof Error ? error.message : String(error)}`,
              { task_id: task.task_id, status: task.status }
            );
          }
        }
      } else if (task.status === 'success') {
        // Task completed
        const idx = taskResults.findIndex((r) => r.task_id === task.task_id);
        if (idx >= 0) {
          taskResults[idx].status = 'success';
          if (task.tx_hash) taskResults[idx].tx_hash = task.tx_hash;
        } else {
          taskResults.push({
            task_id: task.task_id,
            status: 'success',
            tx_hash: task.tx_hash,
          });
        }
        if (task.tx_hash && !txHashes.includes(task.tx_hash)) {
          txHashes.push(task.tx_hash);
        }
      } else if (task.status === 'failed' || task.status === 'abandoned') {
        throw new PipelineTaskError(
          `Task failed: ${task.description || task.status}`,
          { task_id: task.task_id, status: task.status, description: task.description }
        );
      }
    }

    // Check for external signer mode with pending tasks
    if (params.mode === 'external_signer') {
      const pendingTasks = taskResults.filter((t) => t.status === 'sign_pending');
      if (pendingTasks.length > 0) {
        const meta: MetaInfo = {
          latency_ms: nowMs() - startTime,
          endpoint: '/api/get_transaction_status',
          timestamp: new Date().toISOString(),
        };

        return {
          ok: true,
          status: 'needs_external_signature',
          pipeline_id: params.pipeline_id,
          tasks: taskResults,
          tx_hashes: txHashes,
          meta,
        };
      }
    }

    // Check for terminal states - derive from task statuses
    const allTasksComplete = tasks.length > 0 && tasks.every(t => t.status === 'success');
    const anyTaskFailed = tasks.some(t => t.status === 'failed' || t.status === 'abandoned');

    if (allTasksComplete) {
      const meta: MetaInfo = {
        latency_ms: nowMs() - startTime,
        endpoint: '/api/get_transaction_status',
        timestamp: new Date().toISOString(),
      };

      return {
        ok: true,
        status: 'success',
        pipeline_id: params.pipeline_id,
        tasks: taskResults,
        tx_hashes: txHashes,
        meta,
      };
    }

    if (anyTaskFailed) {
      const meta: MetaInfo = {
        latency_ms: nowMs() - startTime,
        endpoint: '/api/get_transaction_status',
        timestamp: new Date().toISOString(),
      };

      return {
        ok: true,
        status: 'failed',
        pipeline_id: params.pipeline_id,
        tasks: taskResults,
        tx_hashes: txHashes,
        meta,
      };
    }

    // Wait before next poll
    await sleep(pollIntervalMs);
  }
}


path: scripts/types.ts
content: import { z } from 'zod';

// ============================================================================
// Input Schemas
// ============================================================================

export const SearchTokenInputSchema = z.object({
  query: z.string().min(1).max(200),
  limit: z.number().int().min(1).max(100).optional().default(10),
});

export const GetTokenPriceInputSchema = z.object({
  token_address: z.string().min(1),
  chain: z.string().optional().default('base'),
});

export const GetBalancesInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
});

export const GetPortfolioInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
});

export const AnalyzeWalletInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
});

export const SwapParamsSchema = z.object({
  from_chain: z.string().min(1),
  from_token: z.string().min(1),
  from_amount: z.string().min(1),
  to_chain: z.string().min(1),
  to_token: z.string().min(1),
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  slippage: z.number().min(0).max(50),
});

export const GetSwapQuoteInputSchema = SwapParamsSchema;

export const ExecuteSwapDryRunInputSchema = SwapParamsSchema.extend({
  dry_run: z.literal(true).optional(),
});

export const ExecuteSwapConfirmedInputSchema = SwapParamsSchema.extend({
  confirmation_token: z.string().optional(),
});

export const PipelineGetStatusInputSchema = z.object({
  pipeline_id: z.string().min(1),
});

export const PipelineSubmitTxHashInputSchema = z.object({
  task_id: z.string().min(1),
  tx_hash: z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'Invalid transaction hash'),
});

export const PipelineRunAndWaitInputSchema = z.object({
  pipeline_id: z.string().min(1),
  timeout_seconds: z.number().int().min(10).max(600).optional().default(120),
  poll_interval_seconds: z.number().int().min(1).max(30).optional().default(2),
  mode: z.enum(['local_signer', 'external_signer']).optional().default('local_signer'),
});

export const BudgetStatusInputSchema = z.object({});

// ============================================================================
// Limit Order Input Schemas
// ============================================================================

export const GetLimitOrdersInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  chain: z.string().optional().default('base'),
});

export const CreateLimitOrderInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  chain: z.string().min(1),
  from_token: z.string().min(1),
  to_token: z.string().min(1),
  from_amount: z.string().min(1),
  limit_price: z.string().min(1),
  expiry_hours: z.number().int().min(1).max(720).optional().default(24),
});

export const CancelLimitOrderInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  order_id: z.string().min(1),
  chain: z.string().optional().default('base'),
});

// ============================================================================
// Perpetuals Input Schemas
// ============================================================================

export const GetPerpPositionsInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
});

export const OpenPerpPositionInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  market: z.string().min(1),
  side: z.enum(['long', 'short']),
  size_usd: z.string().min(1),
  leverage: z.number().min(1).max(100).optional().default(1),
  take_profit: z.string().optional(),
  stop_loss: z.string().optional(),
});

export const ClosePerpPositionInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  position_id: z.string().min(1),
  close_percentage: z.number().min(1).max(100).optional().default(100),
});

// ============================================================================
// Transaction History Input Schema
// ============================================================================

export const GetTransactionHistoryInputSchema = z.object({
  wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
  chain: z.string().optional(),
  limit: z.number().int().min(1).max(100).optional().default(20),
});

// ============================================================================
// Output Types
// ============================================================================

export interface BillingInfo {
  estimated_cost_usd: number;
  payment_required: boolean;
  receipt: string | null;
  protocol: string;
}

export interface MetaInfo {
  latency_ms: number;
  endpoint: string;
  timestamp: string;
}

export interface ToolSuccess<T = unknown> {
  ok: true;
  data: T;
  billing: BillingInfo;
  meta: MetaInfo;
}

export interface ToolError {
  ok: false;
  error: {
    code: string;
    message: string;
    details?: unknown;
  };
}

export type ToolResult<T = unknown> = ToolSuccess<T> | ToolError;

// ============================================================================
// Pipeline Types
// ============================================================================

export type PipelineTaskStatus =
  | 'pending'
  | 'running'
  | 'sign_pending'
  | 'deferred'
  | 'success'
  | 'failed'
  | 'abandoned';

export interface EvmTxData {
  chain_id: string;
  to: string;
  value: string;
  data: string;
  gas: string;
  from: string;
}

export interface PipelineTask {
  task_id: string;
  action_type: string;
  status: PipelineTaskStatus;
  description?: string;
  task_description?: string;
  short_description?: string;
  tx_hash?: string;
  tx_data?: TransactionData;
  evm_tx_data?: EvmTxData;
  log?: string | null;
}

export interface TransactionData {
  to: string;
  data: string;
  value?: string;
  gas?: string;
  maxFeePerGas?: string;
  maxPriorityFeePerGas?: string;
  nonce?: number;
  chainId?: number;
}

export interface PipelineRunResult {
  ok: true;
  status: 'success' | 'failed' | 'timeout' | 'needs_external_signature' | 'in_progress';
  pipeline_id: string;
  tasks: Array<{
    task_id: string;
    status: string;
    tx_hash?: string;
    tx_data?: TransactionData;
  }>;
  tx_hashes: string[];
  meta: MetaInfo;
}

// ============================================================================
// Confirmation Token Types
// ============================================================================

export interface ConfirmationTokenData {
  createdAt: number;
  expiresAt: number;
  normalizedParamsHash: string;
}

export interface NormalizedSwapParams {
  from_chain: string;
  from_token: string;
  from_amount: string;
  to_chain: string;
  to_token: string;
  wallet_address: string;
  slippage: number;
}

// ============================================================================
// Budget Types
// ============================================================================

export interface CallRecord {
  timestamp: number;
  endpoint: string;
  cost_usd: number;
}

export interface BudgetStatus {
  spent_today_usd: number;
  remaining_today_usd: number;
  calls_last_minute: number;
  last_calls: CallRecord[];
}

// ============================================================================
// Tool Handler Types
// ============================================================================

export interface ToolHandler {
  validate: (args: unknown) => string | null;
  run: (args: unknown) => Promise<unknown>;
}

export type ToolRegistry = Record<string, ToolHandler>;

// ============================================================================
// Elsa API Response Types
// ============================================================================

export interface ElsaSearchTokenResponse {
  tokens: Array<{
    name: string;
    symbol: string;
    address: string;
    chain: string;
    decimals: number;
    price_usd?: string;
    logo_url?: string;
  }>;
}

export interface ElsaTokenPriceResponse {
  token_address: string;
  chain: string;
  symbol?: string;
  name?: string;
  price_usd: string;
  decimals?: number;
  price_change_24h?: number;
  market_cap?: string;
  volume_24h?: string;
  _fallback_used?: boolean;
}

export interface ElsaBalancesResponse {
  balances: Array<{
    chain: string;
    token_address: string;
    symbol: string;
    balance: string;
    balance_usd: string;
    decimals: number;
  }>;
  total_usd: string;
}

export interface ElsaPortfolioResponse {
  wallet_address: string;
  total_value_usd: string;
  chains: Array<{
    chain: string;
    value_usd: string;
    tokens: Array<{
      symbol: string;
      address: string;
      balance: string;
      value_usd: string;
    }>;
  }>;
}

export interface ElsaAnalyzeWalletResponse {
  wallet_address: string;
  risk_score: number;
  activity_summary: {
    total_transactions: number;
    first_seen: string;
    last_active: string;
  };
  labels: string[];
}

export interface ElsaSwapQuoteResponse {
  quote_id: string;
  from_chain: string;
  from_token: string;
  from_amount: string;
  from_amount_usd: string;
  to_chain: string;
  to_token: string;
  to_amount: string;
  to_amount_usd: string;
  to_amount_min: string;
  price_impact: string;
  gas_estimate_usd: string;
  route: string[];
}

export interface ElsaExecuteSwapResponse {
  pipeline_id: string;
  status: string;
  tasks?: PipelineTask[];
  message?: string;
}

export interface ElsaPipelineStatusResponse {
  pipeline_id: string;
  status: PipelineTask[];  // API returns tasks as 'status' array
  timestamp?: string;
}

export interface ElsaSubmitTxHashResponse {
  task_id: string;
  status: string;
  message?: string;
}

// ============================================================================
// Limit Order Response Types
// ============================================================================

export interface LimitOrder {
  order_id: string;
  chain: string;
  from_token: string;
  from_token_symbol?: string;
  to_token: string;
  to_token_symbol?: string;
  from_amount: string;
  limit_price: string;
  current_price?: string;
  status: 'open' | 'filled' | 'cancelled' | 'expired';
  created_at: string;
  expires_at?: string;
  filled_at?: string;
  tx_hash?: string;
}

export interface ElsaGetLimitOrdersResponse {
  wallet_address: string;
  orders: LimitOrder[];
}

export interface ElsaCreateLimitOrderResponse {
  order_id: string;
  status: string;
  message?: string;
  pipeline_id?: string;
}

export interface ElsaCancelLimitOrderResponse {
  order_id: string;
  status: string;
  message?: string;
}

// ============================================================================
// Perpetuals Response Types
// ============================================================================

export interface PerpPosition {
  position_id: string;
  market: string;
  side: 'long' | 'short';
  size: string;
  size_usd: string;
  entry_price: string;
  mark_price: string;
  liquidation_price: string;
  leverage: number;
  unrealized_pnl: string;
  unrealized_pnl_percentage: string;
  margin: string;
  take_profit?: string;
  stop_loss?: string;
  created_at: string;
  platform: string;
}

export interface ElsaGetPerpPositionsResponse {
  wallet_address: string;
  positions: PerpPosition[];
  total_unrealized_pnl: string;
  total_margin: string;
}

export interface ElsaOpenPerpPositionResponse {
  position_id: string;
  status: string;
  message?: string;
  pipeline_id?: string;
}

export interface ElsaClosePerpPositionResponse {
  position_id: string;
  status: string;
  realized_pnl?: string;
  message?: string;
  pipeline_id?: string;
}

// ============================================================================
// Transaction History Response Types
// ============================================================================

export interface Transaction {
  tx_hash: string;
  chain: string;
  block_number: number;
  timestamp: string;
  from_address: string;
  to_address: string;
  value: string;
  gas_used: string;
  gas_price: string;
  status: 'success' | 'failed' | 'pending';
  type: string;
  method?: string;
  token_transfers?: Array<{
    token_address: string;
    token_symbol: string;
    from: string;
    to: string;
    amount: string;
    amount_usd?: string;
  }>;
}

export interface ElsaGetTransactionHistoryResponse {
  wallet_address: string;
  transactions: Transaction[];
  total_count?: number;
}


path: scripts/util.ts
content: import { createHash, randomBytes } from 'crypto';
import { pino, Logger } from 'pino';
import { appendFileSync, existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';
import { getConfig } from './env.js';
import type { NormalizedSwapParams, TransactionData } from './types.js';

// ============================================================================
// Logger Setup
// ============================================================================

let loggerInstance: Logger | null = null;

export function getLogger(): Logger {
  if (loggerInstance) {
    return loggerInstance;
  }

  const config = getConfig();

  loggerInstance = pino({
    level: config.LOG_LEVEL,
  });

  return loggerInstance;
}

// ============================================================================
// Audit Logging
// ============================================================================

export function writeAuditLog(entry: Record<string, unknown>): void {
  const config = getConfig();

  if (!config.ELSA_AUDIT_LOG_PATH) {
    return;
  }

  try {
    const dir = dirname(config.ELSA_AUDIT_LOG_PATH);
    if (!existsSync(dir)) {
      mkdirSync(dir, { recursive: true });
    }

    const line = JSON.stringify({
      ...entry,
      timestamp: new Date().toISOString(),
    }) + '\n';

    appendFileSync(config.ELSA_AUDIT_LOG_PATH, line, 'utf8');
  } catch (error) {
    getLogger().error({ error }, 'Failed to write audit log');
  }
}

// ============================================================================
// Crypto Utilities
// ============================================================================

export function generateToken(): string {
  const bytes = randomBytes(32);
  return bytes.toString('base64url');
}

export function hashParams(params: NormalizedSwapParams): string {
  const json = JSON.stringify(params);
  return createHash('sha256').update(json).digest('hex');
}

// ============================================================================
// Parameter Normalization
// ============================================================================

export function normalizeSwapParams(params: {
  from_chain: string;
  from_token: string;
  from_amount: string;
  to_chain: string;
  to_token: string;
  wallet_address: string;
  slippage: number;
}): NormalizedSwapParams {
  return {
    from_chain: params.from_chain.trim().toLowerCase(),
    from_token: params.from_token.trim().toLowerCase(),
    from_amount: params.from_amount.trim(),
    to_chain: params.to_chain.trim().toLowerCase(),
    to_token: params.to_token.trim().toLowerCase(),
    wallet_address: params.wallet_address.trim().toLowerCase(),
    slippage: params.slippage,
  };
}

// ============================================================================
// Transaction Data Mapping
// ============================================================================

export function mapTransactionData(raw: unknown): TransactionData {
  if (!raw || typeof raw !== 'object') {
    throw new Error('Invalid transaction data: expected object');
  }

  const data = raw as Record<string, unknown>;

  if (!data.to || typeof data.to !== 'string') {
    throw new Error('Invalid transaction data: missing "to" field');
  }

  if (!data.data || typeof data.data !== 'string') {
    throw new Error('Invalid transaction data: missing "data" field');
  }

  const result: TransactionData = {
    to: data.to,
    data: data.data,
  };

  // Optional fields with type coercion
  if (data.value !== undefined) {
    result.value = String(data.value);
  }

  if (data.gas !== undefined) {
    result.gas = String(data.gas);
  }

  if (data.maxFeePerGas !== undefined) {
    result.maxFeePerGas = String(data.maxFeePerGas);
  }

  if (data.maxPriorityFeePerGas !== undefined) {
    result.maxPriorityFeePerGas = String(data.maxPriorityFeePerGas);
  }

  if (data.nonce !== undefined) {
    result.nonce = typeof data.nonce === 'number'
      ? data.nonce
      : parseInt(String(data.nonce), 10);
  }

  if (data.chainId !== undefined) {
    result.chainId = typeof data.chainId === 'number'
      ? data.chainId
      : parseInt(String(data.chainId), 10);
  }

  return result;
}

// ============================================================================
// Timing Utilities
// ============================================================================

export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export function nowMs(): number {
  return Date.now();
}

// ============================================================================
// Output Helpers
// ============================================================================

export function out(data: unknown): void {
  console.log(JSON.stringify(data, null, 2));
}

export function exitWithError(message: string, code: number = 1): never {
  out({
    ok: false,
    error: {
      code: 'CLI_ERROR',
      message,
    },
  });
  process.exit(code);
}


path: scripts/index.ts
content: #!/usr/bin/env node

import 'dotenv/config';
import { ZodError } from 'zod';
import { getConfig, isExecutionEnabled, isConfirmationRequired } from './env.js';
import { buildErrorResult, ExecutionDisabledError, ValidationError } from './errors.js';
import { budgetTracker } from './budgets.js';
import { confirmationStore } from './confirm.js';
import { runPipelineAndWait } from './pipeline.js';
import { out, exitWithError, getLogger } from './util.js';
import * as elsa from './elsaClient.js';
import {
  SearchTokenInputSchema,
  GetTokenPriceInputSchema,
  GetBalancesInputSchema,
  GetPortfolioInputSchema,
  AnalyzeWalletInputSchema,
  GetSwapQuoteInputSchema,
  ExecuteSwapDryRunInputSchema,
  ExecuteSwapConfirmedInputSchema,
  PipelineGetStatusInputSchema,
  PipelineSubmitTxHashInputSchema,
  PipelineRunAndWaitInputSchema,
  BudgetStatusInputSchema,
  GetLimitOrdersInputSchema,
  CreateLimitOrderInputSchema,
  CancelLimitOrderInputSchema,
  GetPerpPositionsInputSchema,
  OpenPerpPositionInputSchema,
  ClosePerpPositionInputSchema,
  GetTransactionHistoryInputSchema,
} from './types.js';
import type { ToolHandler, ToolRegistry, ToolResult, BudgetStatus } from './types.js';

// ============================================================================
// Tool Handlers
// ============================================================================

const MVP_TOOLS: ToolRegistry = {
  // -------------------------------------------------------------------------
  // A) elsa_search_token
  // -------------------------------------------------------------------------
  elsa_search_token: {
    validate: (args) => {
      const result = SearchTokenInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = SearchTokenInputSchema.parse(args);
      const result = await elsa.searchToken(input.query, input.limit);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // B) elsa_get_token_price
  // -------------------------------------------------------------------------
  elsa_get_token_price: {
    validate: (args) => {
      const result = GetTokenPriceInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetTokenPriceInputSchema.parse(args);
      const result = await elsa.getTokenPrice(input.token_address, input.chain);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // C) elsa_get_balances
  // -------------------------------------------------------------------------
  elsa_get_balances: {
    validate: (args) => {
      const result = GetBalancesInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetBalancesInputSchema.parse(args);
      const result = await elsa.getBalances(input.wallet_address);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // D) elsa_get_portfolio
  // -------------------------------------------------------------------------
  elsa_get_portfolio: {
    validate: (args) => {
      const result = GetPortfolioInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetPortfolioInputSchema.parse(args);
      const result = await elsa.getPortfolio(input.wallet_address);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // E) elsa_analyze_wallet
  // -------------------------------------------------------------------------
  elsa_analyze_wallet: {
    validate: (args) => {
      const result = AnalyzeWalletInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = AnalyzeWalletInputSchema.parse(args);
      const result = await elsa.analyzeWallet(input.wallet_address);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // F) elsa_get_swap_quote
  // -------------------------------------------------------------------------
  elsa_get_swap_quote: {
    validate: (args) => {
      const result = GetSwapQuoteInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetSwapQuoteInputSchema.parse(args);
      const result = await elsa.getSwapQuote(input);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // G) elsa_execute_swap_dry_run
  // -------------------------------------------------------------------------
  elsa_execute_swap_dry_run: {
    validate: (args) => {
      const result = ExecuteSwapDryRunInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = ExecuteSwapDryRunInputSchema.parse(args);

      // Always force dry_run=true
      const result = await elsa.executeSwap({
        ...input,
        dry_run: true,
      });

      const response: Record<string, unknown> = {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };

      // Issue confirmation token if enabled and response indicates valid swap plan
      if (isConfirmationRequired() && result.data.pipeline_id) {
        const confirmationToken = confirmationStore.issueToken(input);
        response.confirmation_token = confirmationToken;
        response.confirmation_expires_in_seconds = getConfig().ELSA_CONFIRMATION_TTL_SECONDS;
      }

      return response;
    },
  },

  // -------------------------------------------------------------------------
  // H) elsa_budget_status
  // -------------------------------------------------------------------------
  elsa_budget_status: {
    validate: (args) => {
      const result = BudgetStatusInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (_args) => {
      const status = budgetTracker.getStatus();
      return {
        ok: true,
        data: status,
      };
    },
  },

  // -------------------------------------------------------------------------
  // I) elsa_get_limit_orders
  // -------------------------------------------------------------------------
  elsa_get_limit_orders: {
    validate: (args) => {
      const result = GetLimitOrdersInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetLimitOrdersInputSchema.parse(args);
      const result = await elsa.getLimitOrders(input.wallet_address, input.chain);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // J) elsa_get_perp_positions
  // -------------------------------------------------------------------------
  elsa_get_perp_positions: {
    validate: (args) => {
      const result = GetPerpPositionsInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetPerpPositionsInputSchema.parse(args);
      const result = await elsa.getPerpPositions(input.wallet_address);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // K) elsa_get_transaction_history
  // -------------------------------------------------------------------------
  elsa_get_transaction_history: {
    validate: (args) => {
      const result = GetTransactionHistoryInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = GetTransactionHistoryInputSchema.parse(args);
      const result = await elsa.getTransactionHistory(input.wallet_address, input.chain, input.limit);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },
};

// ============================================================================
// Execution Tools (only registered if ELSA_ENABLE_EXECUTION_TOOLS=true)
// ============================================================================

const EXECUTION_TOOLS: ToolRegistry = {
  // -------------------------------------------------------------------------
  // I) elsa_execute_swap_confirmed
  // -------------------------------------------------------------------------
  elsa_execute_swap_confirmed: {
    validate: (args) => {
      const result = ExecuteSwapConfirmedInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = ExecuteSwapConfirmedInputSchema.parse(args);

      // Validate confirmation token if required
      confirmationStore.requireConfirmation(input.confirmation_token, {
        from_chain: input.from_chain,
        from_token: input.from_token,
        from_amount: input.from_amount,
        to_chain: input.to_chain,
        to_token: input.to_token,
        wallet_address: input.wallet_address,
        slippage: input.slippage,
      });

      const result = await elsa.executeSwap({
        from_chain: input.from_chain,
        from_token: input.from_token,
        from_amount: input.from_amount,
        to_chain: input.to_chain,
        to_token: input.to_token,
        wallet_address: input.wallet_address,
        slippage: input.slippage,
        dry_run: false,
      });

      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // J) elsa_pipeline_get_status
  // -------------------------------------------------------------------------
  elsa_pipeline_get_status: {
    validate: (args) => {
      const result = PipelineGetStatusInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = PipelineGetStatusInputSchema.parse(args);
      const result = await elsa.getPipelineStatus(input.pipeline_id);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // K) elsa_pipeline_submit_tx_hash
  // -------------------------------------------------------------------------
  elsa_pipeline_submit_tx_hash: {
    validate: (args) => {
      const result = PipelineSubmitTxHashInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = PipelineSubmitTxHashInputSchema.parse(args);
      const result = await elsa.submitTransactionHash(input.task_id, input.tx_hash);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // L) elsa_pipeline_run_and_wait
  // -------------------------------------------------------------------------
  elsa_pipeline_run_and_wait: {
    validate: (args) => {
      const result = PipelineRunAndWaitInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = PipelineRunAndWaitInputSchema.parse(args);
      return runPipelineAndWait(input);
    },
  },

  // -------------------------------------------------------------------------
  // M) elsa_create_limit_order
  // -------------------------------------------------------------------------
  elsa_create_limit_order: {
    validate: (args) => {
      const result = CreateLimitOrderInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = CreateLimitOrderInputSchema.parse(args);
      const result = await elsa.createLimitOrder(input);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // N) elsa_cancel_limit_order
  // -------------------------------------------------------------------------
  elsa_cancel_limit_order: {
    validate: (args) => {
      const result = CancelLimitOrderInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = CancelLimitOrderInputSchema.parse(args);
      const result = await elsa.cancelLimitOrder(input.wallet_address, input.order_id, input.chain);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // O) elsa_open_perp_position
  // -------------------------------------------------------------------------
  elsa_open_perp_position: {
    validate: (args) => {
      const result = OpenPerpPositionInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = OpenPerpPositionInputSchema.parse(args);
      const result = await elsa.openPerpPosition(input);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },

  // -------------------------------------------------------------------------
  // P) elsa_close_perp_position
  // -------------------------------------------------------------------------
  elsa_close_perp_position: {
    validate: (args) => {
      const result = ClosePerpPositionInputSchema.safeParse(args);
      if (!result.success) {
        return result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      }
      return null;
    },
    run: async (args) => {
      const input = ClosePerpPositionInputSchema.parse(args);
      const result = await elsa.closePerpPosition(input);
      return {
        ok: true,
        data: result.data,
        billing: result.billing,
        meta: result.meta,
      };
    },
  },
};

// ============================================================================
// Build Final Tool Registry
// ============================================================================

function buildToolRegistry(): ToolRegistry {
  const tools: ToolRegistry = { ...MVP_TOOLS };

  if (isExecutionEnabled()) {
    Object.assign(tools, EXECUTION_TOOLS);
  }

  return tools;
}

// ============================================================================
// CLI Main
// ============================================================================

async function main() {
  const args = process.argv.slice(2);

  if (args.length < 1) {
    exitWithError('Usage: npx tsx scripts/index.ts <tool_name> [json_args]');
  }

  const toolName = args[0];
  const rawArgs = args[1] || '{}';

  // Initialize config (validates env)
  try {
    getConfig();
  } catch (error) {
    exitWithError(error instanceof Error ? error.message : String(error));
  }

  const logger = getLogger();
  const TOOLS = buildToolRegistry();

  // Check if tool exists
  if (!TOOLS[toolName]) {
    // Check if it's an execution tool that's disabled
    if (EXECUTION_TOOLS[toolName] && !isExecutionEnabled()) {
      out(buildErrorResult(new ExecutionDisabledError()));
      process.exit(1);
    }

    exitWithError(`Unknown tool: ${toolName}. Available: ${Object.keys(TOOLS).join(', ')}`);
  }

  // Parse arguments
  let parsedArgs: unknown;
  try {
    parsedArgs = JSON.parse(rawArgs);
  } catch (error) {
    out(buildErrorResult(new ValidationError('Invalid JSON arguments', { raw: rawArgs })));
    process.exit(1);
  }

  const tool = TOOLS[toolName];

  // Validate arguments
  const validationError = tool.validate(parsedArgs);
  if (validationError) {
    out(buildErrorResult(new ValidationError(validationError)));
    process.exit(1);
  }

  // Execute tool
  try {
    logger.debug({ tool: toolName, args: parsedArgs }, 'Executing tool');
    const result = await tool.run(parsedArgs);
    out(result);
  } catch (error) {
    logger.error({ tool: toolName, error }, 'Tool execution failed');
    out(buildErrorResult(error));
    process.exit(1);
  }
}

main();


path: scripts/env.ts
content: import { z } from 'zod';

// ============================================================================
// Environment Variable Schema
// ============================================================================

const EnvSchema = z.object({
  // API Configuration
  ELSA_API_URL: z
    .string()
    .url()
    .default('https://x402-api.heyelsa.ai'),
  BASE_RPC_URL: z
    .string()
    .url()
    .default('https://mainnet.base.org'),

  // Wallet Keys
  PAYMENT_PRIVATE_KEY: z
    .string()
    .regex(/^0x[a-fA-F0-9]{64}$/, 'PAYMENT_PRIVATE_KEY must be a valid hex private key with 0x prefix'),
  TRADE_PRIVATE_KEY: z
    .string()
    .regex(/^0x[a-fA-F0-9]{64}$/, 'TRADE_PRIVATE_KEY must be a valid hex private key with 0x prefix')
    .optional(),

  // Budget Configuration
  ELSA_MAX_USD_PER_CALL: z
    .string()
    .transform((v) => parseFloat(v))
    .pipe(z.number().positive())
    .default('0.05'),
  ELSA_MAX_USD_PER_DAY: z
    .string()
    .transform((v) => parseFloat(v))
    .pipe(z.number().positive())
    .default('2.00'),
  ELSA_MAX_CALLS_PER_MINUTE: z
    .string()
    .transform((v) => parseInt(v, 10))
    .pipe(z.number().int().positive())
    .default('30'),
  ELSA_TZ: z
    .string()
    .default('UTC'),

  // Execution Gate
  ELSA_ENABLE_EXECUTION_TOOLS: z
    .string()
    .transform((v) => v === 'true')
    .default('false'),
  ELSA_REQUIRE_CONFIRMATION_TOKEN: z
    .string()
    .transform((v) => v === 'true')
    .default('true'),
  ELSA_CONFIRMATION_TTL_SECONDS: z
    .string()
    .transform((v) => parseInt(v, 10))
    .pipe(z.number().int().positive())
    .default('600'),

  // Logging
  ELSA_AUDIT_LOG_PATH: z
    .string()
    .optional(),
  LOG_LEVEL: z
    .enum(['debug', 'info', 'warn', 'error'])
    .default('info'),
});

export type EnvConfig = z.infer<typeof EnvSchema>;

// ============================================================================
// Parse and Validate Environment
// ============================================================================

let cachedConfig: EnvConfig | null = null;

export function getConfig(): EnvConfig {
  if (cachedConfig) {
    return cachedConfig;
  }

  const result = EnvSchema.safeParse(process.env);

  if (!result.success) {
    const errors = result.error.errors.map(
      (e) => `  ${e.path.join('.')}: ${e.message}`
    );
    throw new Error(
      `Environment validation failed:\n${errors.join('\n')}\n\nRequired: PAYMENT_PRIVATE_KEY`
    );
  }

  cachedConfig = result.data;

  // Default TRADE_PRIVATE_KEY to PAYMENT_PRIVATE_KEY if not set
  if (!cachedConfig.TRADE_PRIVATE_KEY) {
    cachedConfig.TRADE_PRIVATE_KEY = cachedConfig.PAYMENT_PRIVATE_KEY;
  }

  return cachedConfig;
}

// ============================================================================
// Helper Functions
// ============================================================================

export function isExecutionEnabled(): boolean {
  return getConfig().ELSA_ENABLE_EXECUTION_TOOLS;
}

export function isConfirmationRequired(): boolean {
  return getConfig().ELSA_REQUIRE_CONFIRMATION_TOKEN;
}

export function getConfirmationTtlMs(): number {
  return getConfig().ELSA_CONFIRMATION_TTL_SECONDS * 1000;
}


path: scripts/__tests__/budgets.test.ts
content: import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { BudgetExceededError, RateLimitError } from '../errors.js';

// Mock luxon DateTime
vi.mock('luxon', () => {
  let mockNow = Date.now();
  return {
    DateTime: {
      now: () => ({
        setZone: () => ({
          startOf: () => ({
            toMillis: () => {
              // Return start of the current mock day
              const d = new Date(mockNow);
              d.setHours(0, 0, 0, 0);
              return d.getTime();
            },
          }),
        }),
      }),
      _setMockTime: (time: number) => {
        mockNow = time;
      },
      _getMockTime: () => mockNow,
    },
  };
});

// Mock the env module
vi.mock('../env.js', () => ({
  getConfig: () => ({
    ELSA_MAX_USD_PER_CALL: 0.05,
    ELSA_MAX_USD_PER_DAY: 2.00,
    ELSA_MAX_CALLS_PER_MINUTE: 30,
    ELSA_TZ: 'UTC',
  }),
}));

// Import after mocks are set up
import { budgetTracker, getEndpointCost, ENDPOINT_COSTS } from '../budgets.js';
import { DateTime } from 'luxon';

describe('budgets', () => {
  beforeEach(() => {
    budgetTracker.reset();
    vi.useFakeTimers();
    const now = new Date('2024-01-15T12:00:00Z').getTime();
    vi.setSystemTime(now);
    (DateTime as any)._setMockTime(now);
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('ENDPOINT_COSTS', () => {
    it('should have costs for all documented endpoints', () => {
      expect(ENDPOINT_COSTS['/api/search_token']).toBe(0.001);
      expect(ENDPOINT_COSTS['/api/get_token_price']).toBe(0.002);
      expect(ENDPOINT_COSTS['/api/get_balances']).toBe(0.005);
      expect(ENDPOINT_COSTS['/api/get_portfolio']).toBe(0.01);
      expect(ENDPOINT_COSTS['/api/analyze_wallet']).toBe(0.02);
      expect(ENDPOINT_COSTS['/api/get_swap_quote']).toBe(0.01);
      expect(ENDPOINT_COSTS['/api/execute_swap']).toBe(0.02);
      expect(ENDPOINT_COSTS['/api/get_transaction_status']).toBe(0.005);
      expect(ENDPOINT_COSTS['/api/submit_transaction_hash']).toBe(0.005);
    });

    it('should return default cost for unknown endpoints', () => {
      expect(getEndpointCost('/api/unknown')).toBe(0.01);
    });
  });

  describe('per-call budget', () => {
    it('should allow calls within per-call limit', () => {
      expect(() => budgetTracker.checkBudget('/api/search_token')).not.toThrow();
    });
  });

  describe('daily budget', () => {
    it('should track spending across calls', () => {
      budgetTracker.recordCall('/api/get_portfolio', 0.01);
      budgetTracker.recordCall('/api/analyze_wallet', 0.02);

      const status = budgetTracker.getStatus();
      expect(status.spent_today_usd).toBeCloseTo(0.03, 4);
      expect(status.remaining_today_usd).toBeCloseTo(1.97, 4);
    });

    it('should reject calls that would exceed daily limit', () => {
      // Record spending close to limit
      budgetTracker.recordCall('/api/test', 1.99);

      // This 0.02 call would push total over 2.00
      expect(() => budgetTracker.checkBudget('/api/execute_swap')).toThrow(BudgetExceededError);
    });
  });

  describe('rate limiting', () => {
    it('should allow calls within rate limit', () => {
      // Make 29 calls (under limit of 30)
      for (let i = 0; i < 29; i++) {
        budgetTracker.recordCall('/api/search_token', 0.001);
      }

      expect(() => budgetTracker.checkBudget('/api/search_token')).not.toThrow();
    });

    it('should reject calls exceeding rate limit', () => {
      // Make 30 calls (at limit)
      for (let i = 0; i < 30; i++) {
        budgetTracker.recordCall('/api/search_token', 0.001);
      }

      expect(() => budgetTracker.checkBudget('/api/search_token')).toThrow(RateLimitError);
    });

    it('should reset rate limit after 1 minute', () => {
      // Make 30 calls
      for (let i = 0; i < 30; i++) {
        budgetTracker.recordCall('/api/search_token', 0.001);
      }

      // Move forward 61 seconds
      vi.advanceTimersByTime(61_000);

      // Should be allowed now
      expect(() => budgetTracker.checkBudget('/api/search_token')).not.toThrow();
    });
  });

  describe('getStatus', () => {
    it('should return correct budget status', () => {
      budgetTracker.recordCall('/api/search_token', 0.001);
      budgetTracker.recordCall('/api/get_portfolio', 0.01);

      const status = budgetTracker.getStatus();

      expect(status.spent_today_usd).toBeCloseTo(0.011, 4);
      expect(status.remaining_today_usd).toBeCloseTo(1.989, 4);
      expect(status.calls_last_minute).toBe(2);
      expect(status.last_calls).toHaveLength(2);
    });

    it('should only include last 10 calls in last_calls', () => {
      // Make 15 calls
      for (let i = 0; i < 15; i++) {
        budgetTracker.recordCall('/api/search_token', 0.001);
      }

      const status = budgetTracker.getStatus();
      expect(status.last_calls).toHaveLength(10);
      expect(status.calls_last_minute).toBe(15);
    });
  });
});


path: scripts/__tests__/confirm.test.ts
content: import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ConfirmationInvalidError, ConfirmationRequiredError } from '../errors.js';

// Track mock time
let mockTime = Date.now();

// Mock the env module
vi.mock('../env.js', () => ({
  getConfig: () => ({
    ELSA_CONFIRMATION_TTL_SECONDS: 600,
    ELSA_REQUIRE_CONFIRMATION_TOKEN: true,
  }),
  isConfirmationRequired: () => true,
  getConfirmationTtlMs: () => 600_000,
}));

// Mock util module
vi.mock('../util.js', () => ({
  nowMs: () => mockTime,
  generateToken: () => `test-token-${Math.random().toString(36).slice(2)}`,
  normalizeSwapParams: (params: any) => ({
    from_chain: params.from_chain.trim().toLowerCase(),
    from_token: params.from_token.trim().toLowerCase(),
    from_amount: params.from_amount.trim(),
    to_chain: params.to_chain.trim().toLowerCase(),
    to_token: params.to_token.trim().toLowerCase(),
    wallet_address: params.wallet_address.trim().toLowerCase(),
    slippage: params.slippage,
  }),
  hashParams: (params: any) => {
    const json = JSON.stringify(params);
    // Simple hash for testing
    let hash = 0;
    for (let i = 0; i < json.length; i++) {
      const char = json.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return hash.toString(16);
  },
}));

// Import after mocks
import { confirmationStore } from '../confirm.js';

describe('confirmationStore', () => {
  const validParams = {
    from_chain: 'base',
    from_token: [REDACTED_SECRET],
    from_amount: '100',
    to_chain: 'base',
    to_token: '0x4200000000000000000000000000000000000006',
    wallet_address: [REDACTED_SECRET],
    slippage: 0.5,
  };

  beforeEach(() => {
    confirmationStore.reset();
    mockTime = Date.now();
  });

  describe('issueToken', () => {
    it('should issue a token for valid params', () => {
      const [REDACTED_SECRET]);
      expect(token).toBeTruthy();
      expect(typeof token).toBe('string');
      expect(token.length).toBeGreaterThan(10);
    });

    it('should store the token', () => {
      const [REDACTED_SECRET]);
      expect(confirmationStore.has(token)).toBe(true);
      expect(confirmationStore.size()).toBe(1);
    });

    it('should issue unique tokens for each call', () => {
      const token1 = confirmationStore.issueToken(validParams);
      const token2 = confirmationStore.issueToken(validParams);
      expect(token1).not.toBe(token2);
      expect(confirmationStore.size()).toBe(2);
    });
  });

  describe('validateAndConsume', () => {
    it('should validate and consume a valid token', () => {
      const [REDACTED_SECRET]);

      const result = confirmationStore.validateAndConsume(token, validParams);

      expect(result).toBe(true);
      expect(confirmationStore.has(token)).toBe(false);
    });

    it('should throw for non-existent token', () => {
      expect(() =>
        confirmationStore.validateAndConsume('non-existent-token', validParams)
      ).toThrow(ConfirmationInvalidError);
    });

    it('should throw for expired token', () => {
      const [REDACTED_SECRET]);

      // Advance time past TTL (600 seconds = 600000 ms)
      mockTime = mockTime + 601_000;

      expect(() =>
        confirmationStore.validateAndConsume(token, validParams)
      ).toThrow(ConfirmationInvalidError);
    });

    it('should throw for params mismatch', () => {
      const [REDACTED_SECRET]);

      const differentParams = {
        ...validParams,
        from_amount: '200', // Different amount
      };

      expect(() =>
        confirmationStore.validateAndConsume(token, differentParams)
      ).toThrow(ConfirmationInvalidError);
    });

    it('should be one-time use (second use fails)', () => {
      const [REDACTED_SECRET]);

      // First use succeeds
      confirmationStore.validateAndConsume(token, validParams);

      // Second use fails
      expect(() =>
        confirmationStore.validateAndConsume(token, validParams)
      ).toThrow(ConfirmationInvalidError);
    });

    it('should normalize params for comparison', () => {
      const [REDACTED_SECRET]);

      // Params with different casing/whitespace should still match
      const normalizedEquivalent = {
        from_chain: '  BASE  ',
        from_token: [REDACTED_SECRET],
        from_amount: '100',
        to_chain: 'BASE',
        to_token: '0x4200000000000000000000000000000000000006',
        wallet_address: [REDACTED_SECRET],
        slippage: 0.5,
      };

      const result = confirmationStore.validateAndConsume(token, normalizedEquivalent);
      expect(result).toBe(true);
    });
  });

  describe('requireConfirmation', () => {
    it('should pass when token is valid', () => {
      const [REDACTED_SECRET]);

      expect(() =>
        confirmationStore.requireConfirmation(token, validParams)
      ).not.toThrow();
    });

    it('should throw ConfirmationRequiredError when token is undefined', () => {
      expect(() =>
        confirmationStore.requireConfirmation(undefined, validParams)
      ).toThrow(ConfirmationRequiredError);
    });

    it('should throw ConfirmationInvalidError for invalid token', () => {
      expect(() =>
        confirmationStore.requireConfirmation('invalid-token', validParams)
      ).toThrow(ConfirmationInvalidError);
    });
  });

  describe('cleanup', () => {
    it('should clean up expired tokens when issuing new ones', () => {
      // Issue first token
      const token1 = confirmationStore.issueToken(validParams);
      expect(confirmationStore.size()).toBe(1);

      // Advance time past TTL
      mockTime = mockTime + 601_000;

      // Issue second token (should trigger cleanup)
      const token2 = confirmationStore.issueToken(validParams);

      // First token should be cleaned up, only second remains
      expect(confirmationStore.has(token1)).toBe(false);
      expect(confirmationStore.has(token2)).toBe(true);
      expect(confirmationStore.size()).toBe(1);
    });
  });
});


path: examples/demo.md
content: # Elsa x402 OpenClaw Demo

This document demonstrates typical usage patterns for the Elsa x402 OpenClaw skill-pack.

## 1. Token Search

Search for tokens across supported blockchains:

```bash
npx tsx scripts/index.ts elsa_search_token '{"query": "USDC", "limit": 5}'
```

Expected output:
```json
{
  "ok": true,
  "data": {
    "tokens": [
      {
        "name": "USD Coin",
        "symbol": "USDC",
        "address": [REDACTED_SECRET],
        "chain": "base",
        "decimals": 6
      }
    ]
  },
  "billing": {
    "estimated_cost_usd": 0.001,
    "payment_required": true,
    "receipt": null,
    "protocol": "x402-v1-compat"
  },
  "meta": {
    "latency_ms": 245,
    "endpoint": "/api/search_token",
    "timestamp": "2024-01-15T10:30:00.000Z"
  }
}
```

## 2. Portfolio Analysis

Get portfolio overview for a wallet:

```bash
npx tsx scripts/index.ts elsa_get_portfolio '{"wallet_address": [REDACTED_SECRET]}'
```

## 3. Budget Status Check

Check how much budget remains:

```bash
npx tsx scripts/index.ts elsa_budget_status '{}'
```

Expected output:
```json
{
  "ok": true,
  "data": {
    "spent_today_usd": 0.012,
    "remaining_today_usd": 1.988,
    "calls_last_minute": 3,
    "last_calls": []
  }
}
```

## 4. Swap Quote

Get a quote before executing:

```bash
npx tsx scripts/index.ts elsa_get_swap_quote '{
  "from_chain": "base",
  "from_token": [REDACTED_SECRET],
  "from_amount": "100",
  "to_chain": "base",
  "to_token": "0x4200000000000000000000000000000000000006",
  "wallet_address": [REDACTED_SECRET],
  "slippage": 0.5
}'
```

## 5. Safe Swap Execution Flow

### Step 1: Dry Run (Always Do This First!)

```bash
npx tsx scripts/index.ts elsa_execute_swap_dry_run '{
  "from_chain": "base",
  "from_token": [REDACTED_SECRET],
  "from_amount": "100",
  "to_chain": "base",
  "to_token": "0x4200000000000000000000000000000000000006",
  "wallet_address": [REDACTED_SECRET],
  "slippage": 0.5
}'
```

Response includes `confirmation_token`:
```json
{
  "ok": true,
  "data": {
    "pipeline_id": "pip_abc123",
    "status": "dry_run_complete"
  },
  "confirmation_token": "abc123xyz...",
  "confirmation_expires_in_seconds": 600
}
```

### Step 2: Review and Confirm (Requires ELSA_ENABLE_EXECUTION_TOOLS=true)

```bash
npx tsx scripts/index.ts elsa_execute_swap_confirmed '{
  "from_chain": "base",
  "from_token": [REDACTED_SECRET],
  "from_amount": "100",
  "to_chain": "base",
  "to_token": "0x4200000000000000000000000000000000000006",
  "wallet_address": [REDACTED_SECRET],
  "slippage": 0.5,
  "confirmation_token": "abc123xyz..."
}'
```

### Step 3: Complete Pipeline (If Needed)

```bash
npx tsx scripts/index.ts elsa_pipeline_run_and_wait '{
  "pipeline_id": "pip_123456789",
  "timeout_seconds": 120,
  "mode": "local_signer"
}'
```

## Error Handling

All errors return structured JSON:

```json
{
  "ok": false,
  "error": {
    "code": "BUDGET_EXCEEDED",
    "message": "Daily budget limit reached. Spent: $2.00, Limit: $2.00",
    "details": {
      "limit": 2.00,
      "requested": 0.02,
      "spent": 2.00
    }
  }
}
```

## Common Error Codes

| Code | Description |
|------|-------------|
| `BUDGET_EXCEEDED` | Per-call or daily budget limit reached |
| `RATE_LIMIT_EXCEEDED` | Too many calls in the last minute |
| `EXECUTION_DISABLED` | Execution tools not enabled |
| `CONFIRMATION_REQUIRED` | Need to run dry-run first |
| `CONFIRMATION_INVALID` | Token expired, not found, or params mismatch |
| `UPSTREAM_HTTP_ERROR` | Elsa API returned an error |
| `PIPELINE_TIMEOUT` | Pipeline execution timed out |
| `PIPELINE_TASK_ERROR` | A pipeline task failed |
| `VALIDATION_ERROR` | Invalid input parameters |


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