A proper multi-chain wallet implementation that handles the messy reality of blockchain operations. Covers EVM chains via DeBank and Solana via Birdeye, with gas sponsorship that falls back to user-paid when needed. The policy system is notably well-documented since Privy's rule precedence (DENY always wins) trips up most implementations. Includes all the signing primitives you'd expect: EIP-191, EIP-712, transaction signing, plus broadcast capabilities. The three policy modes (allow-all, deny-all, whitelist) cover the main security patterns without the usual footguns. Solid choice if you need wallet functionality that actually works in production rather than just demos.
npx -y skills add starchild-ai-agent/official-skills --skill wallet --agent claude-codeInstalls into .claude/skills of the current project.
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.
All read/transfer/sign operations are Python functions in core.skill_tools.wallet.
Run them from bash and read the JSON result:
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).
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 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:
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:
wallet_balance(chain, address=...) /
wallet_sol_balance(address=...).frontend_action(action_type="user_wallet_tx", ...) flow,
where the user signs in the UI and expected_from is enforced server-side.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.sponsor=False to pay gas from wallet balance.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.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()))"
Always verify balance before, and the result/history after.
# 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')))"
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_sign_typed_data(domain={...}, types={...}, primaryType='Permit', message={...})))"
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).
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.wallet_get_policy(chain_type='ethereum')))"
python3 -c "from core.skill_tools import wallet; import json; print(json.dumps(wallet.validate_and_clean_rules([...], 'ethereum')))"
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).rules = [
{"name": "Deny key export", "method": "exportPrivateKey", "conditions": [], "action": "DENY"},
{"name": "Allow all", "method": "*", "conditions": [], "action": "ALLOW"},
]
⚠️ 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 |
rules = [
{"name": "Deny key export", "method": "exportPrivateKey", "conditions": [], "action": "DENY"},
{"name": "Allow all", "method": "*", "conditions": [], "action": "ALLOW"},
]
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!
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).
| 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 |
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.frontend_action TWICE (one per chain_type).sickn33/antigravity-awesome-skills
moizibnyousaf/ai-agent-skills
github/awesome-copilot