Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Degenbot

A Rust MEV-bot core with a first-class Python driver shell, for Uniswap (V2, V3, V4), Curve V1, Solidly V2, Balancer V2, and Aave V3 integrations on EVM-compatible blockchains.

Degenbot has two equally first-class consumers sharing one Rust core:

  • Pure-Rust MEV bot — cargo add degenbot (the umbrella crate re-exporting the cores; a git/path dependency until the workspace is published to crates.io) and build a fully functional MEV bot in Rust only.
  • Python-driven MEV bot — drive the same Rust core from Python through a thin PyO3 layer that translates Python calls into Rust calls.

The Rust core is the engine; Python is a driver shell, not a co-implementation. Pool/token state, swap math, event decoding, solvers, the pump loop, and swap encoding all live in Rust core crates; the Python layer provides the user-facing API, orchestration, and configuration. See docs/adr/ADR-005-polars-inspired-three-layer-architecture.md for the architectural vision.

Contents

Debugging a failing settlement-arbitrage path, or building your own simulation harness? See INVESTIGATIONS.md — the simulation oracle driver, the per-contract scaffolder, and the path-fixture toolkit.

Overview

Degenbot abstracts the implementation details of Uniswap liquidity pools and their underlying ERC-20 tokens into a set of Rust core crates exposed to Python through a thin PyO3 binding layer. The Rust core owns all performance-critical and stateful logic — pool/token state, swap math, event decoding, solvers, the pump loop, and swap encoding — while the Python companion provides the user-facing API, docstrings, and I/O orchestration.

The Rust core also owns the operator-facing infrastructure: the settlement-arbitrage engine and pump loop, the in-process revm simulation engine, on-chain price readers, the DB-aware pool/Aave updaters, EIP-1559 transaction signing and submission, and WS/HTTP pub-sub. Python remains the user-facing API, configuration, and registry driver, while the Rust degenbot-db crate owns the SQLite schema, file lifecycle, and read/write operations. Python consumers use the stable typed mirror in degenbot.db; there is no Python SQLAlchemy session or ORM layer.

These classes serve as building blocks for the lessons published by BowTiedDevil on Degen Code.

Architecture: The Python-Rust Split

Ownership is strict, which is what makes both consumption paths first-class: the Rust core owns everything stateful and performance-critical — pool/token state, swap math, event decoding, solvers, the pump loop, and swap encoding — while the Python side owns the user-facing API, orchestration, and configuration. The core crates contain no PyO3 code at all, so a pure-Rust bot (cargo add degenbot) runs without any Python machinery; an in-repo proof is rust/crates/facade/degenbot/examples/standalone_consumer.rs. Architectural decisions — state ownership (ADR-003), FFI topology (ADR-005), schema cutover (ADR-010) — are recorded in the ADR design log, with the crate sources as the last word.

Installation

Requirements

  • Python 3.12+
  • pip, uv, or similar package management tool

From PyPI

pip install degenbot

From Source

git clone https://github.com/BowTiedDevil/degenbot.git
cd degenbot
just bootstrap  # or: pip install -e . for release-equivalent defaults

Quick Start

The Bot class is the central session object for all degenbot operations. It manages connections, registries, and provides factory methods for creating pools and tokens:

# RPC_URL is any HTTP RPC endpoint for chain 1, for example
#   "https://eth-mainnet.example.com"
# Initialize Bot from explicit settings
bot = degenbot.Bot(
    chain_id=1,
    node=RPC_URL,
    database="~/.local/state/degenbot/db/degenbot.db",
)

# Bot constructs the RPC provider from the node endpoint and enforces its
# eth_chainId matches chain_id (fail-fast). No manual provider
# registration is needed.
# Create pools and tokens through Bot (I/O-free when possible)
pool = bot.build_pool("0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")
token = bot.build_erc20token("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")  # WETH

# Pools are I/O-free - all data injected at construction
print(f"Pool: {pool.name}")
print(f"Token: {token}")

# Calculate swaps without any network calls
amount_out = pool.calculate_tokens_out_from_tokens_in(
    token_in=pool.token0,
    token_in_quantity=10**18,
)
print(f"Output: {amount_out}")

Direct Pool Construction (Advanced)

Pool classes are Python companions over Rust-owned pool state — direct construction is impossible (any constructor call raises TypeError); a pool comes into being only by registering in a Bot's Rust state. Use Bot.build_pool() in production (or the make_*_pool test helpers in tests):

# Do NOT do this — the constructor always raises TypeError:
try:
    degenbot.UniswapV3Pool("0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")  # ← BROKEN!
    raise AssertionError("direct construction of a pool should be impossible")
except TypeError:
    pass

# Instead, always use Bot to construct pools (registers in Rust state,
# returns the Python companion wrapper):
pool = bot.build_pool("0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")

Core Concepts

I/O-Free Architecture

Degenbot pools follow an I/O-free architecture where on-chain data is fetched at construction time and injected into pool objects. After construction, pools are pure calculation objects with no network dependencies. For the Uniswap V2/V3/V4 families (including Aerodrome and Balancer), Bot.build_pool() performs that full choreography on the Rust side; Curve pools and token metadata use the equivalent Python-side builders. Either way, the pool you receive needs no network access at construction time.

Benefits:

  • Testability: Easy to create test fixtures with mocked data
  • Performance: Swap calculations are pure math, no network calls
  • Reliability: No async complexity in pool logic
  • State Management: Pools can be snapshotted, pickled, and restored

Current status: all pool types (V2, V3, V4, Aerodrome, Camelot, Balancer, Curve) are fully I/O-free — no pool class carries provider-dependent methods. For the Uniswap V2/V3/V4 families (including Aerodrome and Balancer), Bot.build_pool() performs the full fetch-and-register choreography and bot.update(pool) refreshes state from chain; Curve pools and token metadata use the remaining Python builders. Either way, state changes enter a pool only as a validated external_update() message — the pool itself never does I/O.

The Bot Class

Bot is the central session object that owns all runtime state:

import degenbot

# RPC_URL is any HTTP RPC endpoint for chain 1, for example
#   "https://eth-mainnet.example.com"
# Bot manages connections, registries, and provides factory methods
bot = degenbot.Bot(
    chain_id=1,
    node=RPC_URL,
    database=":memory:",
)
# The RPC provider is built from the node endpoint; eth_chainId is enforced to
# equal chain_id at construction.
bot.provider  # the chain's AlloyProvider (chain_id enforced at construction)
bot.chain_id  # 1
# All pool/token creation flows through Bot
pool = bot.build_pool("0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")
token = bot.build_erc20token("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")

# Bot provides token utilities with caching
balance = bot.get_token_balance(token, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
approval = bot.get_token_approval(token, owner="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", spender="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")

Bot properties:

  • bot.chain_id - the configured chain ID for this single-chain session
  • bot.provider / bot.get_provider() - the chain's AlloyProvider (chain_id enforced at construction)
  • bot.pools - PoolRegistry for created pools
  • bot.tokens - TokenRegistry for created tokens
  • bot.managed_pools - ManagedPoolRegistry for V4 pools
  • bot.database_path - the SQLite path used by the Rust-owned database core
  • degenbot.db - the stable Python mirror for Rust-backed database operations and row types

Lifecycle & refresh: Bot is a context manager — with degenbot.Bot(config=...) as bot: (or an explicit, idempotent bot.close()) tears down the provider, the Rust database snapshot, and the Rust engine handles. bot.update(pool, block_number=...) is the canonical refresh entry point for the V2/V3/V4 families: it fetches current chain state from the Rust core and pushes pool.external_update() (returns True only when state changed). bot.release_python_state() drops the Python-side tracker/snapshot caches once the Rust engine owns canonical state. Builders are internal to Bot and not exposed publicly. All pool/token creation goes through Bot.build_pool().

Pool Types and Builders

build_pool(address) is the universal entry point that auto-resolves pool type from DB, registry, and on-chain probing:

Pool Type Method Supports
Uniswap V2 bot.build_pool(address) Standard AMM, Camelot, other forks
Uniswap V3 bot.build_pool(address) Full tick data, range orders
Uniswap V4 bot.build_managed_pool(address, pool_id=...) Singleton architecture with hooks
Curve V1 bot.build_pool(address) StableSwap, metapools, lending pools

When build_pool is called, the pool type is auto-resolved — in order, from the pool registry, the database, and (as a last resort) on-chain probing of the pool contract.

External Updates

Pools receive state updates via external_update() — a pure-logic method that validates the update and transitions pool state. I/O never touches the pool itself: for the V2/V3/V4 families bot.update(pool) fetches current reserves/slot0/liquidity from the Rust core (Curve/Balancer refresh runs through the remaining Python builders), constructs the family's ExternalUpdate message, and pushes it to the pool:

# Builder fetches state from chain (I/O), constructs update, pushes to pool
update = UniswapV2PoolExternalUpdate(
    block_number=block_number,
    reserves_token0=reserves0,
    reserves_token1=reserves1,
)
pool.external_update(update)  # Pure logic — no I/O

# Pool.simulate_swap() previews swaps without state change
# Pool.calculate_tokens_out_from_tokens_in() is pure math after construction

Supported Protocols

DEXs (Automated Market Makers)

Protocol Versions Chains
Uniswap V2, V3, V4 Ethereum, Base
Aerodrome V2, V3 Base
PancakeSwap V2, V3 Ethereum, Base
SushiSwap V2, V3 Ethereum, Base
Curve V1 Ethereum
Solidly V2 Ethereum, Base
Balancer V2 Ethereum
Camelot V2 Arbitrum
SwapBased V2 Base

Lending Protocols

Protocol Features
Aave V3 Supply, Borrow, Withdraw, Repay, Liquidation, E-Mode, GHO

Infrastructure

Feature Description
Chainlink Price Feeds Oracle price data
Anvil Forking Local forked blockchain for testing

Examples

The following examples demonstrate the recommended Bot-based approach for pool and token construction.

All pool and token creation should flow through the Bot class for proper registry management and I/O handling:

import degenbot

# RPC_URL is any HTTP RPC endpoint for chain 1, for example
#   "https://eth-mainnet.example.com"
# Initialize Bot (handles connections, registries)
bot = degenbot.Bot(
    chain_id=1,
    node=RPC_URL,
    database=":memory:",
)
# Build tokens (fetches from DB/RPC, cached in registry)
weth = bot.build_erc20token("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
usdc = bot.build_erc20token("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")

# Build pools (fetches all state from DB/RPC, returns I/O-free pool objects)
v3_pool = bot.build_pool("0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")
v2_pool = bot.build_pool("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")
curve_pool = bot.build_pool("0xbEbc44782C7db0a1A60Cb6fe97d0b483032FF1C7")  # 3Crv

# Universal builder -- auto-resolves pool type
pool = bot.build_pool("0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")  # V3, detected automatically

# Token utilities with automatic caching
balance = bot.get_token_balance(usdc, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
approval = bot.get_token_approval(usdc, owner="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", spender="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")

# Pools are I/O-free after construction - pure calculations
amount_out = v3_pool.calculate_tokens_out_from_tokens_in(
    token_in=v3_pool.token0,
    token_in_quantity=1000_000000,  # 1000 USDC
)

Uniswap V2 Liquidity Pools

V2 pools use the constant-product invariant (x·y=k) with directional fees:

# `lp` is the WBTC/WETH V2 pool; in production it comes from
# `lp = bot.build_pool('0xBb2b8038a1640196FbE3e38816F3e67Cba72D940')`.
# An off-line test fixture built the same pool here so the math below
# runs without RPC.
assert lp.token0.symbol == 'WBTC'
assert lp.token1.symbol == 'WETH'
assert lp.reserves_token0 == 10732489743
assert lp.reserves_token1 == 2056834999904002274711

# V2 directional fees (may differ per direction)
assert lp.fee_token0 == Fraction(3, 1000)
assert lp.fee_token1 == Fraction(3, 1000)

# Calculate swap outputs - pure math, no I/O
assert lp.calculate_tokens_out_from_tokens_in(
    token_in=lp.token1,
    token_in_quantity=1*10**18
) == 5199789

assert lp.calculate_tokens_in_from_tokens_out(
    token_out=lp.token0,
    token_out_quantity=5199789
) == 999999992817074189

# Pools are I/O-free: updates flow through external_update()
# The builder (internal to Bot) fetches state and pushes updates
update = UniswapV2PoolExternalUpdate(
    block_number=100,
    reserves_token0=10732455184,
    reserves_token1=2056841643098872755548,
)
lp.external_update(update)

# Reserves are updated in-place
assert lp.reserves_token0 == 10732455184
assert lp.reserves_token1 == 2056841643098872755548

Uniswap V3 Liquidity Pools

V3 pools use concentrated liquidity with tick-based positions. The V3 pool uses a sparse tick data fetcher for on-demand liquidity loading:

# `lp` is the WBTC/WETH 0.3% V3 pool; in production it comes from
# `lp = bot.build_pool('0xCBCdF9626bC03E24f779434178A73a0B4bad62eD')`.
# An off-line test fixture built the same pool here so the math below
# runs without RPC.
assert lp.token0.symbol == 'WBTC'
assert lp.token1.symbol == 'WETH'
assert lp.fee == 3000
assert lp.liquidity == 544425151051415575
assert lp.sqrt_price_x96 == 34048891009198980752047510166697902
assert lp.tick == 259432

# Calculate inputs and outputs - pure math, no I/O
assert lp.calculate_tokens_out_from_tokens_in(
    token_in=lp.token1,
    token_in_quantity=1*10**18
) == 5398169

# Tick bitmap and tick data are injected at construction
assert 0 in lp.tick_bitmap
assert 0 in lp.tick_data

Uniswap V4 Liquidity Pools

V4 uses a singleton pool manager with hooks. Pools are identified by pool_id instead of address:

# `lp` is the ETH/USDC 0.05% V4 pool; in production it comes from
# `lp = bot.build_managed_pool('<poolManager>', pool_id='0x<...>')` — V4 pools
# are identified by (pool manager, pool id) rather than an address.
# An off-line test fixture built the same pool here so the math below
# runs without RPC.
assert lp.token0.symbol == 'ETH'
assert lp.token1.symbol == 'USDC'
assert lp.liquidity == 60429069420043934
assert lp.sqrt_price_x96 == 4220772448119892035402666
assert lp.tick == -196812

# V4 features: hooks, protocol fees, dynamic LP fees
assert lp.active_hooks == frozenset()
assert lp.pool_key == UniswapV4PoolKey(
    currency0='0x0000000000000000000000000000000000000000',
    currency1='0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    fee=500,
    tick_spacing=10,
    hooks='0x0000000000000000000000000000000000000000',
)

Forking With Anvil

The AnvilFork class is used to launch a fork with anvil from the Foundry toolkit. The fork subprocess is spawned and driven by the Rust core; the Python AnvilFork is a thin companion over it. The object provides a provider attribute — an AlloyProvider — which can be used to communicate with the fork like any typical RPC client.

>>> fork = degenbot.AnvilFork(fork_url='http://localhost:8545')
>>> fork.provider.chain_id
1
>>> fork.provider.block_number
22675736

# The `AnvilFork` instance also exposes HTTP and WS endpoints that can be used to make a
# separate connection from a remote machine.
>>> from degenbot.provider import AlloyProvider
>>> _prov = AlloyProvider(fork.http_url)
>>> _prov.is_connected()
True

# The fork can be reset to a specific block (defaults to the latest block).
>>> fork.reset(block_number=22_675_800)
>>> fork.provider.block_number
22675800

# A different endpoint or start block needs a NEW fork — `reset` cannot retarget
# the fork URL. An "imaginary" block after a historical transaction (anvil
# `--fork-transaction-hash`, see the [Anvil reference](https://getfoundry.sh/anvil/reference/))
# is a constructor option:
>>> fork = degenbot.AnvilFork(
    fork_url='http://localhost:8545',
    fork_transaction_hash='0xc16e63e693a2748559c0fd653ade195be426472dddc5bfa3fcc769c4c88c249c',
)

# Blocks can be manually mined
>>> fork.mine()

# Byte code can be set for an arbitrary address.
>>> fork.set_code(
    address='0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
    code=bytes.fromhex('45')
)
>>> fork.provider.get_code('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
b'\x45'

Anvil Options

The Anvil client offers many options; the most common ones are exposed as AnvilFork constructor options. For fine-grained control, pass any raw anvil flag through the anvil_opts argument (a list of strings, e.g. anvil_opts=['--optimism'] or anvil_opts=['--hardfork=london']) — they are appended after all managed options.

Curve StableSwap Pools (I/O-Free)

Curve pools follow the same I/O-free architecture: the Bot resolves metapool detection, lending-token identification, and all on-chain inputs before the pool runs pure math:

# `tripool` is Curve's 3Crv pool; in production it comes from
# `tripool = bot.build_pool('0xbEbc44782C7db0a1A60Cb6fe97d0b483032FF1C7')`.
# An off-line test fixture built the same pool here so the math below
# runs without RPC.
assert [t.symbol for t in tripool.tokens] == ['DAI', 'USDC', 'USDT']
assert tripool.a_coefficient == 2000
assert tripool.fee == 4000000

# For lending pools (cTokens), rates are resolved before calculation;
# get_dy() resolves all on-chain inputs upfront, then computes with pure math

Balancer V2 Weighted Pools

Balancer V2 weighted pools use the weighted product invariant with configurable token weights and a singleton Vault architecture. The math libraries are ported from the Balancer V2 Solidity monorepo with exact integer-level matching against on-chain results.

from degenbot.balancer.pools import BalancerV2Pool

# `weighted_pool` is the real mainnet "80 BAL 20 WETH" pool, built off-line by
# the fixture above so the math below runs without RPC. In production the same
# object comes from:
#     weighted_pool = bot.build_pool('0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56')
assert isinstance(weighted_pool, BalancerV2Pool)
assert weighted_pool.address == '0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56'
assert weighted_pool.vault == '0xBA12222222228d8Ba445958a75a0704d566BF2C8'
assert [t.symbol for t in weighted_pool.tokens] == ['BAL', 'WETH']
assert weighted_pool.fee == Fraction(1, 100)                 # 1% swap fee
assert weighted_pool.weights == (8 * 10**17, 2 * 10**17)    # 80 BAL / 20 WETH

# Swap math is pure after construction — no I/O
amount_out = weighted_pool.calculate_tokens_out_from_tokens_in(
    token_in=weighted_pool.tokens[1],   # WETH in
    token_out=weighted_pool.tokens[0],  # BAL out
    token_in_quantity=10**18,
)
assert amount_out == 61874980427000000  # ≈ 0.0619 BAL per WETH at 80/20 + 1% fee

amount_in = weighted_pool.calculate_tokens_in_from_tokens_out(
    token_in=weighted_pool.tokens[1],   # WETH in
    token_out=weighted_pool.tokens[0],  # BAL out
    token_out_quantity=100 * 10**18,
)
assert amount_in == 1616565737428323232324

Contract addresses and broken pool filters are centralized in degenbot.balancer.deployments:

from degenbot.balancer.deployments import (
    BALANCER_V2_VAULT_ADDRESS,
    BALANCERQUERIES_CONTRACT_ADDRESS,
    BROKEN_BALANCER_V2_POOLS,
)

# Canonical Vault + BalancerQueries addresses
assert BALANCER_V2_VAULT_ADDRESS == '0xBA12222222228d8Ba445958a75a0704d566BF2C8'
assert BALANCERQUERIES_CONTRACT_ADDRESS == '0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5'

# BROKEN_BALANCER_V2_POOLS is a frozenset of pools with swaps disabled on-chain.
# Filter before constructing:
broken = '0x753BD6a5bF0b14ae7e5d2877e5cD6a3398aA2AAB'  # YUME/WETH 1/99
assert broken in BROKEN_BALANCER_V2_POOLS
assert weighted_pool.address not in BROKEN_BALANCER_V2_POOLS  # 80 BAL 20 WETH is healthy

Balancer V2 Stable Pools

Balancer V2 stable pools (MetaStablePool and ComposableStablePool) use the StableSwap invariant with rate caching. The math libraries are ported from deployed contracts with exact integer-level matching against on-chain results.

Two pool shapes share the same BalancerV2StablePool interface:

  • MetaStablePool — a 2-token stable pool with no BPT token and near-static rates: exact swap math needs no rate provider and no extra I/O.
  • ComposableStablePool — a multi-token stable pool that includes its own BPT token; time-varying rates (e.g., bb-a-* yield tokens) require a live BalancerRateProvider, and without one a swap call raises StaleRateResult (the approximate result is still readable on the exception).
from degenbot.balancer.stable_pools import BalancerV2StablePool
from degenbot.exceptions.pool import StaleRateResult

# Both pools were built off-line by the fixture above so the math runs
# without RPC; production obtains the same objects via `bot.build_pool(address)`.

# MetaStablePool: 2-token, no BPT, near-static rates — exact swap math
# needs no rate provider and no live RPC.
assert isinstance(meta_pool, BalancerV2StablePool)
assert [t.symbol for t in meta_pool.tokens] == ['wstETH', 'WETH']
assert meta_pool.fee == Fraction(4, 10000)        # 0.04%
assert meta_pool.amp == 50_000

amount_out = meta_pool.calculate_tokens_out_from_tokens_in(
    token_in=meta_pool.tokens[1],   # WETH in
    token_out=meta_pool.tokens[0],  # wstETH out
    token_in_quantity=10**18,
)
assert amount_out == 908727110808623404  # ≈ 0.9087 wstETH per WETH @ 1.1 rate

# ComposableStablePool: time-varying rates require a live rate provider;
# without one the call raises StaleRateResult (the approximate result is
# still readable on the exception).
assert isinstance(comp_pool, BalancerV2StablePool)
assert [t.symbol for t in comp_pool.tokens] == ['TUSD', '50TUSD50USDC', 'USDC']
assert comp_pool.fee == Fraction(3, 10000)         # 0.03%

try:
    comp_pool.calculate_tokens_out_from_tokens_in(
        token_in=comp_pool.tokens[0],   # TUSD in
        token_out=comp_pool.tokens[2],  # USDC out
        token_in_quantity=10**18,
    )
except StaleRateResult as e:
    # StaleRateResult wraps the approximate result so callers can still read it
    assert e.amount_in == 10**18
    assert e.amount_out == 1001103

Uniswap Arbitrage

Optimal arbitrage amounts for a cyclic pool sequence are computed by the Rust ArbitrageEngine (EVM-exact U512 solve), driven through EngineRegistry. The older Python cycle/solver classes are retired — the Rust engine is the sole solve surface:

from degenbot.arbitrage.engine_registry import EngineRegistry

# EngineRegistry is the one canonical entry point: it runs the pre-pump
# startup ritual (subscribe -> backfill from snapshot -> verify config) and
# registers cyclic paths against a Bot's shared BotState. The Rust engine
# owns the EVM-exact U512 solve and re-solves affected paths on each block.
registry = EngineRegistry(bot=bot)
# In production, `registry.start(node_http, node_ws)` runs the startup
# ritual (subscribe, snapshot, verify config) and returns BEFORE resume();
# after attaching the result consumer, `registry.engine.resume()` is the
# single gate after which one result batch per block flows.
path_id, created = registry.register_path(
    pools_and_zfos=[(v2_pool, True), (v3_pool, False)],
)
# The registered path is inspectable immediately (a solved snapshot of its
# hops); profitable solves surface in the next `latest_results()` batch.
solved_path = registry.engine.inspect_path(path_id)
assert solved_path["path_id"] == path_id

Swap Encoding & On-Chain Execution

A profitable solve goes straight from the Rust engine to a submitted transaction — there is no Python encoding layer. After a path solves:

  1. Encode — the Rust core emits the per-hop calldata (V2 swap(), V3 swap(), V4 PoolManager swap(), Curve exchange() / exchange_underlying()) and composes it into the cmd-executor contract envelope.
  2. Submit — the Rust submission layer signs (EIP-1559) and sends the transaction; dry-run mode (the default) stops after solving and simulates in-process without submitting.

Running the Settlement-Arbitrage Bot

The end-to-end settlement-arbitrage bot — the flagship Rust-core-driven workload — is a thin Python driver over the degenbot.runner package: the example owns only CLI parsing + SIGINT handling, while BotRunner owns the session and the Rust engine handshake.

# Dry run (default): solves, simulates in-process, renders profit lines — nothing is submitted
uv run python examples/eth_settlement_arbitrage_v2_v3_v4_rust.py \\
  --node "https://eth-mainnet.example.com"

# Restrict to one 3-hop permutation (overrides the driver's default path filter)
uv run python examples/eth_settlement_arbitrage_v2_v3_v4_rust.py --permutation V2-V3-V4

# Live mode: signs and submits real transactions (operator key via env)
uv run python examples/eth_settlement_arbitrage_v2_v3_v4_rust.py --live

Endpoints resolve through the shared four-layer cascade: the operator file's [nodes] tables are the base layer, the DEGENBOT_RPC_HTTP_CHAINID_1 / DEGENBOT_RPC_WS_CHAINID_1 env names override that chain's file entry alone, and the example's own --node flag (one self-classifying URI) outranks the environment. Operator keys come from examples/mainnet.env + the OS env: OPERATOR_ADDRESS / OPERATOR_PRIVATE_KEY in live mode, plus optional EXECUTOR_CONTRACT_ADDRESS overrides. BotRunner performs the driver-side startup handshake, after which the Rust core owns the hot loop — event decode, per-block re-solve, in-process simulation, encoding, submission — and the Python driver owns config, result rendering, and dispatch policy. With --operator-socket PATH, the bot also hosts an OperatorServer that the degenbot path add / degenbot path discover CLI commands target to steer the live path set without a restart (protocol + design in docs/architecture/operator-add-path-surface.md).

Both drivers launch through the repo's ./run_bot.sh wrapper, which owns the shared env exports, the RPC-cascade print, build-on-demand for the Rust driver, and the start/stop/status lifecycle:

./run_bot.sh                      # Python driver, foreground (legacy default)
./run_bot.sh --python start       # Python driver, detached (same command/env as above)
./run_bot.sh --rust start         # pure-Rust driver (rust/examples/settlement_bot)
./run_bot.sh --rust start -- --live --permutation V2-V3-V4   # args after `--` pass through verbatim
./run_bot.sh --rust print-cmd     # resolved driver + command + exports; no build, no launch
./run_bot.sh status               # covers both drivers
./run_bot.sh stop                 # stops whichever driver is running

--python is the default and is byte-identical to the direct uv run invocation above. --rust builds rust/target/$RUST_PROFILE/degenbot-settlement-bot-example when the binary or workspace is stale (RUST_PROFILE=release by default; RUST_PROFILE=dev opts into the workspace opt-level = 1 development profile for iteration) and arms the Rust example's live handshake by exporting SMOKE_RPC_URL from the same RPC cascade the Python driver resolves. --live is never implied — pass it explicitly after --. stop/status cover both driver process names plus the pidfile. The launcher-consolidation record, with the two deliberate divergences between the drivers, lives in docs/architecture/rust-settlement-bot-parity.md.

Bot API Reference

The Bot class is the primary entry point for degenbot usage. Access factories, registries, and utilities through Bot.

Initialization

import degenbot

# RPC_URL is any HTTP RPC endpoint for chain 1, for example
#   "https://eth-mainnet.example.com"
# With explicit settings
bot = degenbot.Bot(
    chain_id=1,
    node=RPC_URL,
    database="~/.local/state/degenbot/db/degenbot.db",
)
# The RPC provider is built from the node endpoint and its eth_chainId is
# enforced to equal chain_id at construction — no manual registration needed.

Universal Pool Builder

# Universal builder — auto-resolves pool type from DB, registry, or on-chain probing
pool = bot.build_pool(
    "0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8",
    state_block=18900000,  # Optional, defaults to current block
)
# For V4 pools, use build_managed_pool with the PoolManager address + pool_id
pool = bot.build_managed_pool(
    "0x...",  # PoolManager address
    pool_id="0x...",
)

Pool Construction by Type

# V2 pool (auto-detected from factory)
pool = bot.build_pool(
    "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
    state_block=18900000,  # Optional, defaults to current block
)

# V3 pool (auto-detected from factory)
pool = bot.build_pool(
    "0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8",
)

# Curve pool (auto-detected from on-chain probing)
pool = bot.build_pool(
    "0xbEbc44782C7db0a1A60Cb6fe97d0b483032FF1C7",
)
# V4 pool (singleton architecture with pool_id)
pool = bot.build_managed_pool(
    "0x...",  # PoolManager address
    pool_id="0x...",
    state_view_address="0x...",
    tokens=["0x...", "0x..."],
    fee=500,
    tick_spacing=10,
)

Token Factory

# ERC-20 token (fetches name, symbol, decimals from DB/RPC)
token = bot.build_erc20token("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")

# Token lookup (from registry if cache hit)
token = bot.get_token("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")

Token Utilities (With Caching)

# Get balance at block (cached per-bot)
balance = bot.get_token_balance(token, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
balance_at_block = bot.get_token_balance(token, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", block_identifier=10000000)

# Get approval amount (cached)
approval = bot.get_token_approval(token, owner="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", spender="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")

# Get total supply (cached)
total_supply = bot.get_token_total_supply(token)

# Get native ETH balance
eth_balance = bot.get_ether_balance(address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")

Accessing Bot Components

# RPC provider (built from config; chain_id enforced at construction)
provider = bot.provider

# Registries (check if already created)
existing_pool = bot.pools.get(chain_id=1, pool_address="0x8ad599c3A0ff1De082011EFDDc58f1908EB6e6D8")
existing_token = bot.tokens.get(token_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", chain_id=1)

# Rust-backed database state; use the stable degenbot.db mirror
from degenbot.db import db_inspect_schema_state

schema_state = db_inspect_schema_state(str(bot.database_path))

Chainlink price feeds provide reliable oracle data for various assets. The ChainlinkPriceContract class simplifies access to these feeds.

# Load the price feed for ETH/USD
# decimals can be provided to avoid a live RPC call
assert price_feed.address == '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419'
assert price_feed.decimals == 8

# price_feed.price requires a Bot instance with RPC access for live data

CLI Reference

Degenbot provides a command-line interface for managing blockchain data and pool state.

Installation

The CLI is installed automatically with the package:

pip install degenbot
degenbot --help

Commands

Database Management

# Back up the database
degenbot database backup

# Reset database (creates fresh schema; hidden command, --force skips the prompt)
degenbot database reset --force

# RETIRED (ADR-052): the database upgrades itself at open. `database
# upgrade` now renders a pointed error and exits 1; use `database heal` for an
# explicit repair.
degenbot database upgrade

# Inspect the schema state (read-only; never writes)
degenbot database inspect

# Compact database to reclaim space
degenbot database compact

# Inspect schema ownership (Alembic vs Rust) + preview the cutover
degenbot database cutover --dry-run

# One-way cutover from Alembic to Rust schema ownership (ADR-010)
degenbot database cutover [--force]

# Out-of-place heal: rebuild a stale Alembic DB at the Rust head schema (ADR-011)
degenbot database heal [--dry-run]

Pool State Management

# Update pool metadata and liquidity positions for all active exchanges
# (--verify-chunk/--verify-all add pre-commit on-chain-truth gates: a
# divergence rolls the chunk back and does NOT advance last_update_block)
degenbot pool update [--chunk SIZE] [--to-block BLOCK] [--verify-chunk/--no-verify-chunk] [--verify-all/--no-verify-all]

# Verify one V3/V4 pool's DB state against on-chain truth at a given block
degenbot pool verify --rpc-url URL --chain 1 --block 18900000 --pool 0x... --family v3|v4 [--pool-manager 0x...]

# Activate an exchange for tracking (ADR-051 D5: one data-driven command;
# --chain is a chain slug or numeric id, --name is the DEX name slug)
degenbot exchange activate --chain base --name uniswap_v3

# Deactivate an exchange
degenbot exchange deactivate --chain base --name uniswap_v3

# Steer a running bot (started with --operator-socket) without restarting it
degenbot path add --socket /path/to/operator.sock --hop V2:0xPoolAddr [--hop V3:0xPoolAddr] [--direction zfo|ozf]
degenbot path discover --socket /path/to/operator.sock [--bound N]

Supported exchanges (--chain <chain> --name <dex>):

  • Base: aerodrome_v2, aerodrome_v3, pancakeswap_v2, pancakeswap_v3, sushiswap_v2, sushiswap_v3, swapbased_v2, uniswap_v2, uniswap_v3, uniswap_v4
  • Ethereum: pancakeswap_v2, pancakeswap_v3, sushiswap_v2, sushiswap_v3, uniswap_v2, uniswap_v3, uniswap_v4

Fleet Posture Management

The bot's operator channel also hosts the live cordon posture of a worker fleet (Nominal/Cordoned state and its thresholds). Socket resolution matches path: --socket flag, else DEGENBOT_OPERATOR_SOCKET, else ~/.config/degenbot/operator.sock.

# Show the LIVE cordon posture: thresholds + Nominal|Cordoned
degenbot fleet posture show [--socket /path/to/operator.sock]

# Re-tune the LIVE cordon thresholds (a partial patch)
degenbot fleet posture set [--socket /path/to/operator.sock] \
  [--cordon-enter-events COUNT] [--cordon-duty-percent PERCENT] \
  [--cordon-enter-window-ms MS] [--cordon-duty-window-ms MS] \
  [--cordon-exit-clean-ms MS] [--cordon-sim-intake-floor COUNT|null]

Aave State Management

# Update Aave V3 positions for all active markets
degenbot aave update [--chunk SIZE] [--to-block BLOCK] [--verify-chunk/--no-verify-chunk] [--dry-run]

# Activate the Aave market for the session chain (global --chain-id; default 1)
degenbot aave activate [--chain-id CHAIN_ID]

# Deactivate an Aave market (default: "Aave Ethereum Market")
degenbot aave deactivate [--chain-id CHAIN_ID] [--name MARKET]

# Show a user's position in a market
degenbot aave position show <ADDRESS> [--market MARKET] [--chain-id CHAIN_ID]

Rust-owned console (ADR-051). The degenbot console is the degenbot-cli Rust binary; the Python console script (degenbot._cli:main) and python -m degenbot are thin passthroughs over the same command model, so argv, prompts, and exit codes exist once. Two read-only commands from the former Python click tree have no Rust arm yet and exit with a usage error: degenbot aave position risk and degenbot aave market show.

Block Identifiers

Commands accepting --to-block support the following formats:

Format Example Description
latest latest Latest block
latest:-N latest:-64 N blocks before latest (default)
safe:+N safe:128 N blocks after safe block
Number 18900000 Specific block number

Configuration

Configuration File

The operator file $XDG_CONFIG_HOME/degenbot/config.toml (else ~/.config/degenbot/config.toml, or the DEGENBOT_CONFIG override) is the typed Rust BotConfig file layer: its tables must name declared schema sections (see docs/rust-config-keys.md for the authoritative, generated key reference), plus the free-form [failure_policy] table. It is the BASE layer of the cascade — the per-chain endpoints, the session chain id, and the database path are read from it:

# Per-chain node endpoints, keyed by chain id: the base layer.
[nodes]
http = { 1 = "http://localhost:8545" }
ws = { 1 = "ws://localhost:8546" }

[session]
chain_id = 1

[database]
path = "./degenbot.db"

[telemetry]
otel = true
jaeger_endpoint = "http://localhost:4318"
metrics_addr = "0.0.0.0:9464"

# Free-form per-bucket failure-reaction overrides (ADR-040). An empty or
# missing table runs the default matrix.
[failure_policy]

[nodes.ipc] takes a socket path or an ipc:// URL for a node running beside the bot. Treat the file as a secret carrier: chmod 600 it, and prefer a per-machine environment variable for anything credential-shaped.

Layer precedence

Precedence Layer Supplies
1 explicit CLI --node <uri>, --chain-id, --database
2 environment DEGENBOT_RPC_{HTTP,WS,IPC}_CHAINID_<chain>, DEGENBOT_DEFAULT_CHAIN_ID, DEGENBOT_DB_PATH
3 (base) --config file [nodes.*], session.chain_id, database.path, and every other typed section
4 declared default the schema default (database.path → the XDG state home)

An env entry for one chain overrides that chain's file entry ALONE, and --node <uri> outranks the environment; it is repeatable, and the value classifies its own transport — http(s):// → nodes.http, ws(s):// → nodes.ws, ipc:// or a socket path → nodes.ipc.

degenbot config show --resolved prints the whole inventory as the process resolves it, each key annotated with the layer that won it, and degenbot config path prints the file the cascade reads; both are read-only (see docs/rust-cli.md). That is the place to look first when an edited file entry appears to be ignored.

A surviving pre-0.6 spelling ([rpc], [ws], [database] filepath, top-level default_chain_id) is refused at boot, as is the [otel] table — see docs/config-migration.md for the replacement table.

The SQLite database defaults to the XDG state home — $XDG_STATE_HOME/degenbot/db/degenbot.db when $XDG_STATE_HOME is an absolute path, else ~/.local/state/degenbot/db/degenbot.db.

Environment Variables

Variable Values Description
DEGENBOT_DEBUG 1, true, yes Enable debug-level logging output
DEGENBOT_DEFAULT_CHAIN_ID integer chain id The env layer of the declared session.chain_id; overrides the file's [session] chain_id (ADR-006, one Bot per chain). Only the top-level FILE key default_chain_id is retired — this env name is live. A Bot refuses to construct without a chain id from some layer, and the connected RPC's eth_chainId is enforced to match at construction
DEGENBOT_RPC_HTTP_CHAINID_<ID> any HTTP(S) URL HTTP RPC endpoint for chain <ID>; overrides that chain's [nodes.http] entry alone
DEGENBOT_RPC_WS_CHAINID_<ID> any WS(S) URL WebSocket endpoint for chain <ID>; overrides that chain's [nodes.ws] entry alone
DEGENBOT_RPC_IPC_CHAINID_<ID> any ipc:// URL or socket path Local IPC endpoint for chain <ID>; overrides that chain's [nodes.ipc] entry alone
DEGENBOT_DB_PATH filesystem path SQLite database file; overrides the file's database.path
DEGENBOT_DEBUG=1 python my_script.py

The Rust Core (degenbot_rs Rust crate, degenbot._ffi Python module)

The Rust core is the engine of degenbot — it owns all performance-critical and stateful logic. Python reaches it through the degenbot._ffi extension module, a thin PyO3 binding layer (rust/crates/shells/degenbot-python/) that translates Python calls into Rust calls with no business logic of its own. The underlying core crates are pyo3-free by default and are consumable directly from pure Rust through the umbrella degenbot crate — currently via a git/path dependency (the crates are not yet published to crates.io); the in-repo proof is rust/crates/facade/degenbot/examples/standalone_consumer.rs, gated by just test-standalone.

The extension is built automatically during installation using maturin (or uv sync, which invokes maturin under the hood).

Available Functions

Tick Math

Uniswap V3 tick-to-price conversions (Q96 fixed point):

from degenbot.uniswap.math import get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio

# Convert tick to sqrt price (Q96)
sqrt_price = get_sqrt_ratio_at_tick(253320)
assert sqrt_price == 25082941840919119221697001330704483

# Convert the full sqrt price back into the tick — exact round-trip
assert get_tick_at_sqrt_ratio(sqrt_price) == 253320

ABI Decoding

High-performance ABI decoding for contract data:

from degenbot._ffi.abi import decode, decode_single, encode

# Encode then decode multiple values
types = ["address", "uint256", "uint256"]
data = encode(types, ["0x0000000000000000000000000000000000000001", 100, 200])
values = decode(types, data)  # Returns list of decoded values
assert values == ["0x0000000000000000000000000000000000000001", 100, 200]

# Decode a single value
address = decode_single("address", data[:32])
assert address == "0x0000000000000000000000000000000000000001"

Address Utilities

EIP-55 checksummed address conversion:

from degenbot import get_checksum_address

checksummed = get_checksum_address("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
assert checksummed == "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"

ABI Encoding & Selectors

Encode function calls and compute selectors:

from degenbot.contract import encode_function_call, get_function_selector, decode_return_data
# Get a 4-byte function selector
selector = get_function_selector("transfer(address,uint256)")
assert selector == "0xa9059cbb"

# Encode a function call (selector + encoded args)
calldata = encode_function_call(
    "transfer(address,uint256)",
    ["0x0000000000000000000000000000000000000001", "100"],
)
assert calldata[:4].hex() == "a9059cbb"

# Decode the same calldata body back out
values = decode_return_data(calldata[4:], ["address", "uint256"])
assert values == ["0x0000000000000000000000000000000000000001", "100"]

Provider Classes

bot.provider is the normal way to reach chain data. The extension also exposes the raw synchronous/async RPC provider classes directly for the cases where the Bot conveniences don't fit:

# RPC_URL is any HTTP RPC endpoint for chain 1, for example
#   "https://eth-mainnet.example.com"
# Create provider with connection pooling
provider = AlloyProvider(RPC_URL)

# Contract interaction
contract = Contract(
    "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    provider_url=RPC_URL,
)

# Query blockchain
block_number = provider.get_block_number()
chain_id = provider.get_chain_id()
logs = provider.get_logs(
    from_block=block_number - 10,
    to_block=block_number,
    addresses=["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],
)
result = contract.call(
    "balanceOf(address)",
    ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
    block_number,
)

provider.close()

Async Provider

The extension also includes async wrappers for use with asyncio:

from degenbot._ffi.contract import AsyncContract
from degenbot._ffi.provider import AsyncAlloyProvider

# Create an async provider
async_provider = await AsyncAlloyProvider.create(
    rpc_url="https://eth-mainnet.example.com",
    max_retries=10,
    max_blocks_per_request=5000,
)

# Async contract interaction (built via `create`; `from_provider` wraps an existing provider)
async_contract = await AsyncContract.create("0x...", provider_url="https://...")
result = await async_contract.call("balanceOf(address)", ["0x..."])

# Batch multiple contract calls
results = await async_contract.batch_call(
    [("balanceOf(address)", ["0x..."]), ("totalSupply()", [])],
)

Log Filtering

from degenbot._ffi.provider import LogFilter

# Build a log filter
log_filter = LogFilter(
    from_block=1000000,
    to_block=1000100,
    addresses=["0x0000000000000000000000000000000000000001"],
    topics=[["0x0000000000000000000000000000000000000000000000000000000000000001"]],
)

AlloyProvider also exposes pub-sub — subscribe_blocks(), subscribe_logs(...), subscribe_pending_transactions(), and friends return an async-iterable AlloySubscription (the primitive the settlement-arbitrage pump consumes) — plus offline modes (AlloyProvider.offline_from_json_file(path) / offline_from_json_string(s)) that answer from recorded RPC fixtures for deterministic tests, and opt-in transport-level rate limiting (requests_per_second + burst constructor args).

Engine and Dispatch Surface

Advanced drivers can also reach the engine directly through the degenbot._ffi module — the settlement-arbitrage engine (ArbitrageEngine), the shared Bot state handle, plus the I/O, submission, and price seams — instead of going through the degenbot.* conveniences. Its type stubs (src/degenbot/_ffi/*.pyi) are the reference surface.

Why Rust for the Hot Path

The MEV workload — per-block re-solve of hundreds of cyclic paths, EVM-exact revm simulation, ABI decode, tick math, and swap encoding — is latency-bound at the CPython boundary, so the pump loop runs in Rust with the GIL released around each PyO3 crossing. Per-operation microbenchmarks are not tracked in-repo.

Build Requirements

Cargo commands without a package selector build only the two pure-Rust workspace defaults: the degenbot umbrella and the degenbot-cli console. The PyO3 extension and non-publishable sample crates remain workspace members for explicit recipes and the full workspace gate, but a plain cargo build does not compile them.

The extension is pre-built in published packages. For source builds:

  • Rust 1.98.1 (the workspace MSRV is Rust 1.97)
  • maturin (installed automatically with uv sync)

Local Rust and maturin development builds intentionally use the workspace [profile.dev]: opt-level = 1, no LTO or stripping, and line-tables-only debug information. This favors representative optimization over an unoptimized debug build while keeping iterative rebuilds cheaper than release. Release wheels use [profile.release]: thin LTO, stripping, and codegen-units = 1 for the final extension link. The selected core-library packages intentionally use codegen-units = 16 to reduce release compile time; Cargo has no per-package LTO or strip override, and the final thin-LTO link reconverges those units.

# Build the two default pure-Rust entry points
cargo build --manifest-path rust/Cargo.toml

# Build the extension (same as `just build-rust-extension`)
cargo build --locked -p degenbot_rs --features extension-module --manifest-path rust/Cargo.toml

# Run the standalone smokes and canonical full Rust suite
just test-rust

# Bootstrap locked Python dependencies, then build/install the explicit
# development feature set through the canonical extension path.
just bootstrap

# Rebuild only after Rust edits; verify the receipt before using the .so.
just dev
just verify-build-fresh

# Direct Cargo hotpath test/build (same requirement):
RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }--cfg tokio_unstable" cargo test --locked \
  -p degenbot-bot --features hotpath --lib profiling:: -- --ignored --nocapture

Rust feature matrix

The Rust workspace is checked by named feature lanes. The default lane never uses --all-features, so a default-feature regression cannot be hidden by an exhaustive build.

Lane Feature set Recipe
Workspace defaults Every workspace member with its declared default features; no --all-features just lint-rust-check or just check-rust-default
Pure-Rust consumer degenbot and its examples, with the umbrella's defaults and no PyO3 binding just check-rust-consumer
Binding defaults degenbot_rs with its broad default domain features, without extension-module just check-rust-binding-default
Development wheel extension-module, degenbot-bot/hotpath, degenbot-bot/hotpath-prometheus, degenbot-solvers/hotpath, degenbot-bot/allocator-ctrl, otel, and mimalloc; the recipe adds RUSTFLAGS=--cfg tokio_unstable for full Tokio runtime metrics just check-rust-dev-features
Release wheel extension-module (forwards to pyo3/extension-module) plus degenbot_rs defaults; no dev-only profiling, telemetry, allocator-control, or mimalloc features just check-rust-extension-release or just build-rust-extension
Diagnostic Workspace --all-features, including test-only and mutually exclusive variants just check-rust-all-features

The development-wheel list is the binding manifest's canonical dev-features alias selected by just dev; just bootstrap installs locked Python dependencies without building the project through a second path. Release wheels are built with maturin --release --features pyo3/extension-module; that release command replaces the development feature list and keeps the binding crate's defaults, while excluding the development-only features above. The release profile keeps thin LTO, stripping, and the intentional per-package codegen-unit policy; the extension-release recipe checks this same feature set with Cargo's release profile.

Documentation

Additional documentation is available in the docs/ directory:

  • Architecture: High-level architectural patterns
    • Rust-Owned Settlement-Arbitrage Bot — the original ArbitrageEngine design (Plans 079–082); marked historical, kept as a design-history reference (the current state layer follows the ADR log)
    • Operator Add-Path Surface — steering a live bot (mid-run add-path + bounded on-demand discovery) over the Unix-socket JSON-lines operator channel
    • Semantic Matching — Event processing patterns for Aave
  • Architecture Decision Records: the ADR design log for the Python→Rust migration (three-layer architecture, per-chain Bot, schema retention/cutover, registration-verify lifecycle, executor grammar, …)
  • Execution Strategy: the user-owned ExecutionStrategy seam (ADR-025)
  • Aave V3: Comprehensive control flow diagrams and amount transformations for Aave operations
  • CLI: Detailed CLI command reference (aave.md, database.md, pool.md)
  • Logging: Controlling RUST_LOG / DEGENBOT_DEBUG tracing, the env-gated hard/loud diagnostics, and debug-named diagnostics

Contract Reference

Verified Solidity source code for all supported protocols is in contract_reference/:

Protocol Path Contents
Uniswap V2 contract_reference/uniswap/V2/ Factory, Pair, ERC20, SafeMath, Math, UQ112x112
Uniswap V3 contract_reference/uniswap/V3/ Factory, Pool, Oracle, Tick, TickBitmap, SqrtPriceMath, SwapMath, TickMath, FullMath, Position, etc.
Uniswap V4 contract_reference/uniswap/V4/ PoolManager, Pool, Hooks, TickBitmap, SqrtPriceMath, SwapMath, ProtocolFeeLibrary, LPFeeLibrary, ERC6909, etc.
Aave V3 contract_reference/aave/ Pool (10 revisions), AToken (5 revisions), VariableDebtToken, GhoVariableDebtToken (6 revisions), GhoDiscountRateStrategy, AaveOracle, stkAAVE, RewardsController

Useful when auditing, or when you need to understand the exact on-chain behavior of a supported protocol. See contract_reference/README.md for the full index.

Contributing

Contributions are welcome! Please submit issues and pull requests to the GitHub repository.

Development Setup

git clone https://github.com/BowTiedDevil/degenbot.git
cd degenbot
just bootstrap

# Run the full gate: standalone-Rust smoke + cargo workspace + full pytest
just test

# Individual tracks:
just test-rust    # cargo workspace + just test-standalone
just test-python  # uv run --no-sync pytest

License

This code is published under a permissive MIT license. See LICENSE for details.

Donation

If you find this code valuable, please fund continuing development by donating to 0xADAf500b965545C8A766CD9Cdeb3BF3FBef073e5 on any EVM compatible chain.

Metadata

Release files for degenbot 0.6.0a13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for degenbot 0.6.0a13
File Size Uploaded
degenbot-0.6.0a13.tar.gz 8.6 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for degenbot 0.6.0a13
File
degenbot-0.6.0a13-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
degenbot-0.6.0a13-cp312-abi3-manylinux_2_28_x86_64.whl CPython 3.12 abi3 Linux glibc 2.28+ x86-64 Details
degenbot-0.6.0a13-cp312-abi3-manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.28+ ARM64 Details
degenbot-0.6.0a13-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl CPython 3.12 abi3 macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64 Details

Total release size: 98.4 MB

Release files / degenbot-0.6.0a13.tar.gz

Download URL degenbot-0.6.0a13.tar.gz
Size 8.6 MB
Tags Source
SHA-256 checksum
How to use checksums
d669350709c6e0c7693b67accd32dd79bdd1f1b22c63c423ebfb925536b6f813
BLAKE2b-256 checksum
How to use checksums
462a56ff651936b6116e2bd68caf9862c5e9e451c268da1e3b69f039f2ab14d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / degenbot-0.6.0a13-cp312-abi3-win_amd64.whl

Download URL degenbot-0.6.0a13-cp312-abi3-win_amd64.whl
Size 18.8 MB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
0649a7912b3fee41eb2f695bbb7107d192c3feeb9361b2299f7b4a035f31b331
BLAKE2b-256 checksum
How to use checksums
c25350aeab4b0bf8e03c9c34f4ddf9919f8db05d4bab141d928734db2b2fd9d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / degenbot-0.6.0a13-cp312-abi3-manylinux_2_28_x86_64.whl

Download URL degenbot-0.6.0a13-cp312-abi3-manylinux_2_28_x86_64.whl
Size 18.5 MB
Tags CPython 3.12 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
235922b45b2eaa33f6300ac88026911ce0d34b967a37b322d66b90676d892bbe
BLAKE2b-256 checksum
How to use checksums
3b32ae6f3179efc7559a09b07ae0a7a9acf8f06f6edae76b9d347c1b7ec98d35
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / degenbot-0.6.0a13-cp312-abi3-manylinux_2_28_aarch64.whl

Download URL degenbot-0.6.0a13-cp312-abi3-manylinux_2_28_aarch64.whl
Size 17.6 MB
Tags CPython 3.12 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
d72ca7bc36241ded11212f2bb6d7fd949a7da3767cbaeaeecee1cac86884e0e7
BLAKE2b-256 checksum
How to use checksums
52f275fa2ee2e9f85015490efb22011e12908c095c5bb69a1f599164147df187
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / degenbot-0.6.0a13-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl

Download URL degenbot-0.6.0a13-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Size 34.9 MB
Tags CPython 3.12 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
11e6c04cb1f1cd7b5d35f35e7a9b36b1c17fb45e39b91122fa1f35ce5834d950
BLAKE2b-256 checksum
How to use checksums
18b4eb2e6f38592d134ba4e75f05b558512b345a6f5f05927acd348c36e25e39
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0a13 This release

5 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page