
Wallet
- 8.6k installs
- 18 repo stars
- Updated July 27, 2026
- starchild-ai-agent/official-skills
wallet is an agent skill that |.
About
| --- name: wallet version: 3.6.0 description: | 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). author: starchild tags: [wallet, evm, solana, transfer, sign, policy, debank, birdeye] delivery: script metadata: starchild: emoji: 💰 skillKey: 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).
- **Policy default: OFF** (allow-all). Only when policy is enabled do transactions need UI confirmation.
- **Balance sources**: DeBank (EVM), Birdeye (Solana), wallet-service (fallback). DeBank/Birdeye keys are auto-injected by
- 2. transfer (amount in wei)
- Check current policy:
- (Optional) pre-validate rules:
Wallet by the numbers
- 8,608 all-time installs (skills.sh)
- +74 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #76 of 2,209 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
wallet capabilities & compatibility
- Capabilities
- **policy default: off** (allow all). only when p · **balance sources**: debank (evm), birdeye (sola · 2. transfer (amount in wei) · check current policy: · (optional) pre validate rules:
- Use cases
- documentation
What wallet says it does
--- name: wallet version: 3.6.0 description: | 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).
Balances, transfers, signing, and policy management.
npx skills add https://github.com/starchild-ai-agent/official-skills --skill walletAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8.6k |
|---|---|
| repo stars | ★ 18 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | starchild-ai-agent/official-skills ↗ |
What problem does wallet solve for developers using this skill?
|
Who is it for?
Developers who need wallet patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
|
What you get
Actionable workflows and conventions from SKILL.md for wallet.
- registered wallet agent tools
- signed transactions and transfer results
Files
💰 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:
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 wallet addresses |
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 |
Key Facts
- Amounts are in wei for EVM (
wallet_transfer/wallet_sign_transaction). 0.01 ETH =10000000000000000. For ERC-20 token sends,amountis0(native) and the transfer is encoded indatacalldata. - 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=Falseto 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). The 16 common chains (ethereum, base, arbitrum, optimism, polygon, linea, bsc, avalanche, fantom, gnosis, zksync, scroll, blast, mantle, celo, aurora) have built-in fallback mapping. - Balance sources: DeBank (EVM), Birdeye (Solana), wallet-service (fallback). DeBank/Birdeye keys are auto-injected by sc-proxy.
Workflows
Check balances
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.
# 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
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:
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:
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_actiontool — needs an active SSE session (won't work from a background task). wallet_balancerequireschain— usewallet_get_all_balancesfor discovery.- For both EVM + Solana policy, call
frontend_actionTWICE (one per chain_type).
"""
Wallet Skill — Multi-chain wallet (EVM + Solana)
Balances, transfers, signing, policy management.
Delegates to /app/tools/wallet for core functions.
"""
import logging
from typing import List
logger = logging.getLogger(__name__)
def register(api) -> List[str]:
"""Register all wallet tools with the Agent framework."""
registered = []
try:
from .wallet import (
WalletInfoTool,
WalletBalanceTool,
WalletSolBalanceTool,
WalletGetAllBalancesTool,
WalletTransferTool,
WalletSignTransactionTool,
WalletSignTool,
WalletSignTypedDataTool,
WalletTransactionsTool,
WalletSolTransferTool,
WalletSolSignTransactionTool,
WalletSolSignTool,
WalletSolTransactionsTool,
WalletGetPolicyTool,
WalletProposePolicyTool,
)
tools = [
WalletInfoTool(),
WalletBalanceTool(),
WalletSolBalanceTool(),
WalletGetAllBalancesTool(),
WalletTransferTool(),
WalletSignTransactionTool(),
WalletSignTool(),
WalletSignTypedDataTool(),
WalletTransactionsTool(),
WalletSolTransferTool(),
WalletSolSignTransactionTool(),
WalletSolSignTool(),
WalletSolTransactionsTool(),
WalletGetPolicyTool(),
WalletProposePolicyTool(),
]
for tool in tools:
api.register_tool(tool)
registered.append(tool.name)
except Exception as e:
logger.error(f"Failed to register wallet tools: {e}", exc_info=True)
return registered
"""
Wallet skill exports — for use in task scripts via core.skill_tools.
Usage:
from core.skill_tools import wallet
info = wallet.wallet_info()
bal = wallet.wallet_balance(chain="base")
all_bal = wallet.wallet_get_all_balances()
Delegates to /app/tools/wallet core functions for single-source maintenance.
"""
import asyncio
try:
import nest_asyncio
nest_asyncio.apply()
except ImportError:
pass
# Import core functions from /app/tools/wallet
from tools.wallet import (
_wallet_request,
_is_fly_machine,
_get_wallet_addresses,
_validate_and_clean_rules,
DEBANK_CHAIN_MAP,
)
from core.http_client import proxied_get
EVM_CHAINS = list(DEBANK_CHAIN_MAP.keys())
def _run(coro):
"""Run async in sync context (works even if event loop is running)."""
try:
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
except RuntimeError:
return asyncio.run(coro)
# ── Info ─────────────────────────────────────────────────────────────────────
def wallet_info():
"""Get all wallet addresses."""
return _run(_wallet_request("GET", "/agent/wallet"))
# ── Balances ─────────────────────────────────────────────────────────────────
def wallet_balance(chain: str, address: str = "", asset: str = ""):
"""Get EVM balance on a chain (via DeBank or wallet-service fallback)."""
import os
async def _impl():
if chain not in EVM_CHAINS:
return {"error": f"Invalid chain '{chain}'. Must be one of: {', '.join(EVM_CHAINS)}"}
debank_key = os.environ.get("DEBANK_API_KEY", "")
if debank_key:
evm_address = address
if not evm_address:
addrs = await _get_wallet_addresses()
evm_address = addrs.get("evm", "")
if not evm_address:
return {"error": "Could not determine EVM wallet address"}
resp = proxied_get(
"https://pro-openapi.debank.com/v1/user/token_list",
params={"id": evm_address, "chain_id": DEBANK_CHAIN_MAP.get(chain), "is_all": "false"},
headers={"AccessKey": debank_key},
)
resp.raise_for_status()
return {"address": evm_address, "chain": chain, "tokens": resp.json(), "source": "debank"}
else:
params = [f"chain_type=ethereum&chain={chain}"]
if asset: params.append(f"asset={asset}")
return await _wallet_request("GET", f"/agent/balance?{'&'.join(params)}")
return _run(_impl())
def wallet_sol_balance(address: str = "", asset: str = ""):
"""Get Solana balance."""
import os
async def _impl():
birdeye_key = os.environ.get("BIRDEYE_API_KEY", "")
if birdeye_key:
sol_address = address
if not sol_address:
addrs = await _get_wallet_addresses()
sol_address = addrs.get("sol", "")
if not sol_address:
return {"error": "Could not determine Solana wallet address"}
resp = proxied_get(
"https://public-api.birdeye.so/wallet/v2/net-worth",
params={"wallet": sol_address},
headers={"X-API-KEY": birdeye_key, "x-chain": "solana", "accept": "application/json"},
)
resp.raise_for_status()
return {"address": sol_address, "source": "birdeye", "data": resp.json()}
else:
params = ["chain_type=solana"]
if asset: params.append(f"asset={asset}")
return await _wallet_request("GET", f"/agent/balance?{'&'.join(params)}")
return _run(_impl())
def wallet_get_all_balances(evm_address: str = "", sol_address: str = ""):
"""Get balances across all chains."""
async def _impl():
ea, sa = evm_address, sol_address
if not ea or not sa:
if _is_fly_machine():
try:
addrs = await _get_wallet_addresses()
ea = ea or addrs.get("evm", "")
sa = sa or addrs.get("sol", "")
except Exception:
pass
# Delegate to per-chain balance calls
result = {}
if ea:
for chain in EVM_CHAINS:
try:
data = wallet_balance(chain, ea)
if "error" not in data:
result[chain] = data
except Exception:
pass
if sa:
try:
data = wallet_sol_balance(sa)
if "error" not in data:
result["solana"] = data
except Exception:
pass
return result
return _run(_impl())
# ── Transfers ────────────────────────────────────────────────────────────────
def wallet_transfer(to, amount, chain_id=1, data="", **kw):
"""Sign and broadcast EVM tx."""
async def _impl():
body = {"to": to, "amount": str(amount), "chain_id": chain_id}
if data: body["data"] = data
for k in ("gas_limit", "gas_price", "max_fee_per_gas", "max_priority_fee_per_gas", "nonce"):
if kw.get(k): body[k] = kw[k]
if kw.get("tx_type") is not None: body["tx_type"] = kw["tx_type"]
return await _wallet_request("POST", "/agent/transfer", body)
return _run(_impl())
def wallet_sign_transaction(to, amount, chain_id=1, data="", **kw):
"""Sign EVM tx without broadcasting."""
async def _impl():
body = {"to": to, "amount": str(amount), "chain_id": chain_id}
if data: body["data"] = data
for k in ("gas_limit", "gas_price", "max_fee_per_gas", "max_priority_fee_per_gas", "nonce"):
if kw.get(k): body[k] = kw[k]
if kw.get("tx_type") is not None: body["tx_type"] = kw["tx_type"]
return await _wallet_request("POST", "/agent/sign-transaction", body)
return _run(_impl())
def wallet_sign(message):
"""EIP-191 personal_sign."""
return _run(_wallet_request("POST", "/agent/sign", {"message": message}))
def wallet_sign_typed_data(domain, types, primaryType, message):
"""Sign EIP-712 typed data."""
return _run(_wallet_request("POST", "/agent/sign-typed-data", {
"domain": domain, "types": types, "primaryType": primaryType, "message": message,
}))
def wallet_transactions(chain="ethereum", asset="", limit=20):
"""EVM tx history."""
qs = f"?chain_type=ethereum&chain={chain}&asset={asset or 'eth'}&limit={limit}"
return _run(_wallet_request("GET", f"/agent/transactions{qs}"))
# ── Solana ───────────────────────────────────────────────────────────────────
def wallet_sol_transfer(transaction, caip2="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"):
"""Sign and broadcast Solana tx."""
return _run(_wallet_request("POST", "/agent/sol/transfer", {
"transaction": transaction, "caip2": caip2,
}))
def wallet_sol_sign_transaction(transaction):
"""Sign Solana tx without broadcasting."""
return _run(_wallet_request("POST", "/agent/sol/sign-transaction", {"transaction": transaction}))
def wallet_sol_sign(message):
"""Sign message with Solana wallet."""
return _run(_wallet_request("POST", "/agent/sol/sign", {"message": message}))
def wallet_sol_transactions(chain="solana", asset="sol", limit=20):
"""Solana tx history."""
qs = f"?chain_type=solana&chain={chain}&asset={asset}&limit={limit}"
return _run(_wallet_request("GET", f"/agent/transactions{qs}"))
# ── Policy ───────────────────────────────────────────────────────────────────
def wallet_get_policy(chain_type="ethereum"):
"""Get current policy status."""
return _run(_wallet_request("GET", f"/agent/policy?chain_type={chain_type}"))
def validate_and_clean_rules(rules, chain_type):
"""Validate rules (re-export from system)."""
return _validate_and_clean_rules(rules, chain_type)
"""
Wallet Tool Wrappers — BaseTool classes for Agent framework.
Delegates to /app/tools/wallet core functions for single-source-of-truth maintenance.
"""
import logging
import os
import time
from core.tool import BaseTool, ToolContext, ToolResult
# ── Import core wallet functions from /app/tools/wallet ─────────────────────
from tools.wallet import (
_is_fly_machine,
_wallet_request,
_get_wallet_addresses,
_validate_and_clean_rules,
DEBANK_CHAIN_MAP,
)
from core.http_client import proxied_get
logger = logging.getLogger(__name__)
EVM_CHAINS = list(DEBANK_CHAIN_MAP.keys())
def _fly_check():
if not _is_fly_machine():
return ToolResult(success=False, error="Not running on a Fly Machine — wallet unavailable")
return None
def _proxied_get_with_retry(url, params=None, headers=None, timeout=30, max_retries=3):
"""proxied_get with retry on timeout / 429 / 5xx."""
import requests
last_exc = None
for attempt in range(max_retries):
try:
resp = proxied_get(url, params=params, headers=headers, timeout=timeout)
if resp.status_code == 429 or resp.status_code >= 500:
if attempt < max_retries - 1:
time.sleep(1 * (attempt + 1))
continue
resp.raise_for_status()
return resp
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
last_exc = e
if attempt < max_retries - 1:
time.sleep(1)
except requests.exceptions.HTTPError:
raise
except Exception as e:
last_exc = e
break
raise last_exc or Exception("Max retries exceeded")
# ── Info ─────────────────────────────────────────────────────────────────────
class WalletInfoTool(BaseTool):
@property
def name(self): return "wallet_info"
@property
def description(self): return "Get all on-chain wallet addresses for this agent (one per chain)."
@property
def parameters(self): return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kw) -> ToolResult:
if err := _fly_check(): return err
try:
return ToolResult(success=True, output=await _wallet_request("GET", "/agent/wallet"))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── EVM Balance ──────────────────────────────────────────────────────────────
class WalletBalanceTool(BaseTool):
@property
def name(self): return "wallet_balance"
@property
def description(self): return """Get EVM wallet balance on a specific chain. Omit 'asset' to discover ALL tokens.
Chains: ethereum, base, arbitrum, optimism, polygon, linea.
Use wallet_get_all_balances for all chains at once."""
@property
def parameters(self): return {
"type": "object",
"properties": {
"chain": {"type": "string", "enum": EVM_CHAINS, "description": "Required. Blockchain network."},
"address": {"type": "string", "description": "EVM address (0x...). Omit for own wallet."},
"asset": {"type": "string", "description": "Asset filter. Omit for ALL tokens."},
},
"required": ["chain"],
}
async def execute(self, ctx: ToolContext, chain="", address="", asset="", **kw) -> ToolResult:
if not chain or chain not in EVM_CHAINS:
return ToolResult(success=False, error=f"'chain' required. One of: {', '.join(EVM_CHAINS)}")
debank_key = os.environ.get("DEBANK_API_KEY", "")
if debank_key:
evm_address = address
if not evm_address:
if err := _fly_check(): return err
try:
addrs = await _get_wallet_addresses()
evm_address = addrs.get("evm", "")
except Exception as e:
return ToolResult(success=False, error=f"Failed to get wallet address: {e}")
if not evm_address:
return ToolResult(success=False, error="Could not determine EVM wallet address")
debank_chain_id = DEBANK_CHAIN_MAP.get(chain)
try:
resp = _proxied_get_with_retry(
"https://pro-openapi.debank.com/v1/user/token_list",
params={"id": evm_address, "chain_id": debank_chain_id, "is_all": "false"},
headers={"AccessKey": debank_key},
)
return ToolResult(success=True, output={
"address": evm_address, "chain": chain, "tokens": resp.json(), "source": "debank",
})
except Exception as e:
return ToolResult(success=False, error=f"DeBank request failed: {e}")
else:
if err := _fly_check(): return err
try:
params = [f"chain_type=ethereum&chain={chain}"]
if asset: params.append(f"asset={asset}")
data = await _wallet_request("GET", f"/agent/balance?{'&'.join(params)}")
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Solana Balance ───────────────────────────────────────────────────────────
class WalletSolBalanceTool(BaseTool):
@property
def name(self): return "wallet_sol_balance"
@property
def description(self): return "Get Solana wallet balance. Omit 'asset' to discover ALL SPL tokens."
@property
def parameters(self): return {
"type": "object",
"properties": {
"address": {"type": "string", "description": "Solana address. Omit for own wallet."},
"asset": {"type": "string", "description": "Asset filter. Omit for ALL tokens."},
},
}
async def execute(self, ctx: ToolContext, address="", asset="", **kw) -> ToolResult:
birdeye_key = os.environ.get("BIRDEYE_API_KEY", "")
if birdeye_key:
sol_address = address
if not sol_address:
if err := _fly_check(): return err
try:
addrs = await _get_wallet_addresses()
sol_address = addrs.get("sol", "")
except Exception as e:
return ToolResult(success=False, error=f"Failed to get wallet address: {e}")
if not sol_address:
return ToolResult(success=False, error="Could not determine Solana wallet address")
try:
resp = _proxied_get_with_retry(
"https://public-api.birdeye.so/wallet/v2/net-worth",
params={"wallet": sol_address},
headers={"X-API-KEY": birdeye_key, "x-chain": "solana", "accept": "application/json"},
)
return ToolResult(success=True, output={"address": sol_address, "source": "birdeye", "data": resp.json()})
except Exception as e:
return ToolResult(success=False, error=f"Birdeye request failed: {e}")
else:
if err := _fly_check(): return err
try:
params = ["chain_type=solana"]
if asset: params.append(f"asset={asset}")
data = await _wallet_request("GET", f"/agent/balance?{'&'.join(params)}")
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── All Balances ─────────────────────────────────────────────────────────────
class WalletGetAllBalancesTool(BaseTool):
@property
def name(self): return "wallet_get_all_balances"
@property
def description(self): return "Get complete balance snapshot across ALL chains (EVM + Solana) with USD values."
@property
def parameters(self): return {
"type": "object",
"properties": {
"evm_address": {"type": "string", "description": "EVM address (0x...). Omit for own."},
"sol_address": {"type": "string", "description": "Solana address. Omit for own."},
},
}
async def execute(self, ctx: ToolContext, evm_address="", sol_address="", **kw) -> ToolResult:
import asyncio
if not evm_address or not sol_address:
if _is_fly_machine():
try:
addrs = await _get_wallet_addresses()
evm_address = evm_address or addrs.get("evm", "")
sol_address = sol_address or addrs.get("sol", "")
except Exception:
pass
result = {}
errors = []
evm_usd = 0.0
sol_usd = 0.0
async def _fetch_evm():
nonlocal evm_usd
if not evm_address: return
debank_key = os.environ.get("DEBANK_API_KEY", "")
if not debank_key:
errors.append("No DEBANK_API_KEY"); return
try:
resp = _proxied_get_with_retry(
"https://pro-openapi.debank.com/v1/user/all_token_list",
params={"id": evm_address, "is_all": "true"},
headers={"AccessKey": debank_key},
)
tokens = resp.json()
by_chain = {}
for t in tokens:
c = t.get("chain", "unknown")
if c not in by_chain:
by_chain[c] = {"tokens": [], "total_usd": 0.0}
usd = t.get("price", 0) * t.get("amount", 0)
by_chain[c]["tokens"].append(t)
by_chain[c]["total_usd"] = round(by_chain[c]["total_usd"] + usd, 2)
evm_usd += usd
result["evm"] = {"address": evm_address, "chains": by_chain, "total_usd": round(evm_usd, 2), "source": "debank"}
except Exception as e:
errors.append(f"DeBank: {e}")
async def _fetch_sol():
nonlocal sol_usd
if not sol_address: return
birdeye_key = os.environ.get("BIRDEYE_API_KEY", "")
if not birdeye_key:
errors.append("No BIRDEYE_API_KEY"); return
try:
resp = _proxied_get_with_retry(
"https://public-api.birdeye.so/wallet/v2/net-worth",
params={"wallet": sol_address},
headers={"X-API-KEY": birdeye_key, "x-chain": "solana", "accept": "application/json"},
)
data = resp.json()
sol_usd = data.get("data", {}).get("totalUsd", 0)
result["solana"] = {"address": sol_address, "source": "birdeye", "data": data, "total_usd": round(sol_usd, 2)}
except Exception as e:
errors.append(f"Birdeye: {e}")
await asyncio.gather(_fetch_evm(), _fetch_sol())
result["total_usd_value"] = round(evm_usd + sol_usd, 2)
if errors: result["errors"] = errors
has_data = "evm" in result or "solana" in result
if not has_data and errors:
return ToolResult(success=False, error="All balance queries failed: " + "; ".join(errors))
return ToolResult(success=True, output=result)
# ── EVM Transfer ─────────────────────────────────────────────────────────────
class WalletTransferTool(BaseTool):
@property
def name(self): return "wallet_transfer"
@property
def description(self): return """Sign and BROADCAST an EVM transaction. Gas is sponsored by default (falls back to user-paid if unavailable). Set sponsor=false to pay gas from wallet balance.
Use '0' amount for contract calls. Policy-gated if enabled."""
@property
def parameters(self): return {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Target address (0x...)"},
"amount": {"type": "string", "description": "Amount in wei"},
"chain_id": {"type": "integer", "description": "Chain ID (default: 1)"},
"data": {"type": "string", "description": "Hex calldata for contract calls"},
"gas_limit": {"type": "string"}, "gas_price": {"type": "string"},
"max_fee_per_gas": {"type": "string"}, "max_priority_fee_per_gas": {"type": "string"},
"nonce": {"type": "string"}, "tx_type": {"type": "integer", "description": "0=legacy, 2=EIP-1559"},
"sponsor": {"type": "boolean", "description": "Gas sponsorship: true=platform pays gas, false=user pays gas from wallet. Omit for auto (try sponsor, fallback to user-paid)."},
},
"required": ["to", "amount"],
}
async def execute(self, ctx: ToolContext, to="", amount="", chain_id=1, data="",
gas_limit="", gas_price="", max_fee_per_gas="", max_priority_fee_per_gas="",
nonce="", tx_type=None, sponsor=None, **kw) -> ToolResult:
if err := _fly_check(): return err
if not to or not amount:
return ToolResult(success=False, error="'to' and 'amount' required")
body = {"to": to, "amount": amount, "chain_id": chain_id}
if data: body["data"] = data
if gas_limit: body["gas_limit"] = gas_limit
if gas_price: body["gas_price"] = gas_price
if max_fee_per_gas: body["max_fee_per_gas"] = max_fee_per_gas
if max_priority_fee_per_gas: body["max_priority_fee_per_gas"] = max_priority_fee_per_gas
if nonce: body["nonce"] = nonce
if tx_type is not None: body["tx_type"] = tx_type
if sponsor is not None: body["sponsor"] = sponsor
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/transfer", body))
except Exception as e:
msg = str(e)
if "policy" in msg.lower():
return ToolResult(success=False, error=f"Policy violation: {msg}")
return ToolResult(success=False, error=msg)
# ── EVM Sign Transaction ────────────────────────────────────────────────────
class WalletSignTransactionTool(BaseTool):
@property
def name(self): return "wallet_sign_transaction"
@property
def description(self): return "Sign an EVM transaction WITHOUT broadcasting. Returns signed tx data."
@property
def parameters(self): return {
"type": "object",
"properties": {
"to": {"type": "string"}, "amount": {"type": "string"},
"chain_id": {"type": "integer"}, "data": {"type": "string"},
"gas_limit": {"type": "string"}, "gas_price": {"type": "string"},
"max_fee_per_gas": {"type": "string"}, "max_priority_fee_per_gas": {"type": "string"},
"nonce": {"type": "string"}, "tx_type": {"type": "integer"},
},
"required": ["to", "amount"],
}
async def execute(self, ctx: ToolContext, to="", amount="", chain_id=1, data="",
gas_limit="", gas_price="", max_fee_per_gas="", max_priority_fee_per_gas="",
nonce="", tx_type=None, **kw) -> ToolResult:
if err := _fly_check(): return err
if not to: return ToolResult(success=False, error="'to' required")
body = {"to": to, "amount": amount, "chain_id": chain_id}
if data: body["data"] = data
if gas_limit: body["gas_limit"] = gas_limit
if gas_price: body["gas_price"] = gas_price
if max_fee_per_gas: body["max_fee_per_gas"] = max_fee_per_gas
if max_priority_fee_per_gas: body["max_priority_fee_per_gas"] = max_priority_fee_per_gas
if nonce: body["nonce"] = nonce
if tx_type is not None: body["tx_type"] = tx_type
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/sign-transaction", body))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── EVM Sign Message ────────────────────────────────────────────────────────
class WalletSignTool(BaseTool):
@property
def name(self): return "wallet_sign"
@property
def description(self): return "Sign a message (EIP-191 personal_sign). Proves wallet ownership."
@property
def parameters(self): return {
"type": "object",
"properties": {"message": {"type": "string", "description": "Message to sign"}},
"required": ["message"],
}
async def execute(self, ctx: ToolContext, message="", **kw) -> ToolResult:
if err := _fly_check(): return err
if not message: return ToolResult(success=False, error="'message' required")
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/sign", {"message": message}))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── EVM Sign Typed Data ─────────────────────────────────────────────────────
class WalletSignTypedDataTool(BaseTool):
@property
def name(self): return "wallet_sign_typed_data"
@property
def description(self): return "Sign EIP-712 structured data (permits, orders, etc.)."
@property
def parameters(self): return {
"type": "object",
"properties": {
"domain": {"type": "object", "description": "EIP-712 domain separator"},
"types": {"type": "object", "description": "Type definitions"},
"primaryType": {"type": "string", "description": "Primary type name"},
"message": {"type": "object", "description": "Data to sign"},
},
"required": ["domain", "types", "primaryType", "message"],
}
async def execute(self, ctx: ToolContext, domain=None, types=None,
primaryType="", message=None, **kw) -> ToolResult:
if err := _fly_check(): return err
if not all([domain, types, primaryType, message]):
return ToolResult(success=False, error="All params required: domain, types, primaryType, message")
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/sign-typed-data", {
"domain": domain, "types": types, "primaryType": primaryType, "message": message,
}))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── EVM Transactions ────────────────────────────────────────────────────────
class WalletTransactionsTool(BaseTool):
@property
def name(self): return "wallet_transactions"
@property
def description(self): return "Get recent EVM transaction history."
@property
def parameters(self): return {
"type": "object",
"properties": {
"chain": {"type": "string"}, "asset": {"type": "string"},
"limit": {"type": "integer", "description": "Max 100"},
},
}
async def execute(self, ctx: ToolContext, chain="ethereum", asset="eth", limit=20, **kw) -> ToolResult:
if err := _fly_check(): return err
try:
qs = f"?chain_type=ethereum&chain={chain}&asset={asset}&limit={limit}"
return ToolResult(success=True, output=await _wallet_request("GET", f"/agent/transactions{qs}"))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Solana Transfer ──────────────────────────────────────────────────────────
class WalletSolTransferTool(BaseTool):
@property
def name(self): return "wallet_sol_transfer"
@property
def description(self): return "Sign and BROADCAST a Solana transaction. User pays gas (SOL required for fees). Policy-gated if enabled."
@property
def parameters(self): return {
"type": "object",
"properties": {
"transaction": {"type": "string", "description": "Base64-encoded Solana tx"},
"caip2": {"type": "string", "description": "CAIP-2 chain ID (default: mainnet)"},
},
"required": ["transaction"],
}
async def execute(self, ctx: ToolContext, transaction="", caip2="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", **kw) -> ToolResult:
if err := _fly_check(): return err
if not transaction: return ToolResult(success=False, error="'transaction' required")
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/sol/transfer", {
"transaction": transaction, "caip2": caip2,
}))
except Exception as e:
msg = str(e)
if "policy" in msg.lower():
return ToolResult(success=False, error=f"Policy violation: {msg}")
return ToolResult(success=False, error=msg)
# ── Solana Sign Transaction ─────────────────────────────────────────────────
class WalletSolSignTransactionTool(BaseTool):
@property
def name(self): return "wallet_sol_sign_transaction"
@property
def description(self): return "Sign a Solana transaction WITHOUT broadcasting."
@property
def parameters(self): return {
"type": "object",
"properties": {"transaction": {"type": "string", "description": "Base64-encoded Solana tx"}},
"required": ["transaction"],
}
async def execute(self, ctx: ToolContext, transaction="", **kw) -> ToolResult:
if err := _fly_check(): return err
if not transaction: return ToolResult(success=False, error="'transaction' required")
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/sol/sign-transaction", {
"transaction": transaction,
}))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Solana Sign Message ─────────────────────────────────────────────────────
class WalletSolSignTool(BaseTool):
@property
def name(self): return "wallet_sol_sign"
@property
def description(self): return "Sign a message with Solana wallet (base64)."
@property
def parameters(self): return {
"type": "object",
"properties": {"message": {"type": "string", "description": "Base64-encoded message"}},
"required": ["message"],
}
async def execute(self, ctx: ToolContext, message="", **kw) -> ToolResult:
if err := _fly_check(): return err
if not message: return ToolResult(success=False, error="'message' required")
try:
return ToolResult(success=True, output=await _wallet_request("POST", "/agent/sol/sign", {"message": message}))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Solana Transactions ──────────────────────────────────────────────────────
class WalletSolTransactionsTool(BaseTool):
@property
def name(self): return "wallet_sol_transactions"
@property
def description(self): return "Get recent Solana transaction history."
@property
def parameters(self): return {
"type": "object",
"properties": {
"chain": {"type": "string"}, "asset": {"type": "string"},
"limit": {"type": "integer"},
},
}
async def execute(self, ctx: ToolContext, chain="solana", asset="sol", limit=20, **kw) -> ToolResult:
if err := _fly_check(): return err
try:
qs = f"?chain_type=solana&chain={chain}&asset={asset}&limit={limit}"
return ToolResult(success=True, output=await _wallet_request("GET", f"/agent/transactions{qs}"))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Get Policy ───────────────────────────────────────────────────────────────
class WalletGetPolicyTool(BaseTool):
@property
def name(self): return "wallet_get_policy"
@property
def description(self): return """Get wallet policy status.
- enabled=false → allow-all (default)
- enabled=true, rules=[] → deny-all
- enabled=true, rules=[...] → rules enforced"""
@property
def parameters(self): return {
"type": "object",
"properties": {
"chain_type": {"type": "string", "enum": ["ethereum", "solana"], "default": "ethereum"},
},
}
async def execute(self, ctx: ToolContext, chain_type="ethereum", **kw) -> ToolResult:
if err := _fly_check(): return err
try:
return ToolResult(success=True, output=await _wallet_request("GET", f"/agent/policy?chain_type={chain_type}"))
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Propose Policy ───────────────────────────────────────────────────────────
class WalletProposePolicyTool(BaseTool):
@property
def name(self): return "wallet_propose_policy"
@property
def description(self): return """Propose a wallet policy update. Sends action_request to frontend for user confirmation.
For both EVM and Solana, call TWICE (once per chain_type)."""
@property
def parameters(self): return {
"type": "object",
"properties": {
"chain_type": {"type": "string", "enum": ["ethereum", "solana"]},
"rules": {"type": "array", "description": "Privy policy rule objects", "items": {
"type": "object",
"properties": {
"name": {"type": "string"}, "method": {"type": "string"},
"conditions": {"type": "array", "items": {"type": "object"}},
"action": {"type": "string", "enum": ["ALLOW", "DENY"]},
},
}},
"title": {"type": "string", "description": "Short title (shown in UI)"},
"description": {"type": "string", "description": "What this policy does"},
},
"required": ["chain_type", "rules", "title", "description"],
}
async def execute(self, ctx: ToolContext, chain_type="", rules=None,
title="", description="", **kw) -> ToolResult:
if not chain_type or chain_type not in ("ethereum", "solana"):
return ToolResult(success=False, error="chain_type must be 'ethereum' or 'solana'")
if rules is None:
return ToolResult(success=False, error="'rules' required")
if not title:
return ToolResult(success=False, error="'title' required")
# Use system validation function
cleaned_rules, validation_errors = _validate_and_clean_rules(rules, chain_type)
if validation_errors:
return ToolResult(
success=False,
error="Rule validation failed:\n" + "\n".join(f"- {e}" for e in validation_errors),
)
# Truncate names to Privy limit
for rule in cleaned_rules:
if isinstance(rule, dict) and "name" in rule and len(rule["name"]) > 50:
rule["name"] = rule["name"][:50]
container_id = os.environ.get("FLY_MACHINE_ID", "") or os.environ.get("FLY_ALLOC_ID", "") or "local-dev"
action_id = f"act_{int(time.time())}_{os.urandom(4).hex()}"
payload = {
"container_id": container_id,
"chain_type": chain_type,
"rules": cleaned_rules,
}
streaming = getattr(ctx, "streaming", None)
if streaming:
streaming.action_request(
action_id=action_id,
action="update_wallet_policy",
title=title,
description=description or title,
payload=payload,
require_signature=True,
)
return ToolResult(success=True, output={
"status": "action_request_sent",
"action_id": action_id,
"message": f"Policy proposal sent. Chain: {chain_type}, Rules: {len(cleaned_rules)}.",
})
else:
return ToolResult(
success=False,
error="Streaming context not available — cannot send action_request.",
)
Related skills
How it compares
Pick wallet over generic Web3 SDK skills when you need pre-wired agent tools for balances, transfers, and signing on both EVM and Solana.
FAQ
What does wallet do?
|
When should I use wallet?
|
Is wallet safe to install?
Review the Security Audits panel on this page before installing in production.