# Wallet

> Multi-chain wallet: EVM and Solana balances, transfers, signing, and policy. Use when checking balances, sending tokens, signing typed data, or proposing a wallet policy (e.g. send 10 USDC on Base, sign EIP-712, Solana balance).

- Skill: `starchild-ai-agent/wallet` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds add starchild-ai-agent/wallet`
- Raw SKILL.md: https://api.skillmd.com/api/skills/starchild-ai-agent/wallet/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: starchild-ai-agent (https://skillmd.com/u/starchild-ai-agent)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/starchild-ai-agent/wallet

---


# 💰 Wallet Skill

Multi-chain wallet for EVM (DeBank-supported chains) + Solana. Balances,
transfers, signing, and policy management. **Script skill** — call the functions
below via bash; no wallet tools are registered.

## How to call

All read/transfer/sign operations are Python functions in `core.skill_tools.wallet`.
Run them from bash and read the JSON result:

```bash
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_balance(chain='base')))"
```

The **one** operation that is NOT a script function is proposing a wallet policy
— it needs to render a confirmation card in the UI, so it goes through the native
`frontend_action` tool (see Policy Management below).

## Functions (`from core.skill_tools import wallet`)

| Function | Description |
|----------|-------------|
| `wallet_info()` | Get all AGENT wallet addresses |
| `get_user_wallets()` | The USER'S OWN wallets (login + secondary) — read-only, from env |
| `wallet_balance(chain, address="", asset="")` | EVM balance on a chain (DeBank). `chain` required |
| `wallet_sol_balance(address="", asset="")` | Solana balance (Birdeye) |
| `wallet_get_all_balances(evm_address="", sol_address="")` | All chains at once |
| `wallet_transfer(to, amount, chain_id=1, data="", **kw)` | **Broadcast** EVM tx (gas sponsored by default) |
| `wallet_sign_transaction(to, amount, chain_id=1, data="", **kw)` | Sign EVM tx (no broadcast) |
| `wallet_sign(message)` | EIP-191 message signing |
| `wallet_sign_typed_data(domain, types, primaryType, message)` | EIP-712 typed data signing |
| `wallet_transactions(chain="ethereum", asset="", limit=20)` | EVM tx history |
| `wallet_sol_transfer(transaction, caip2=...)` | **Broadcast** Solana tx (base64) |
| `wallet_sol_sign_transaction(transaction)` | Sign Solana tx (no broadcast) |
| `wallet_sol_sign(message)` | Solana message signing |
| `wallet_sol_transactions(chain="solana", asset="sol", limit=20)` | Solana tx history |
| `wallet_get_policy(chain_type="ethereum")` | Check policy status |
| `validate_and_clean_rules(rules, chain_type)` | Pre-validate policy rules before proposing |

## The User's Own Wallets (login / secondary)

The agent wallet is NOT the user's wallet. The platform injects the user's own
wallet identities as env vars (synced from the control plane at container start
and on user wallet actions):

- `USER_LOGIN_WALLET_ADDRESS` / `USER_LOGIN_WALLET_TYPE` — the wallet the user
  logs in with (or bound as primary).
- `USER_SECONDARY_WALLET_ADDRESS` / `USER_SECONDARY_WALLET_TYPE` — the user's
  other linked wallet (e.g. Solana when login is EVM).

When asked "what's my wallet" / "my login wallet" / "check MY balance", read
these — do NOT answer with the agent wallet or say you don't know:

```bash
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.get_user_wallets()))"
```

Empty/missing values mean the user has never bound a wallet in that slot
(e.g. social login) — say so and point them to wallet binding in the web app.

Rules:
- **Read-only.** The agent holds no keys for these wallets. To check the user's
  balances, pass the address into `wallet_balance(chain, address=...)` /
  `wallet_sol_balance(address=...)`.
- **Transactions from the user's wallet** never go through script functions —
  use the native `frontend_action(action_type="user_wallet_tx", ...)` flow,
  where the user signs in the UI and `expected_from` is enforced server-side.

## Key Facts

- **Amounts are in wei** for EVM (`wallet_transfer` / `wallet_sign_transaction`). 0.01 ETH = `10000000000000000`. For ERC-20 token sends, `amount` is `0` (native) and the transfer is encoded in `data` calldata.
- **Gas is sponsored by default** on EVM chains — user doesn't need native tokens for gas. Falls back to user-paid if unavailable. Pass `sponsor=False` to pay gas from wallet balance.
- **Policy default: OFF** (allow-all). Only when policy is enabled do transactions need UI confirmation.
- **Supported EVM chains**: All DeBank-supported chains. Common names auto-mapped (e.g. `avalanche` → `avax`, `bsc` → `bsc`, `zksync` → `era`). Fallback aliases include ethereum/base/arbitrum/optimism/polygon/linea/bsc/avalanche/fantom/gnosis/zksync/scroll/blast/mantle/celo/aurora **plus** monad/world/unichain/abstract/sonic/berachain.
- **Balance sources**: DeBank (EVM), Birdeye (Solana), wallet-service (fallback). DeBank/Birdeye keys are auto-injected by sc-proxy.

## Workflows

### Check balances
```bash
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_balance(chain='base')))"
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_get_all_balances()))"
```

### Send a transaction (EVM)
Always verify balance before, and the result/history after.
```bash
# 1. check
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_balance(chain='base')))"
# 2. transfer (amount in wei)
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_transfer(to='0x...', amount='10000000000000000', chain_id=8453)))"
# 3. verify
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_transactions(chain='base')))"
```

### Sign EIP-712 typed data
```bash
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_sign_typed_data(domain={...}, types={...}, primaryType='Permit', message={...})))"
```

## Policy Management

Checking policy is a script function; **proposing** a policy uses the native
`frontend_action` tool (it renders a signature card in the UI — a script cannot).

1. Check current policy:
   ```bash
   python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_get_policy(chain_type='ethereum')))"
   ```
2. (Optional) pre-validate rules:
   ```bash
   python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.validate_and_clean_rules([...], 'ethereum')))"
   ```
3. Propose — call the **`frontend_action` tool** (not a script):
   ```
   frontend_action(action_type="update_wallet_policy", chain_type="ethereum", rules=[...])
   ```
   The user confirms + signs in the UI. **Call once per chain** (EVM + Solana = two calls).

### Standard Wildcard Policy (when needed)
```
rules = [
  {"name": "Deny key export", "method": "exportPrivateKey", "conditions": [], "action": "DENY"},
  {"name": "Allow all", "method": "*", "conditions": [], "action": "ALLOW"},
]
```

### Policy Modes — CRITICAL DECISION TABLE

⚠️ **DENY > ALLOW in Privy.** `DENY *` overrides ALL ALLOW rules. NEVER mix them.

| Mode | Rules | Effect |
|------|-------|--------|
| **Allow-all** (default) | `DENY exportPrivateKey` + `ALLOW *` | Everything allowed except key export |
| **Deny-all** (lockdown) | `DENY exportPrivateKey` + `DENY *` | Nothing works. No ALLOW rules! |
| **Whitelist** (selective) | `DENY exportPrivateKey` + specific ALLOW rules only | Only whitelisted ops work, rest implicitly denied |

### Mode 1: Allow-All (Standard Wildcard)
```
rules = [
  {"name": "Deny key export", "method": "exportPrivateKey", "conditions": [], "action": "DENY"},
  {"name": "Allow all", "method": "*", "conditions": [], "action": "ALLOW"},
]
```

### Mode 2: Deny-All (Lockdown)
```
rules = [
  {"name": "Deny key export", "method": "exportPrivateKey", "conditions": [], "action": "DENY"},
  {"name": "Deny all actions", "method": "*", "conditions": [], "action": "DENY"},
]
# ⚠️ NO ALLOW rules here — DENY * would override them!
```

### Mode 3: Whitelist (Selective Allow)
```
rules = [
  {"name": "Deny key export", "method": "exportPrivateKey", "conditions": [], "action": "DENY"},
  {"name": "Allow transfer to Uniswap", "method": "eth_sendTransaction", "conditions": [
    {"field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"}
  ], "action": "ALLOW"},
]
# ⚠️ NO "DENY *" here! enabled=true already denies everything not ALLOWed.
# Adding DENY * would override the ALLOW rules above (DENY > ALLOW).
```

## Privy Policy Rules — Key Constraints

| Rule | Details |
|------|---------|
| **Default behavior** | `enabled=true` → deny-all unless explicitly ALLOWed |
| **DENY > ALLOW** | DENY always wins when both match |
| **Empty conditions** | Only `exportPrivateKey` and `*` (wildcard) allow `conditions: []` |
| **TX methods need conditions** | `eth_sendTransaction`, `eth_signTransaction`, `eth_signTypedData_v4`, `eth_signUserOperation`, `signAndSendTransaction`, etc. ALL require ≥1 condition |
| **Valid field_sources** | EVM: `ethereum_transaction` (to/value/chain_id), `ethereum_calldata` (function_name), `ethereum_typed_data_domain` (chainId/verifyingContract), `ethereum_typed_data_message`, `system` |
| **Valid operators** | `eq`, `gt`, `gte`, `lt`, `lte`, `in` (array, max 100 values) |
| **Dual chain** | Call `frontend_action(action_type="update_wallet_policy", ...)` TWICE for EVM + Solana |

## Gotchas

- Policy proposal goes through the `frontend_action` tool — needs an active SSE session (won't work from a background task).
- `wallet_balance` requires `chain` — use `wallet_get_all_balances` for discovery.
- For both EVM + Solana policy, call `frontend_action` TWICE (one per chain_type).

