Skill: rishabluthra/sponge-wallet (version 1.0.0)

--- name: sponge-wallet description: Manages crypto wallets, transfers tokens, swaps on DEXes, checks balances, and accesses paid APIs (search, image gen, prediction markets, web scraping, document parsing, sales prospecting) via x402 micropayments. Use when the user asks about wallet balances, token transfers, swaps, blockchain payments, or paid API services. --- # Sponge Wallet Skill Multi-chain crypto wallet with transfers, swaps, and paid API access. ## Authentication **IMPORTANT**: If any tool returns `"Not authenticated"` or `"Invalid API key"`, run the login flow. Login is two-phase (because Claude Code runs commands non-interactively): **Phase 1** — Start the device flow (returns a URL and code as JSON): ```bash node <skill-path>/scripts/wallet.mjs login ``` Show the `verification_url` and `user_code` to the user. Tell them to open the URL in their browser and enter the code. **Phase 2** — After the user confirms they have approved, poll for the token: ```bash node <skill-path>/scripts/wallet.mjs login --poll <device_code> <interval> <expires_in> ``` Use the `device_code`, `interval`, and `expires_in` values from Phase 1 output. Credentials are saved to `~/.spongewallet/credentials.json` automatically. Credential resolution order: 1. `SPONGE_API_KEY` environment variable (if set, skips stored credentials) 2. `~/.spongewallet/credentials.json` (saved by login) Other auth commands: - `node wallet.mjs whoami` — show current auth status - `node wallet.mjs logout` — remove stored credentials ## How to Execute ```bash node <skill-path>/scripts/wallet.mjs <tool_name> '<json_args>' ``` Output is JSON with `status: "success"` or `status: "error"`. ## Available Tools ### Wallet & Balance | Tool | Description | Required | Optional | |------|-------------|----------|----------| | `get_balance` | Check balances across chains | — | `chain` | | `get_solana_tokens` | Discover all SPL tokens in wallet | `chain` | — | | `search_solana_tokens` | Search Jupiter token database | `query` | `limit` | ### Transfers | Tool | Description | Required | Optional | |------|-------------|----------|----------| | `evm_transfer` | Transfer ETH/USDC on Ethereum/Base | `chain`, `to`, `amount`, `currency` | — | | `solana_transfer` | Transfer SOL/USDC on Solana | `chain`, `to`, `amount`, `currency` | — | ### Swaps | Tool | Description | Required | Optional | |------|-------------|----------|----------| | `solana_swap` | Swap tokens via Jupiter | `chain`, `input_token`, `output_token`, `amount` | `slippage_bps` | ### Transactions | Tool | Description | Required | Optional | |------|-------------|----------|----------| | `get_transaction_status` | Check tx status | `transaction_hash`, `chain` | — | | `get_transaction_history` | View past transactions | — | `limit`, `chain` | ### Funding & Withdrawals | Tool | Description | Required | Optional | |------|-------------|----------|----------| | `request_funding` | Request funds from owner | `amount`, `chain`, `currency` | — | | `withdraw_to_main_wallet` | Return funds to owner | `chain`, `amount` | `currency` | ### Paid APIs (Sponge x402) | Tool | Description | Required | Optional | |------|-------------|----------|----------| | `sponge` | Unified paid API interface | `task` | See [REFERENCE.md](REFERENCE.md) | | `create_x402_payment` | Create x402 payment payload | `chain`, `to`, `amount` | `token`, `decimals` | ## Chain Reference **Test keys** (`sponge_test_*`): `sepolia`, `base-sepolia`, `solana-devnet`, `tempo` **Live keys** (`sponge_live_*`): `ethereum`, `base`, `solana` ## Common Workflows ### Check Balance → Transfer → Verify ```bash node wallet.mjs get_balance '{"chain":"base"}' node wallet.mjs evm_transfer '{"chain":"base","to":"0x...","amount":"10","currency":"USDC"}' node wallet.mjs get_transaction_status '{"transaction_hash":"0x...","chain":"base"}' ``` ### Swap Tokens on Solana ```bash node wallet.mjs search_solana_tokens '{"query":"BONK"}' node wallet.mjs solana_swap '{"chain":"solana","input_token":"SOL","output_token":"BONK","amount":"0.5"}' ``` ### Sponge Paid APIs ```bash node wallet.mjs sponge '{"task":"search","query":"AI research papers"}' node wallet.mjs sponge '{"task":"image","prompt":"sunset over mountains"}' node wallet.mjs sponge '{"task":"predict","semantic_search":"will-trump-win-2028"}' node wallet.mjs sponge '{"task":"crawl","url":"https://example.com"}' node wallet.mjs sponge '{"task":"parse","document_url":"https://example.com/doc.pdf"}' node wallet.mjs sponge '{"task":"prospect","apollo_query":"Stripe","apollo_endpoint":"companies"}' ``` ## Error Handling | Error | Resolution | |-------|------------| | `Not authenticated` | Run `node wallet.mjs login` | | `Invalid API key` | Run `node wallet.mjs login` to re-authenticate | | `Chain 'X' is not allowed` | Use correct key type (test vs live) for the chain | | `Insufficient balance` | Use `request_funding` | | `Address not in allowlist` | Add recipient in the dashboard | See [REFERENCE.md](REFERENCE.md) for detailed parameter docs.

path: REFERENCE.md
content: # Sponge Wallet — Tool Parameter Reference

Detailed parameter documentation for every tool exposed by the Sponge Wallet skill.

---

## get_balance

Check wallet balances for native tokens and USDC across supported chains.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | No | Specific chain to check. If omitted, returns all chains. Values: `ethereum`, `base`, `sepolia`, `base-sepolia`, `tempo`, `solana`, `solana-devnet`, `all` |

```bash
node wallet.mjs get_balance '{}'
node wallet.mjs get_balance '{"chain":"base"}'
```

---

## evm_transfer

Transfer ETH or USDC on Ethereum or Base networks. USDC transfers use gas sponsorship automatically (sender doesn't need ETH for gas).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | Yes | `ethereum`, `base`, `sepolia`, or `base-sepolia` |
| `to` | string | Yes | Recipient address (0x-prefixed). Must be in your allowlist. |
| `amount` | string | Yes | Amount to send (e.g., `"0.1"` for ETH, `"100"` for USDC) |
| `currency` | string | Yes | `ETH` or `USDC` |

```bash
node wallet.mjs evm_transfer '{"chain":"base","to":[REDACTED_SECRET],"amount":"10","currency":"USDC"}'
```

---

## solana_transfer

Transfer SOL or USDC on Solana.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | Yes | `solana` or `solana-devnet` |
| `to` | string | Yes | Recipient address (base58 public key). Must be in your allowlist. |
| `amount` | string | Yes | Amount to send (e.g., `"0.5"` for SOL, `"100"` for USDC) |
| `currency` | string | Yes | `SOL` or `USDC` |

```bash
node wallet.mjs solana_transfer '{"chain":"solana","to":"5xG1...","amount":"1","currency":"SOL"}'
```

---

## solana_swap

Swap tokens on Solana using Jupiter aggregator. Finds the best route across all Solana DEXes.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | Yes | `solana` or `solana-devnet` |
| `input_token` | string | Yes | Token to swap from — symbol (`SOL`, `USDC`) or mint address |
| `output_token` | string | Yes | Token to swap to — symbol or mint address |
| `amount` | string | Yes | Amount of input token to swap (e.g., `"1.5"`) |
| `slippage_bps` | number | No | Slippage tolerance in basis points (default: 50 = 0.5%, max: 1000 = 10%) |

```bash
node wallet.mjs solana_swap '{"chain":"solana","input_token":"SOL","output_token":"USDC","amount":"1"}'
node wallet.mjs solana_swap '{"chain":"solana","input_token":"SOL","output_token":"BONK","amount":"0.5","slippage_bps":100}'
```

---

## get_solana_tokens

Discover all SPL tokens in your Solana wallet with metadata from Jupiter.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | Yes | `solana` or `solana-devnet` |

```bash
node wallet.mjs get_solana_tokens '{"chain":"solana"}'
```

---

## search_solana_tokens

Search Jupiter's token database by symbol or name.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | Token symbol, name, or partial match (e.g., `"BONK"`, `"dogwifhat"`) |
| `limit` | number | No | Max results (default: 10, max: 20) |

```bash
node wallet.mjs search_solana_tokens '{"query":"BONK"}'
node wallet.mjs search_solana_tokens '{"query":"pepe","limit":5}'
```

---

## get_transaction_status

Check the status of a transaction.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `transaction_hash` | string | Yes | Transaction hash (EVM) or signature (Solana) |
| `chain` | string | Yes | Chain the transaction is on |

```bash
node wallet.mjs get_transaction_status '{"transaction_hash":"0xabc123...","chain":"base"}'
```

---

## get_transaction_history

View past transactions for this agent's wallets.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `limit` | number | No | Number of transactions to return (default: 50) |
| `chain` | string | No | Filter by chain. If omitted, returns all chains. |

```bash
node wallet.mjs get_transaction_history '{}'
node wallet.mjs get_transaction_history '{"limit":10,"chain":"base"}'
```

---

## request_funding

Request additional funds from the agent's owner. Creates a pending approval request — funds are NOT transferred immediately.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `amount` | string | Yes | Amount to request (e.g., `"50.00"`) |
| `chain` | string | Yes | Chain to receive funds on |
| `currency` | string | Yes | Currency to request. Valid options depend on chain — EVM: `ETH`, `USDC`; Solana: `SOL`, `USDC`; Tempo: `pathUSD` |

```bash
node wallet.mjs request_funding '{"amount":"50","chain":"base","currency":"USDC"}'
```

---

## withdraw_to_main_wallet

Return funds from the agent wallet to the owner's main wallet. Does not require allowlist approval.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | Yes | Chain to withdraw on |
| `amount` | string | Yes | Amount to withdraw (e.g., `"0.1"`) |
| `currency` | string | No | `native` (default) for chain's native token, or `USDC` |

```bash
node wallet.mjs withdraw_to_main_wallet '{"chain":"base","amount":"0.05","currency":"native"}'
node wallet.mjs withdraw_to_main_wallet '{"chain":"base","amount":"100","currency":"USDC"}'
```

---

## sponge

Unified interface for paid API services via x402 micropayments (~$0.01 USDC per request). Payment is handled automatically.

### Common Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `task` | string | Yes | `search`, `image`, `predict`, `crawl`, `llm`, `parse`, or `prospect` |
| `query` | string | Depends | Search query or LLM prompt (for `search`, `llm`) |
| `prompt` | string | Depends | Image generation or LLM prompt (for `image`, `llm`) |
| `url` | string | Depends | URL to crawl (for `crawl`) |
| `model` | string | No | LLM model name (for `llm`, default: `openai/gpt-4o-mini`) |
| `num_results` | number | No | Number of search results (for `search`, default: 10) |
| `provider` | string | No | Override provider: `exa`, `dome`, `firecrawl`, `openrouter`, `gemini`, `reducto`, `apollo` |
| `auto_pay` | boolean | No | Auto-pay for services (default: true). Set false to see payment requirements first. |
| `payment_signature` | string | No | Base64 payment signature (only if `auto_pay` is false) |

### Prediction Market Parameters (task=predict)

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `semantic_search` | string | No | Polymarket semantic search slug (e.g., `"will-trump-win-2028"`) |
| `dome_path` | string | No | Dome API path (default: `/v1/polymarket/markets`) |

### Document Parsing Parameters (task=parse)

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `document_url` | string | Yes | URL of document to parse (PDF, image, spreadsheet) |

### Sales Prospecting Parameters (task=prospect)

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `apollo_endpoint` | string | No | `companies` (default), `people`, `enrich`, or `bulk_enrich` |
| `apollo_query` | string | No | Company name or domain to search (use for specific company lookups, NOT industry searches) |
| `apollo_filters` | object | No | Search filters — e.g., `{"q_organization_keyword_tags":["AI","fintech"],"organization_num_employees_ranges":["1,500"]}` |
| `apollo_person_titles` | string[] | No | Filter people by titles (e.g., `["CEO","CTO"]`) |
| `apollo_per_page` | number | No | Results per page (max: 20, default: 20) |
| `apollo_email` | string | No | Email for enrichment |
| `apollo_linkedin_url` | string | No | LinkedIn URL for enrichment |
| `apollo_person_id` | string | No | Apollo person ID for enrichment |
| `apollo_first_name` | string | No | First name for enrichment |
| `apollo_last_name` | string | No | Last name for enrichment |
| `apollo_organization_name` | string | No | Org name for enrichment |
| `apollo_domain` | string | No | Company domain for enrichment |
| `apollo_reveal_personal_emails` | boolean | No | Include personal emails (default: true) |
| `apollo_details` | object[] | No | Array of person details for bulk enrichment (max 10) |

### Examples

```bash
# Web search
node wallet.mjs sponge '{"task":"search","query":"latest AI research"}'

# Image generation
node wallet.mjs sponge '{"task":"image","prompt":"sunset over mountains"}'

# Prediction markets
node wallet.mjs sponge '{"task":"predict","semantic_search":"will-trump-win-2028","dome_path":"/v1/polymarket/markets"}'

# Web scraping
node wallet.mjs sponge '{"task":"crawl","url":"https://example.com"}'

# LLM completion
node wallet.mjs sponge '{"task":"llm","query":"Summarize quantum computing","model":"openai/gpt-4o"}'

# Document parsing
node wallet.mjs sponge '{"task":"parse","document_url":"https://example.com/report.pdf"}'

# Prospect: search companies by industry
node wallet.mjs sponge '{"task":"prospect","apollo_endpoint":"companies","apollo_filters":{"q_organization_keyword_tags":["artificial intelligence"],"organization_num_employees_ranges":["1,500"]},"apollo_per_page":5}'

# Prospect: enrich a contact
node wallet.mjs sponge '{"task":"prospect","apollo_endpoint":"enrich","apollo_email":"[REDACTED_SECRET]"}'
```

---

## create_x402_payment

Create a signed x402 payment payload for paying external APIs. This is a low-level tool — prefer using `sponge` which handles payment automatically.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `chain` | string | Yes | Chain for payment. Map from 402 response `network` field: `eip155:8453` -> `base`, `eip155:84532` -> `base-sepolia`, etc. |
| `to` | string | Yes | Recipient address from 402 response `payTo` field |
| `amount` | string | Yes | Human-readable amount (e.g., `"0.01"` for 0.01 USDC). Convert from `maxAmountRequired` by dividing by 10^decimals. |
| `token` | string | No | Token contract address from 402 response `asset` field. Defaults to USDC. |
| `decimals` | number | No | Token decimals (default: 6 for USDC) |
| `valid_for_seconds` | number | No | Payment validity in seconds (default: 3600) |
| `resource_url` | string | No | URL of the resource being purchased |
| `resource_description` | string | No | Description of the resource |

```bash
node wallet.mjs create_x402_payment '{"chain":"base-sepolia","to":[REDACTED_SECRET],"amount":"0.01","token":[REDACTED_SECRET]}'
```

---

## Testnet-Only Tools

These tools are only available with test API keys (`sponge_test_*`):

### tempo_transfer

Send pathUSD on the Tempo testnet.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `to` | string | Yes | Recipient address |
| `amount` | string | Yes | Amount of pathUSD to send |
| `use_gas_sponsorship` | boolean | No | Let the platform cover gas fees (recommended) |

### request_tempo_faucet

Get free Tempo testnet tokens. No parameters required.

```bash
node wallet.mjs request_tempo_faucet '{}'
```


path: README.md
content: # Sponge Wallet — Claude Skill

A Claude Code skill for managing crypto wallets, transferring tokens, swapping on DEXes, and accessing paid APIs via x402 micropayments.

## Install

```bash
git clone https://github.com/paysponge/wallet-skills.git ~/.claude/skills/sponge-wallet
```

That's it. No dependencies — just needs Node.js 18+.

## Login

On first use, Claude will prompt you to log in. Or run manually:

```bash
node ~/.claude/skills/sponge-wallet/scripts/wallet.mjs login
```

This opens a browser for OAuth authentication. Credentials are saved to `~/.spongewallet/credentials.json`.

## What it can do

- **Check balances** across Ethereum, Base, and Solana
- **Transfer** ETH, SOL, and USDC
- **Swap tokens** on Solana via Jupiter
- **View transaction** status and history
- **Request funding** from your main wallet
- **Search the web** via Exa (x402)
- **Generate images** via Gemini (x402)
- **Query prediction markets** via Dome (x402)
- **Scrape web pages** via Firecrawl (x402)
- **Parse documents** via Reducto (x402)
- **Find sales prospects** via Apollo (x402)

## Usage

Just ask Claude naturally:

- "Check my wallet balance"
- "Transfer 10 USDC to 0x..."
- "Swap 1 SOL for USDC"
- "Search for AI research papers"
- "What are the odds on the next election?"

## Supported chains

| Key type | Chains |
|----------|--------|
| `sponge_test_*` | sepolia, base-sepolia, solana-devnet, tempo |
| `sponge_live_*` | ethereum, base, solana |

## Docs

- [SKILL.md](SKILL.md) — Tool catalog and workflows
- [REFERENCE.md](REFERENCE.md) — Detailed parameter reference


path: scripts/wallet.mjs
content: #!/usr/bin/env node

/**
 * Sponge Wallet CLI wrapper for Claude Code skills.
 *
 * Connects to the Sponge Wallet MCP server via JSON-RPC, initializes a session,
 * then calls the requested tool.
 *
 * Usage:
 *   node wallet.mjs login                          # Authenticate via browser (OAuth device flow)
 *   node wallet.mjs logout                         # Remove stored credentials
 *   node wallet.mjs whoami                         # Show current auth status
 *   node wallet.mjs <tool_name> '<json_args>'      # Call a wallet tool
 *
 * Examples:
 *   node wallet.mjs get_balance '{}'
 *   node wallet.mjs evm_transfer '{"chain":"base","to":"0x...","amount":"10","currency":"USDC"}'
 *
 * Credential resolution order:
 *   1. SPONGE_API_KEY environment variable
 *   2. ~/.spongewallet/credentials.json (saved by `login` command)
 *
 * Environment:
 *   SPONGE_API_KEY  - Optional. Overrides stored credentials.
 *   SPONGE_API_URL  - Optional. Override API base URL (default: https://api.wallet.paysponge.com)
 */

import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

const DEFAULT_API_URL = "https://api.wallet.paysponge.com";
const API_URL = process.env.SPONGE_API_URL || DEFAULT_API_URL;
const MCP_ENDPOINT = `${API_URL}/mcp`;

const CREDENTIALS_DIR = path.join(os.homedir(), ".spongewallet");
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");

// ---------------------------------------------------------------------------
// Credential management
// ---------------------------------------------------------------------------

function loadCredentials() {
  try {
    if (!fs.existsSync(CREDENTIALS_FILE)) return null;
    const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
    if (!data.apiKey) return null;
    return data;
  } catch {
    return null;
  }
}

function saveCredentials(credentials) {
  fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
  fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
}

function deleteCredentials() {
  if (fs.existsSync(CREDENTIALS_FILE)) fs.unlinkSync(CREDENTIALS_FILE);
}

function getApiKey() {
  // 1. Environment variable
  if (process.env.SPONGE_API_KEY) return process.env.SPONGE_API_KEY;
  // 2. Stored credentials
  const creds = loadCredentials();
  return creds?.apiKey ?? null;
}

// ---------------------------------------------------------------------------
// OAuth Device Flow
// ---------------------------------------------------------------------------

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Login Phase 1: Request a device code and return the verification URL.
 * Outputs JSON so Claude can show the URL to the user.
 */
async function loginStart() {
  const res = await fetch(`${API_URL}/api/oauth/device/authorization`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      clientId: "spongewallet-skill",
      scope: "wallet:read wallet:write transaction:sign transaction:write",
    }),
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Failed to start device flow: ${text}`);
  }

  const device = await res.json();

  // Try to copy code to clipboard
  let copied = false;
  try {
    const { execSync } = await import("node:child_process");
    const platform = process.platform;
    if (platform === "darwin") {
      execSync(`echo -n ${JSON.stringify(device.userCode)} | pbcopy`);
      copied = true;
    } else if (platform === "linux") {
      execSync(`echo -n ${JSON.stringify(device.userCode)} | xclip -selection clipboard 2>/dev/null || echo -n ${JSON.stringify(device.userCode)} | xsel --clipboard 2>/dev/null`);
      copied = true;
    } else if (platform === "win32") {
      execSync(`echo|set /p=${JSON.stringify(device.userCode)} | clip`);
      copied = true;
    }
  } catch {
    // Clipboard not available
  }

  // Output structured JSON for Claude to present to the user
  console.log(JSON.stringify({
    status: "login_required",
    verification_url: device.verificationUri,
    user_code: device.userCode,
    device_code: device.deviceCode,
    expires_in: device.expiresIn,
    interval: device.interval,
    copied_to_clipboard: copied,
    message: `Visit ${device.verificationUri} and enter code: ${device.userCode}`,
  }, null, 2));
}

/**
 * Login Phase 2: Poll for token completion after user has approved in browser.
 * Called with the device_code from Phase 1.
 */
async function loginPoll(deviceCode, intervalSeconds, expiresInSeconds) {
  const expiresAt = Date.now() + expiresInSeconds * 1000;
  let interval = intervalSeconds * 1000;

  while (Date.now() < expiresAt) {
    await sleep(interval);

    const tokenRes = await fetch(`${API_URL}/api/oauth/device/token`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grantType: "urn:ietf:params:oauth:grant-type:device_code",
        deviceCode,
        clientId: "spongewallet-skill",
      }),
    });

    if (tokenRes.ok) {
      const token = await tokenRes.json();

      const credentials = {
        [REDACTED_SECRET],
        agentId: token.agentId || undefined,
        createdAt: new Date().toISOString(),
        baseUrl: API_URL !== DEFAULT_API_URL ? API_URL : undefined,
      };
      saveCredentials(credentials);

      console.log(JSON.stringify({
        status: "success",
        message: "Authenticated successfully. You can now use any wallet tool.",
        credentials_file: CREDENTIALS_FILE,
      }, null, 2));
      return;
    }

    const errorData = await tokenRes.json().catch(() => ({}));
    switch (errorData.error) {
      case "authorization_pending":
        // Still waiting — keep polling silently
        break;
      case "slow_down":
        interval += 5000;
        break;
      case "access_denied":
        throw new Error("Access denied by user.");
      case "expired_token":
        throw new Error("Device code expired. Please run login again.");
      default:
        throw new Error(`Auth failed: ${errorData.errorDescription || errorData.error || "Unknown error"}`);
    }
  }

  throw new Error("Device code expired. Please run login again.");
}

function logout() {
  deleteCredentials();
  console.log("Logged out. Credentials removed.");
}

function whoami() {
  const envKey = process.env.SPONGE_API_KEY;
  const creds = loadCredentials();

  if (envKey) {
    const prefix = envKey.substring(0, 20);
    const isTest = envKey.startsWith("sponge_test_");
    console.log(`Authenticated via SPONGE_API_KEY environment variable`);
    console.log(`  Key: ${prefix}...`);
    console.log(`  Mode: ${isTest ? "testnet" : "mainnet"}`);
  } else if (creds) {
    const prefix = creds.apiKey.substring(0, 20);
    const isTest = creds.apiKey.startsWith("sponge_test_");
    console.log(`Authenticated via stored credentials`);
    console.log(`  Key: ${prefix}...`);
    console.log(`  Mode: ${isTest ? "testnet" : "mainnet"}`);
    console.log(`  File: ${CREDENTIALS_FILE}`);
    if (creds.agentId) console.log(`  Agent: ${creds.agentId}`);
    if (creds.createdAt) console.log(`  Since: ${creds.createdAt}`);
  } else {
    console.log("Not authenticated.");
    console.log("\nRun: node wallet.mjs login");
  }
}

// ---------------------------------------------------------------------------
// MCP JSON-RPC
// ---------------------------------------------------------------------------

async function jsonrpc(apiKey, method, params, sessionId) {
  const headers = {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream",
  };
  if (sessionId) {
    headers["Mcp-Session-Id"] = sessionId;
  }

  const res = await fetch(MCP_ENDPOINT, {
    method: "POST",
    headers,
    body: JSON.stringify({
      jsonrpc: "2.0",
      method,
      params,
      id: crypto.randomUUID(),
    }),
  });

  const returnedSessionId = res.headers.get("mcp-session-id") || sessionId;

  if (!res.ok) {
    let errorBody;
    try { errorBody = await res.json(); } catch { errorBody = { message: res.statusText }; }
    throw new Error(errorBody?.error?.message || errorBody?.message || `HTTP ${res.status}`);
  }

  const data = await res.json();
  if (data.error) {
    throw new Error(data.error.message || JSON.stringify(data.error));
  }

  return { result: data.result, sessionId: returnedSessionId };
}

async function callTool(apiKey, tool, toolArgs) {
  // Step 1: Initialize MCP session
  const init = await jsonrpc(apiKey, "initialize", {
    protocolVersion: "2025-03-26",
    capabilities: {},
    clientInfo: { name: "sponge-wallet-skill", version: "0.1.0" },
  });

  const sessionId = init.sessionId;

  // Step 2: Send initialized notification
  await fetch(MCP_ENDPOINT, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Accept": "application/json, text/event-stream",
      "Mcp-Session-Id": sessionId,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: "notifications/initialized",
    }),
  });

  // Step 3: Call the tool
  const { result } = await jsonrpc(apiKey, "tools/call", {
    name: tool,
    arguments: toolArgs,
  }, sessionId);

  return result;
}

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

const command = process.argv[2];

// Handle auth commands (no API key needed)
if (command === "login") {
  const subcommand = process.argv[3];
  try {
    if (subcommand === "--poll") {
      // Phase 2: poll for token with device_code, interval, expires_in
      const deviceCode = process.argv[4];
      const interval = parseInt(process.argv[5] || "5", 10);
      const expiresIn = parseInt(process.argv[6] || "900", 10);
      if (!deviceCode) {
        console.error(JSON.stringify({ status: "error", error: "Usage: node wallet.mjs login --poll <device_code> [interval] [expires_in]" }));
        process.exit(1);
      }
      await loginPoll(deviceCode, interval, expiresIn);
    } else {
      // Phase 1: start device flow, return URL as JSON
      await loginStart();
    }
  } catch (err) {
    console.error(JSON.stringify({ status: "error", error: err.message }));
    process.exit(1);
  }
  process.exit(0);
}

if (command === "logout") {
  logout();
  process.exit(0);
}

if (command === "whoami") {
  whoami();
  process.exit(0);
}

// Everything else requires authentication
const [REDACTED_SECRET]);

if (!apiKey) {
  console.error(JSON.stringify({
    status: "error",
    error: "Not authenticated. Run: node wallet.mjs login",
  }));
  process.exit(1);
}

const toolName = command;
const argsRaw = process.argv[3] || "{}";

if (!toolName) {
  console.error(JSON.stringify({
    status: "error",
    error: "Usage: node wallet.mjs <tool_name> '<json_args>'",
    available_tools: [
      "login", "logout", "whoami",
      "get_balance", "evm_transfer", "solana_transfer", "solana_swap",
      "get_solana_tokens", "search_solana_tokens",
      "get_transaction_status", "get_transaction_history",
      "request_funding", "withdraw_to_main_wallet",
      "sponge", "create_x402_payment",
    ],
  }));
  process.exit(1);
}

let args;
try {
  args = JSON.parse(argsRaw);
} catch {
  console.error(JSON.stringify({
    status: "error",
    error: `Invalid JSON arguments: ${argsRaw}`,
  }));
  process.exit(1);
}

try {
  const result = await callTool(apiKey, toolName, args);

  // MCP tool results have { content: [{ type, text }], isError? }
  if (result?.content) {
    const textParts = result.content
      .filter(c => c.type === "text")
      .map(c => {
        try { return JSON.parse(c.text); }
        catch { return c.text; }
      });

    const output = textParts.length === 1 ? textParts[0] : textParts;
    console.log(JSON.stringify({
      status: result.isError ? "error" : "success",
      data: output,
    }, null, 2));
  } else {
    console.log(JSON.stringify({ status: "success", data: result }, null, 2));
  }
} catch (err) {
  console.error(JSON.stringify({
    status: "error",
    error: err.message || String(err),
  }));
  process.exit(1);
}


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