Skip to content

feat: uniswap-trading AI-agent skill (D3 skeleton) - #478

Draft
TimeToBuildBob wants to merge 7 commits into
uniswap-python:masterfrom
TimeToBuildBob:feat/agent-skill-skeleton
Draft

TimeToBuildBob wants to merge 7 commits into
uniswap-python:masterfrom
TimeToBuildBob:feat/agent-skill-skeleton

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

What

Skeleton for the AI-agent integration (grant deliverable D3): a `skills/uniswap-trading/` directory containing

  • SKILL.md — Anthropic skill format; drops into Claude Code (`~/.claude/skills/`) or gptme as-is
  • scripts/uniswap_agent.py — subprocess CLI any agent framework can call: `status` / `quote` / `swap` / `balance` / `approve`, `--json` output, stable exit codes (0 ok / 1 runtime / 2 safety refusal)
  • Safety gates: dry-run by default (nothing signed without `--broadcast`), broadcast restricted to testnets unless `UNISWAP_AGENT_ALLOW_MAINNET=1`, 5% slippage ceiling
  • tests/ — 17 offline tests for the gates and CLI surface (no network)
  • demo_sepolia.sh — runnable demo

Verified

  • Live v3 quote (mainnet, public RPC): 1 WETH → 2513.82 USDC
  • Live v4 quote via V4Quoter: 1 ETH → 2513.78 USDC (consistent ✓)
  • Mainnet broadcast refused with exit 2; dry-run plans render min-out from slippage
  • `python3 -m pytest skills/uniswap-trading/tests/ -q` → 17 passed

Known gap (for the Sepolia tx demo)

The testnet demo can't broadcast yet because v0.8.0 has no Sepolia path:

  • v3: `uniswap.py` hardcodes the mainnet quoter/router addresses regardless of chain
  • v4: the `constants.py` address maps are production-only. Official Sepolia v4 deployments cover 6 of the 8 contracts `Uniswap4.init` loads (addresses in the README); `position_descriptor` and `reserves_lens` still need Sepolia deployments/addresses

That enablement is D2-adjacent library work — leaving it out of this PR deliberately.

Status

Draft skeleton for @liquid-8 to take over, extend, or rewrite as part of the funded D3 work — no attachment to any of it. TODO list in `skills/uniswap-trading/README.md`.

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.61%. Comparing base (02c0368) to head (25ec81e).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #478   +/-   ##
=======================================
  Coverage   70.61%   70.61%           
=======================================
  Files          14       14           
  Lines        4302     4302           
=======================================
  Hits         3038     3038           
  Misses       1264     1264           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@greptile-apps

greptile-apps Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds an agent-oriented Uniswap trading skill with a subprocess CLI, safety gates, documentation, an offline test suite, and a Sepolia demo scaffold.

  • Supports status, quote, swap, balance, and approval operations across Uniswap versions 2–4.
  • Keeps state-changing operations dry-run by default and gates broadcasts by network, private-key availability, and slippage.
  • Validates configured wallet addresses before balance client construction.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current balance path rejects absent, malformed, and zero wallet addresses before constructing the client.

Important Files Changed

Filename Overview
skills/uniswap-trading/scripts/uniswap_agent.py Adds the agent CLI and now rejects absent, malformed, and zero wallet addresses before executing balance requests.
skills/uniswap-trading/tests/test_safety_gates.py Adds offline coverage for broadcast restrictions, slippage, token resolution, command parsing, and wallet-address refusal.
skills/uniswap-trading/SKILL.md Documents setup, commands, safety rules, output semantics, and current network limitations.
skills/uniswap-trading/demo_sepolia.sh Provides read-only mainnet demonstrations and an explicitly gated Sepolia broadcast scaffold.
skills/uniswap-trading/README.md Describes installation, validation, known Sepolia limitations, and remaining implementation work.

Reviews (3): Last reviewed commit: "fix(skill): validate balance wallet addr..." | Re-trigger Greptile

Comment thread skills/uniswap-trading/scripts/uniswap_agent.py
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread skills/uniswap-trading/scripts/uniswap_agent.py Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI failure analysis (session b7c3)

The 3 failing tests are pre-existing failures unrelated to this PR:

tests/test_uniswap.py::TestUniswap::test_get_price_output[1-ETH-UNI-1000000000000000000] FAILED
tests/test_uniswap.py::TestUniswap::test_get_price_output[1-UNI-ETH-10000000000000000]  FAILED
tests/test_uniswap.py::TestUniswap::test_get_price_output[1-ETH-UNI-2000000000000000000] FAILED

Root cause: EVM error InvalidJump from the Uniswap v1 UNI exchange contract (0x6fa90CcE27Bba2535B9329e340FD0cCebd358100) when calling getEthToTokenOutputPrice. This indicates the contract likely has no remaining liquidity — calling the v1 price output function with zero liquidity causes an InvalidJump in the Vyper bytecode.

Evidence these are pre-existing:

  • The same 3 tests failed on run 33101882352 (the very first push on this branch, before any skill-specific commits were added)
  • This branch only adds files in skills/uniswap-trading/ — no changes to tests/test_uniswap.py or the uniswap/ library
  • All skill-specific tests pass: skills/uniswap-trading/tests/test_safety_gates.py — 20/20 ✅

Suggested fix: Mark these v1 ETH-UNI output price tests with @pytest.mark.xfail(reason="Uniswap v1 UNI exchange has no liquidity — InvalidJump", strict=False) to keep CI green without hiding the underlying issue.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Folded in corrections from a cross-agent dogfood round (Gordon, a trading-focused agent on our infra, ran the branch end-to-end: clean install, all tests passing, working mainnet v3/v4 quotes plus Polygon v3 quoting on the 0.05% WETH/USDC.e pool — and plans to replace ~150 LOC of his own swap tooling with this once released):

  • Human-readable amounts: quote/swap/balance JSON now carries advisory <field>_human + decimals_<field> companions (best-effort ERC-20 decimals(); omitted rather than fatal on lookup failure — integer base-unit fields remain the source of truth)
  • Deterministic v3-testnet refusal: v3 on testnets now exits 2 (safety, non-retryable) with an explanation, instead of surfacing the mainnet-hardcoded contract addresses as a retryable-looking BadFunctionCallOutput
  • Setup docs install from this branch until a release contains the skill
  • Double-armed mainnet broadcast: non-testnet broadcasting now requires BOTH UNISWAP_AGENT_ALLOW_MAINNET=1 (operator's standing decision) AND --allow-mainnet per call

Tests 18 → 28; verified live against public RPCs (mainnet quote with human fields, Sepolia refusal, single-arm broadcast refusal). The dogfood run itself doubles as early adoption evidence for the D3 baseline: two independent agent integrations (gptme, plus Gordon's trading stack) across two chains.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Follow-up for the red test (v1, mainnet) check: the 3 failures are pre-existing on master — the v1 UNI exchange contract has no remaining liquidity (EVM error InvalidJump), unrelated to this branch. Opened #479 to xfail those cases on master so CI goes green here and for every future PR; once merged, re-running this PR's checks should show fully green. No re-diagnosis needed.

@TimeToBuildBob
TimeToBuildBob force-pushed the feat/agent-skill-skeleton branch from 3bcc0c2 to c5f495f Compare October 11, 2026 02:27
TimeToBuildBob and others added 5 commits October 15, 2026 02:00
Agent-facing integration surface for uniswap-python: an Anthropic-format
SKILL.md plus a subprocess CLI (quote/swap/balance/approve/status) with
JSON output and safety gates (dry-run by default, testnet-only broadcast,
slippage ceiling). Offline tests for the gates; demo script verified live
against mainnet v3+v4 quoters via public RPCs.

Groundwork for UF grant deliverable D3 (AI-agent integration + testnet
demo). Sepolia broadcast is blocked on library-side Sepolia enablement —
documented in the README with the official v4 Sepolia addresses.

Co-Authored-By: Bob <[email protected]>
Git-Session-Id: d14a73a4-480f-4938-9c06-fcbadbfb603c
Git-Session-Id: d14a73a4-480f-4938-9c06-fcbadbfb603c
Git-Session-Id: d14a73a4-480f-4938-9c06-fcbadbfb603c
Git-Session-Id: d14a73a4-480f-4938-9c06-fcbadbfb603c
Four corrections from Gordon's dogfood run (trading-agent perspective):

- Human-readable amounts: quote/swap/balance outputs now carry advisory
  <field>_human + decimals_<field> companions (best-effort ERC-20 decimals;
  omitted, never fatal, when the lookup fails)
- Deterministic v3-testnet refusal: v3 on testnets exits 2 (safety, don't
  retry) instead of surfacing the library's mainnet-hardcoded contracts as
  a retryable-looking runtime error
- Setup docs install from the PR branch until a release contains the skill
- Non-testnet broadcast is now double-armed: UNISWAP_AGENT_ALLOW_MAINNET=1
  (operator standing decision) AND --allow-mainnet (per-call confirmation)

Tests 18 -> 28; verified live: mainnet quote with human fields, Sepolia v3
refusal, single-arm mainnet broadcast refused.

Git-Session-Id: d14a73a4-480f-4938-9c06-fcbadbfb603c
@TimeToBuildBob
TimeToBuildBob force-pushed the feat/agent-skill-skeleton branch from c5f495f to 2cdf77f Compare October 15, 2026 02:00
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Rebased onto current master (post-#491). The five CI/test-fix commits that used to sit under the skill were #479's content and are superseded by #491, so this PR now carries only the five skill commits: skills/uniswap-trading/ (824 lines, no library changes). Offline safety-gate tests: 28 passed. Mergeable again.

…reset

Git-Session-Id: 4dfa6bdf-a702-43e1-b0bf-4a7e07461cdb
v4 uses deprecated Node.js 20 (forced to Node.js 24 in recent runners)
which causes intermittent upload failures that block otherwise-green CI.
v5 is the supported release. fail_ci_if_error=false: coverage upload is
instrumentation, not a correctness gate — transient Codecov outages should
not fail the test run.

Git-Session-Id: 80b13aa8-1c0d-4abb-82f2-2eda9d90539b
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Sepolia failure list (from running the demo against a real Sepolia fork)

Per the "Known gap" section above, here is the concrete list of where the Sepolia path fails, reproduced empirically. This is the scope for the library PR.

Setup: poetry install in this repo, plus a local fork of Sepolia (anvil --fork-url https://ethereum-sepolia-rpc.publicnode.com --chain-id 11155111 --port 8545) so the demo could run against real Sepolia contract state without faucet ETH. Public RPC: ethereum-sepolia-rpc.publicnode.com (eth_chainId → 0xaa36a7).

1. v4 is hard-blocked on chain 11155111. Uniswap4.__init__ raises before any call:

Exception: Netid 11155111 is not supported yet.

uniswap/uniswap4.py guards on _quoter_contract_addresses_v4, but then indexes eight v4 maps by self.net_name: quoter, router, stateview, permit2, poolmanager, position_manager, position_descriptor, reserves_lens. None of the eight has a sepolia entry, so the fix is eight map entries, not one.

2. position_descriptor and reserves_lens are not deployed on Sepolia at all. The other six are (verified eth_getCode non-empty): PoolManager 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543, UniversalRouter 0x3A9D48AB9751398BbFa63ad67599Bb04e4BdF98b, V4Quoter 0x61b3f2011a92d183c7dbadbda940a7555ccf9227, StateView 0xe1dd9c3fa50edb962e442f60dfbc432e24537e4c, PositionManager 0x429ba70129df741B2Ca2a85BC3A2a3328e5c09b4, Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3.

The existing try/except NameNotFound around each contract load does not help, because the address lookup sits outside the try:

position_descriptor_address = _position_descriptor_contract_addresses_v4[self.net_name]
reserves_lens_address = _reserves_lens_contract_addresses_v4[self.net_name]

A KeyError there is uncaught. Those two lookups need to become optional (.get() + skip the load).

3. v3 uses mainnet addresses on every chain. uniswap/uniswap.py (v3 branch) hardcodes the mainnet factory / quoter / router / position manager. All four are empty on Sepolia (eth_getCode → 0x), so a v3 quote against the fork fails with BadFunctionCallOutput: Could not transact with/call contract function. Meanwhile v4 fails earlier with the netid error, so on Sepolia the skill can do neither v3 nor v4 today.

4. Sepolia does have a real v3 deployment — the fix is per-chain addresses, not a new deployment:

Contract Sepolia address
V3Factory 0x0227628f3F023bb0B980b67D528571c95c6DaC1c
SwapRouter 0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E
WETH9 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14
USDC 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238

(router.factory() → the factory above; router.WETH9() → the WETH9 above.) WETH/USDC pools exist at fee tiers 100 / 500 / 3000 / 10000, and the fee-3000 pool has liquidity (liquidity() → 8.79e15), so a v3 Sepolia swap is viable once the library uses per-chain addresses.

5. A v4 Sepolia swap additionally needs a live pool. PoolManager.getPool() reverted for every (token0, token1, fee, tickSpacing) combination tried (ETH/WETH × USDC, fees 100/500/3000/10000, tick spacings 1/10/60/200), and StateView.getSlot0 could not be decoded for the derived pool ids. So v4 on Sepolia is not just an address-map change: a pool with liquidity has to be located or initialized. A v3 demo does not need this.

Also: uniswap/tokens.py get_tokens("sepolia") raises "Unknown net" — the skill's own alias table already carries Sepolia WETH/USDC, but the library token map does not.

Scope for the library PR

  1. uniswap/constants.py — add sepolia to the v4 maps (quoter, router, stateview, permit2, poolmanager, position_manager).
  2. uniswap/constants.py + uniswap/uniswap.py — make the v3 factory/quoter/router/position-manager addresses chain-dependent, with the Sepolia deployment above.
  3. uniswap/uniswap4.py — make position_descriptor and reserves_lens optional per chain.
  4. uniswap/tokens.py — add a Sepolia token map.
  5. (v4 only) locate or initialize a Sepolia v4 pool with liquidity.

Ownership

I have not opened the library PR for this. The grant notes list Sepolia enablement as D2-adjacent (Yohan's) work, and the two optional-contract choices in (3) are design calls in that area — so this should be picked up by Yohan with Erik reviewing, rather than landing as a parallel branch. Flagging here so the scope is not rediscovered. @liquid-8 @ErikBjare — is the library PR yours, and should I rebase this skill PR onto it once it lands?

(For reference, the full write-up with commands and receipts is in Bob's workspace: knowledge/research/2026-10-21-uniswap-python-sepolia-failure-list.md.)

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

One correction/addition to the scope above: the v3 Quoter on Sepolia is still unlocated.

uniswap.py loads the v3 quoter as a separate contract from the router, so a per-chain v3 fix needs the Sepolia quoter address too — not just factory + router + position manager. I verified the factory, SwapRouter, WETH9 and USDC (see the table), but every quoter candidate I probed comes back with empty code, and v3-periphery/deploys.md only lists mainnet-style addresses while the v3 SDK npm tarball ships no deployments.json in the versions I checked.

So scope item 2 is not fully specified until that one address is found. Anyone with the Sepolia v3 deployment artifact (a Uniswap interface config, or a v3-periphery deployment JSON for chain 11155111) can close it in a minute — happy to do it myself if you point me at one.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Follow-up: the quoter gap is closed, and it turned up a second library requirement I didn't have before.

Full Sepolia v3 deployment (found by walking the factory's deployer 0x3344BBDCeb8f6fb52de759c127E4A44EFb40432A contract-creation txs; every address confirmed non-empty via eth_getCode on ethereum-sepolia-rpc.publicnode.com):

Contract Sepolia address
UniswapV3Factory 0x0227628f3F023bb0B980b67D528571c95c6DaC1c
SwapRouter02 0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E
QuoterV2 0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3
NonfungiblePositionManager 0x1238536071E1c677A632429e3655c799b22cDA52
TickLens 0x0b343475d44EC2b4b8243EBF81dc888BF0A14b36
UniswapV3Staker 0x6CAB6be593731f560d9792094EbB6d663e48F983
V3Migrator 0x729004182cF005CEC8Bd85df140094b6aCbe8b15
NFTDescriptor 0x3B5E3c5E595D85fbFBC2a42ECC091e183E76697C
ProxyAdmin 0x5d3ceBAb8fbA22B48d725a40452e874F24E537f4
Wrapped Ether (WETH9) 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14
UniswapV2Router02 0x425141165d3DE9FEC831896C016617a52363b687
UniswapV2Factory 0xB7f907f7A9eBC822a80BD25E224be42Ce0A698A0

Second finding: Sepolia runs QuoterV2, the library ships the v1 quoter ABI. uniswap/assets/uniswap-v3/quoter.abi is the v1 quoter — 5 positional args quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96). Sepolia's contract is QuoterV2 — one struct arg quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)). Calling the v1 signature against the v2 contract reverts with empty data, which is exactly the BadFunctionCallOutput the library surfaces.

Verified against the Sepolia fork: the v2 struct call returns a real quote.

QuoterV2 quote 0.001 WETH -> USDC (fee 3000) = 49735253  (49.73 USDC)

Revised final scope for the library PR (6 items):

  1. constants.py — add sepolia to the v4 maps (quoter, router, stateview, permit2, poolmanager, position_manager).
  2. constants.py + uniswap.py — make the v3 factory/quoter/router/position-manager addresses chain-dependent (table above).
  3. uniswap4.py — make position_descriptor and reserves_lens optional per chain (not deployed on Sepolia; their address lookups raise KeyError outside the existing try/except).
  4. tokens.py — add a Sepolia token map (get_tokens("sepolia") currently raises "Unknown net").
  5. assets/uniswap-v3/quoter.abi + uniswap.py — support QuoterV2 (struct-arg) in addition to the v1 positional signature, or ship a v2 ABI and branch on the call style.
  6. (v4 only) locate or initialize a Sepolia v4 pool with liquidity — a v3 demo does not need this.

Items 1–5 are enough for a working v3 Sepolia tx demo. Item 6 is only needed for v4.

Still my question from the previous comment: is the library PR yours (@liquid-8) with @ErikBjare reviewing, or should I take it? Say the word and I'll implement all five and rebase this PR onto it.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Root cause found: the Sepolia router is an early v3-periphery SwapRouter02 — and the v3 swap now works

Two follow-ups to the scope above. The first is a root cause for the swap revert; the second changes the scope from 6 items to 7.

1. The swap revert was a selector mismatch, not a pool problem

The revert signature was the clue: empty revert data, no sub-calls at all, tiny gas. That is a call that matched no function and fell through to the fallback — not a swap that failed.

Checked against the deployed bytecode of 0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E (24497 bytes), with selectors computed from the verified ABI (Blockscout, contracts/SwapRouter02.sol, solc 0.7.6) rather than guessed:

Selector Signature In bytecode
0x414bf389 modern 8-field exactInputSingle absent
0x04e45aaf early 7-field exactInputSingle present
0xb858183f exactInput((bytes,address,uint256,uint256)) present (early 4-field path)

The verified source explains it: Sepolia's only v3 router is the early v3-periphery SwapRouter02, whose ExactInputSingleParams has no deadline field:

struct ExactInputSingleParams {
    address tokenIn; address tokenOut; uint24 fee; address recipient;
    uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96;
}   // 7 fields — vs. the modern 8-field struct with `deadline`

The library ships the modern 8-field ABI (uniswap/assets/uniswap-v3/router.abi), so the library's call encoded a selector that does not exist on Sepolia and reverted with empty data.

Also visible in the verified function list, consistent with the early release: factoryV2, positionManager, callPositionManager, checkOracleSlippage, swapExactTokensForTokens, wrapETH, pull, approveMax — a V2+V3 hybrid router.

Correction to my earlier note: the "degenerate pool reserves" theory was wrong. The fee-3000 WETH/USDC pool 0x6Ce0896eAE6D4BD668fDe41BB784548fb8F59b50 holds 3,889,867 USDC and 52.89 WETH (balanceOf on the fork), liquidity() = 8.79e15, slot0().tick = 168143. Reserves were ample — the payout was never the problem. The odd ~49 USDC per 0.001 WETH quote is just the fork's notional price curve, and the swap honours it.

2. The v3 Sepolia swap is now proven

Calling the early 7-field exactInputSingle (native ETH as value, tokenIn = WETH9, tokenOut = USDC, fee 3000, amountIn = 1e15) against a Sepolia fork:

eth_call  → amountOut = 49735064  (49.7351 USDC)
tx        → 0xf51c7019e9b1bd26d54653fa35276b2dda4bca4492a91961e52923bead539a23
            status 1, gasUsed 123072
acct USDC → 249,735,064  (200,000,000 before + 49,735,064 received)

So: address + QuoterV2 + early router ABI → the v3 Sepolia swap works. The only thing still missing for a real Sepolia tx hash is a funded key (no faucet ETH reachable from here).

3. Scope item 7 — the library needs per-chain router ABI selection, not just per-chain addresses

One ABI cannot serve both chains, because the selector differs and the wrong one silently falls through to the fallback:

  • uniswap/assets/uniswap-v3/router.abi — the modern 8-field router ABI (mainnet, and any chain on a current v3-periphery release).
  • A Sepolia-specific early router ABI with the 7-field struct, selected per chain.

This is the same defect class as the QuoterV2 finding: the library hardcodes one chain's ABI, and Sepolia runs different versions of both the quoter and the router. Items 5 and 7 together mean the library cannot speak to Sepolia without a per-chain ABI selection mechanism.

Final scope: 7 items (v4 map entries; chain-dependent v3 addresses; optional position_descriptor/reserves_lens; Sepolia token map; QuoterV2 ABI; v4 pool discovery — v4 only; per-chain v3 router ABI).

Ownership question still stands: is the library PR yours (Yohan) or mine? I've deliberately not opened one. Happy to implement all 7 items if it's mine — the scope is now fully specified with receipts.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant