Skip to content

[New Skill]: defi/evm_tx_handler — NLP-driven agent wallet for EVM buy/sell/send #142

@rosspeili

Description

@rosspeili

Skill Name

defi/evm_tx_handler

What should this skill do?

Summary

A Skillware skill that lets AI agents operate a dedicated EVM agent wallet to perform basic on-chain actions — buy, sell, and send — from structured trade intent, not slash commands.

The LLM handles natural language (e.g. “buy 10 Degen on Base”). The skill handles deterministic Web3: resolve names → quote → preview → (optional confirm) → sign → broadcast → wait for receipt.

Inspiration only (do not copy): OPSIE web3_handler.py — useful for feature ideas (multi-chain, token registry, Uni V2 swaps, gas policy, preview/confirm). Reimplement cleanly for Skillware; no /0x commands, no input() loops, no editing Python to add tokens.

Problem

Agents need a safe, repeatable way to trade and transfer on EVM chains without hard-coded CLI syntax. Users speak in NL; the skill enforces typed requests, previews, and optional confirmation before signing.

Agent vs skill

Layer Responsibility
Agent Parse user NL → partial intent JSON; ask for missing fields in natural language; show preview; pass confirmed: true when user approves
Skill Merge intent with config/YAML registries; validate; quote on-chain; build/sign txs; async receipt wait; return structured JSON

No regex NLP inside the skill — the agent fills the schema per instructions.md.

Architecture (bundle)

skills/defi/evm_tx_handler/
├── manifest.yaml
├── skill.py              # resolve | quote | preview | execute | transfer | balances | wallet_info | update_preferences
├── instructions.md       # NL patterns, missing-field dialogue, safety
├── card.json
├── test_skill.py
├── config.yaml.example   # long-term prefs (no secrets)
└── data/
    ├── chains.yaml       # chain id, RPC env key, native token, explorer, router
    ├── tokens.yaml       # friendly name → contract per chain
    └── addressbook.yaml  # label → address (e.g. mom, george)

Configuration layers

Layer Holds Persists
.env AGENT_WALLET_PRIVATE_KEY, *_RPC_URL, optional API keys secrets only
config.yaml default chain, default spend asset, gas policy, slippage, confirm toggle, optional caps long-term; change only via update_preferences or explicit user intent
Per-request intent / overrides fields for this tx (e.g. aggressive gas once) does not overwrite config unless user asks to change defaults
chains.yaml / tokens.yaml / addressbook.yaml registry data user-editable YAML

Defaults: chain ethereum; spend asset native ETH when buying unless config or user specifies; gas normal unless user says low / high / aggressive; confirm_before_send: true unless config disables it.

Example agent flow

  1. User: “Buy 10 Degen on Base”
  2. Agent → resolve with partial intent → skill returns missing_fields: ["spend_asset"], suggested_defaults: { "spend_asset": "usdc" } from config
  3. Agent: “Sure — what should I pay with? I can use USDC from your config.”
  4. User: “Use USDC”
  5. Agent → quote → preview (you give X USDC, get ~10 DEGEN, rate, gas, router)
  6. User confirms (if enabled) → agent → execute with confirmed: true
  7. Skill returns tx_hash, explorer URL, receipt status

Send example: “Send 10 degen to mom” — resolves token + addressbook label; fills chain/gas from config.

MVP scope

  • Chains: Ethereum + Base (extensible via chains.yaml)
  • Actions: buy, sell, transfer (native + ERC20)
  • DEX: Uniswap V2-compatible router per chain (verify addresses; do not trust OPSIE literals)
  • web3.py v6+, EIP-1559 gas, correct ERC20 decimals, async receipt wait
  • Tests: mocked Web3; no live keys in CI

Out of scope (v1)

  • Uniswap V3 / aggregators (1inch, 0x)
  • Cross-chain bridges
  • HD wallet generation inside skill (document: create dedicated wallet externally, key in .env only)
  • NLP/regex parsing inside skill.py
  • Using personal/main wallet — dedicated agent wallet only

Security & constitution (manifest)

  • Dedicated agent wallet only; strong disclaimer that agents mis-parse and markets move
  • Private key only in .env; never in tool args, YAML, or logs
  • Optional: max_trade_usd, chain/token allowlists
  • Fail closed if required env or registry entry missing
  • Not financial or legal advice

Reference features from OPSIE (reimplement, don’t port)

Worth studying: multi-chain connect, token dict, address labels, buy/sell path quotes, approve+swap, gas strategy, preview before send, wait_for_transaction_receipt. Avoid: slash commands, interactive prompts, mutating source for config, hardcoded pub key, legacy-only gas, wrong decimal handling.

Ideal Inputs & Outputs

Tool shape

Single skill, action-based execute() (model-agnostic). Agent passes structured JSON; skill returns structured JSON.

Actions

action Purpose
resolve Merge intent + config + YAML registries; return missing fields + suggestions
quote On-chain quote (amounts in/out, path, slippage)
preview Human/agent-readable summary (also included in quote)
execute Sign and send swap (requires confirmed: true when config says so)
transfer Send native or ERC20 to address or addressbook label
balances Wallet balances on a chain
wallet_info Agent address, supported chains, prefs summary (no secrets)
update_preferences Persist long-term config.yaml changes only when user explicitly intends

Intent object (partial OK on resolve)

{
  "side": "buy | sell | send",
  "chain": "base",
  "target_asset": "degen",
  "spend_asset": "usdc",
  "amount": 10,
  "amount_kind": "target_out | spend_in",
  "recipient": "mom",
  "gas_policy": "normal | low | high | aggressive",
  "slippage_bps": 50,
  "confirmed": false
}

Field notes

  • chain — default from config (ethereum) if omitted
  • spend_asset — default from config for buys; else native ETH on chain
  • gas_policy — default normal from config; per-tx override does not persist
  • recipient — addressbook label or 0x… for send
  • amount_kind — for “buy 10 Degen”, amount is target_out

Example: resolve (missing spend asset)

Input

{
  "action": "resolve",
  "intent": {
    "side": "buy",
    "chain": "base",
    "target_asset": "degen",
    "amount": 10,
    "amount_kind": "target_out"
  }
}

Output

{
  "status": "needs_input",
  "resolved": {
    "chain": "base",
    "target_asset": "degen",
    "amount": 10,
    "amount_kind": "target_out",
    "gas_policy": "normal"
  },
  "missing_fields": ["spend_asset"],
  "suggested_defaults": {
    "spend_asset": "usdc"
  },
  "agent_hint": "Ask which token to pay with; suggest USDC from config."
}

Example: quote + preview

Input

{
  "action": "quote",
  "intent": {
    "side": "buy",
    "chain": "base",
    "target_asset": "degen",
    "spend_asset": "usdc",
    "amount": 10,
    "amount_kind": "target_out"
  }
}

Output

{
  "status": "ready",
  "preview": {
    "side": "buy",
    "chain": "base",
    "you_pay": { "asset": "usdc", "amount": "123.45" },
    "you_receive": { "asset": "degen", "amount": "10" },
    "rate": "1 usdc = 0.081 degen",
    "gas_estimate": { "policy": "normal", "max_fee_gwei": "0.05" },
    "router": "uniswap_v2",
    "usd": { "pay": 123.45, "receive": 0.10 },
    "warnings": ["Rates are indicative; slippage may apply."]
  },
  "quote": {
    "path": ["0x...", "0x..."],
    "amount_in_wei": "...",
    "amount_out_wei": "...",
    "min_out_wei": "...",
    "deadline": 1710000000
  },
  "requires_confirmation": true
}

Example: execute (after user confirms)

Input

{
  "action": "execute",
  "intent": {
    "side": "buy",
    "chain": "base",
    "target_asset": "degen",
    "spend_asset": "usdc",
    "amount": 10,
    "amount_kind": "target_out"
  },
  "confirmed": true
}

Output

{
  "status": "confirmed",
  "tx_hash": "0x...",
  "explorer_url": "https://basescan.org/tx/0x...",
  "receipt": {
    "block_number": 123,
    "gas_used": 250000,
    "success": true
  }
}

Example: transfer

Input

{
  "action": "transfer",
  "intent": {
    "chain": "base",
    "target_asset": "degen",
    "amount": 10,
    "recipient": "mom"
  },
  "confirmed": true
}

Output

{
  "status": "confirmed",
  "tx_hash": "0x...",
  "explorer_url": "https://basescan.org/tx/0x...",
  "recipient_resolved": "0x..."
}

config.yaml.example (long-term prefs, no secrets)

default_chain: ethereum
default_spend_asset: usdc
gas_policy: normal
confirm_before_send: true
slippage_bps: 50
max_trade_usd: 500

Environment variables (manifest env_vars)

Variable Required Purpose
AGENT_WALLET_PRIVATE_KEY yes Dedicated agent wallet only
ETHEREUM_RPC_URL yes (if chain used) Ethereum JSON-RPC
BASE_RPC_URL yes (if chain used) Base JSON-RPC
COINGECKO_API_KEY no USD estimates in preview
ETHERSCAN_API_KEY no Optional verification/links

Private key referenced in config as private_key_env: AGENT_WALLET_PRIVATE_KEY — never inline.


Dependencies

  • web3 ≥ 6.x
  • pyyaml
  • requests or httpx (CoinGecko, optional)

Acceptance criteria (for implementation PR)

  • SkillLoader.load_skill("defi/evm_tx_handler") succeeds
  • resolve returns missing fields + defaults from config
  • quote / preview for buy and sell on mocked Uni V2 router
  • execute refuses when confirm_before_send: true and confirmed is false
  • transfer resolves addressbook labels
  • Per-tx gas override does not mutate config.yaml
  • update_preferences persists only explicit preference changes
  • No private key in logs or returned JSON
  • pytest passes with mocked Web3 (no live keys in CI)
  • instructions.md, card.json, docs/skills/evm_tx_handler.md per skill standard

Targeted Models (if applicable)

Model Agnostic (All)

Metadata

Metadata

Assignees

Labels

discussionOpen discussion for RFCs and proposals.enhancementNew feature or requesthelp wantedExtra attention is neededskill requestRequest for a new capability to be added.
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions