Skip to main content
BopAMM is currently undergoing contract audits. Before audits are complete, integrators are advised to implement their own safety checks on top of swaps against BopAMM.
This guide walks you through a complete BopAMM swap on Ethereum: approving the contract, fetching the current state, quoting against the live on-chain book, simulating the swap to catch reverts, and submitting it through a block builder. You’ll sell 1 USDC for WETH.
What you’ll build: A USDC to WETH swap against BopAMM, quoted and simulated mid-block, then submitted through a block builder.Time required: 15-20 minutesPrerequisites: Basic EVM knowledge and web3.py, an X-API-Key for the BopAMM operator, a funded signing key, and the ability to submit transactions through a builder RPC (Flashbots Protect or equivalent).
BopAMM’s contracts are unaudited at this time. If you route user funds through them - especially as an aggregator - enforce your own value checks (for example, validate the amount received against an independent price source) rather than relying on the contract’s minAmountOut alone.

Setup

Install web3, eth-account, and httpx, then define the constants and contract handle used throughout. BopAMM needs only two contract functions: quote() to price a swap and swap() to execute it.
import httpx
from eth_account import Account
from web3 import Web3
from web3.exceptions import ContractLogicError

API_KEY = "<your-api-key>"
PRIVATE_KEY = "0x<your-private-key>"

API_BASE = "https://api.bebop.xyz/bopamm/ethereum/v1"
RPC_URL = "https://ethereum-rpc.publicnode.com"

# Block builder that includes your swap behind the operator's registry update.
BUILDER_RPC_URL = "https://rpc.flashbots.net/?hint=default_logs&originId=protect-website"

# EIP-1559 fee estimates.
BLOCKNATIVE_GAS_URL = "https://api.blocknative.com/gasprices/blockprices"
BLOCKNATIVE_CONFIDENCE = 75

# BopAmmV2: the taker entrypoint and the approval target.
CONTRACT_ADDRESS = Web3.to_checksum_address("0xdB13ad0fcD134E9c48f2fDaEa8f6751a0F5349ca")

WETH = Web3.to_checksum_address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
USDC = Web3.to_checksum_address("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
WETH_DECIMALS = 18
USDC_DECIMALS = 6

SLIPPAGE_BPS = 50  # 0.5% slippage tolerance

CONTRACT_ABI = [
    {
        "name": "quote",
        "type": "function",
        "stateMutability": "view",
        "inputs": [
            {"name": "tokenIn", "type": "address"},
            {"name": "tokenOut", "type": "address"},
            {"name": "amountIn", "type": "uint256"},
        ],
        "outputs": [{"name": "amountOut", "type": "uint256"}],
    },
    {
        "name": "swap",
        "type": "function",
        "stateMutability": "payable",
        "inputs": [
            {"name": "tokenIn", "type": "address"},
            {"name": "tokenOut", "type": "address"},
            {"name": "amountIn", "type": "uint256"},
            {"name": "minAmountOut", "type": "uint256"},
            {"name": "expiry", "type": "uint256"},
            {"name": "recipient", "type": "address"},
        ],
        "outputs": [{"name": "amountOut", "type": "uint256"}],
    },
]

web3 = Web3(Web3.HTTPProvider(RPC_URL))
account = Account.from_key(PRIVATE_KEY)
contract = web3.eth.contract(address=CONTRACT_ADDRESS, abi=CONTRACT_ABI)

amount_in = 1 * 10**USDC_DECIMALS  # sell 1 USDC
Keep API_KEY and PRIVATE_KEY out of source. Load them from environment variables (for example with os.environ and python-dotenv) rather than hardcoding them.

1. Approve the BopAMM contract

Before swapping, the contract needs permission to pull your USDC. Approve the BopAmmV2 contract for the scope of this demo (10 USDC). This is the standard check-and-approve pattern. See Token Approvals for the canonical helper and the max-vs-exact tradeoffs.
ERC20_ABI = [
    {
        "constant": True,
        "inputs": [
            {"name": "_owner", "type": "address"},
            {"name": "_spender", "type": "address"},
        ],
        "name": "allowance",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function",
    },
    {
        "constant": False,
        "inputs": [
            {"name": "_spender", "type": "address"},
            {"name": "_value", "type": "uint256"},
        ],
        "name": "approve",
        "outputs": [{"name": "", "type": "bool"}],
        "type": "function",
    },
]

DEMO_SCOPE = 10 * 10**USDC_DECIMALS  # approve up to 10 USDC for this demo

usdc = web3.eth.contract(address=USDC, abi=ERC20_ABI)
current = usdc.functions.allowance(account.address, CONTRACT_ADDRESS).call()

if current < DEMO_SCOPE:
    approve_tx = usdc.functions.approve(CONTRACT_ADDRESS, DEMO_SCOPE).build_transaction({
        "from": account.address,
        "nonce": web3.eth.get_transaction_count(account.address),
        "gasPrice": web3.eth.gas_price,
    })
    signed = web3.eth.account.sign_transaction(approve_tx, private_key=PRIVATE_KEY)
    tx_hash = web3.eth.send_raw_transaction(signed.raw_transaction)
    web3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
    print(f"Approved 10 USDC for {CONTRACT_ADDRESS}")
A few BopAMM-specific notes:
  • The approval target is the BopAmmV2 contract itself (CONTRACT_ADDRESS). There’s no separate balance manager.
  • Native ETH in needs no approval. Send msg.value == amountIn and pass the sentinel address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE as tokenIn.
  • Cross-pair swaps (for example WETH to WBTC) don’t require a USDC approval. Intermediate USDC stays inside the contract.
This demo approves a fixed 10 USDC to keep the allowance scoped. Programmatic integrators usually approve the maximum amount once per token to avoid re-approving on every trade. See Token Approvals.

2. Fetch the live state

You can get the current snapshot two ways: a call to GET /state, or the streaming WebSocket, which pushes a fresh snapshot whenever a new aggregated book is available. Both carry the same data: the snapshot block, the aggregated books, and the state_overrides you need to quote against the live book before the on-chain registry update lands. Use REST for a single swap and the stream for continuous pricing.
The operator exposes the current aggregated book at GET /state.
GET https://api.bebop.xyz/bopamm/ethereum/v1/state
X-API-Key: <your-api-key>
response = httpx.get(f"{API_BASE}/state", headers={"X-API-Key": API_KEY})
state = response.json()

state_contract = Web3.to_checksum_address(state["contract"])
print(f"state_block={state['stateBlock']} overrides={len(state['state_overrides'])}")
Example response (abridged):
{
  "stateBlock": 25203086,
  "targetBlock": 25203087,
  "contract": "0xDa7AfeeD01fe625CF15d187a19f94B45f00b8C5F",
  "state_overrides": {
    "0x99131f24ab938b63c7f74c5439520f0db9ce5184595a612398a2fe92fa728537": "0x01d02000000000000000000000000001dfff027fff0000000000000000000000",
    "0x99131f24ab938b63c7f74c5439520f0db9ce5184595a612398a2fe92fa728536": "0x6a19e77b02008187ff8000000000000000000000000000000000000000000000"
  },
  "books": {
    "0": {
      "bids": [["2006.7400000000", "0.0320000000"]],
      "asks": [["2007.3200000000", "4.0950000000"], ["2007.4200000000", "4.0950000000"]]
    }
  }
}
Key fields:
FieldDescription
stateBlockThe block the snapshot is valid for. Use it as block_identifier when you quote and simulate.
targetBlockThe next block the signed books target.
contractThe registry contract the overrides apply to. The REST response includes it (read it from there, don’t hardcode); the stream omits it, so source it once as shown in the WebSocket tab.
state_overridesSlot-to-value pairs you pass as stateDiff to eth_call to evaluate against the live book.
booksThe aggregated book per asset id, with bids and asks as [price, size] pairs (price in USDC per base unit, size in base-token units). This quickstart prices through quote() rather than reading books directly.

3. Get a quote

quote() is a view function that walks the book and returns the expected output for a given input. A plain eth_call to quote() only succeeds in a block where the registry update has already landed. To quote against the latest book at any point in the block, apply the state_overrides as a stateDiff against the contract from the snapshot.
amount_out = contract.functions.quote(USDC, WETH, amount_in).call(
    block_identifier=state["stateBlock"],
    state_override={state_contract: {"stateDiff": state["state_overrides"]}},
)

amount_after_slippage = amount_out * (10_000 - SLIPPAGE_BPS) // 10_000

print(f"Quoted amount: {amount_out / 10**WETH_DECIMALS} WETH")
# Quoted amount: 0.000498176673375445 WETH
The book lives in the registry contract’s storage. Each block, the operator submits a transaction that writes fresh signed prices to it. The state_overrides are exactly those writes, pre-stamped for stateBlock, so passing them as stateDiff lets quote() read the latest book at any point in the block. Without them, quote() only reflects the book once the operator’s update transaction has landed.

4. Simulate and size gas

Before broadcasting, run the swap as an eth_call against the same override to catch reverts (like Expired or InsufficientLiquidity) without spending gas, then estimate gas with the override to set a real gas limit.
expiry = web3.eth.get_block("latest")["timestamp"] + 120

swap = contract.functions.swap(
    USDC, WETH, amount_in, amount_after_slippage, expiry, account.address
)

sim_kwargs = dict(
    transaction={"from": account.address, "value": 0},
    block_identifier=state["stateBlock"],
    state_override={state_contract: {"stateDiff": state["state_overrides"]}},
)

try:
    simulated_out = swap.call(**sim_kwargs)
except ContractLogicError as exc:
    raise SystemExit(f"Swap simulation reverted: {exc}")

estimated_gas = swap.estimate_gas(**sim_kwargs)

print(f"Simulated amount out: {simulated_out / 10**WETH_DECIMALS} WETH")
print(f"Estimated gas: {estimated_gas}")
# Simulated amount out: 0.000498176673375445 WETH
# Estimated gas: 166077
Pass the same state_override to estimate_gas. A plain estimate_gas at the chain head reverts with StaleUpdate, because the on-chain book isn’t current until the registry update lands.

5. Submit through a block builder

This is the step that differs from RFQ and Aggregation. A BopAMM swap() settles only when a builder that supports BopAMM includes it in the same block as the operator’s registry update for the assets involved. That happens two ways: submit the transaction directly to a supported builder, or send it to any node and have a supported builder win the block.
For a plain swap(), submit directly to a supported builder RPC. A public-mempool submission only goes through when a supporting builder happens to win the block; when a non-supporting builder wins, it reverts with StaleBook. To drop this constraint, use swapWithFallback, which settles via RFQ in the same transaction when the BopAMM leg can’t land.
Fetch EIP-1559 fees from Blocknative:
gas_resp = httpx.get(BLOCKNATIVE_GAS_URL)
estimates = gas_resp.json()["blockPrices"][0]["estimatedPrices"]
Each estimate pairs a confidence (the chance of inclusion in the next block) with the fees that buy it. Higher confidence costs more:
{
  "unit": "gwei",
  "blockPrices": [
    {
      "blockNumber": 25203087,
      "baseFeePerGas": 1.021795326,
      "estimatedPrices": [
        { "confidence": 99, "maxPriorityFeePerGas": 0.098, "maxFeePerGas": 2.1 },
        { "confidence": 95, "maxPriorityFeePerGas": 0.094, "maxFeePerGas": 2.1 },
        { "confidence": 90, "maxPriorityFeePerGas": 0.089, "maxFeePerGas": 2.1 },
        { "confidence": 80, "maxPriorityFeePerGas": 0.079, "maxFeePerGas": 2.1 },
        { "confidence": 70, "maxPriorityFeePerGas": 0.069, "maxFeePerGas": 2.1 }
      ]
    }
  ]
}
Pick the cheapest estimate that still clears your confidence threshold, then build and sign the transaction. Pad the estimated gas (here by 50%) to absorb book changes between simulation and inclusion:
# Lowest estimate that still meets the confidence threshold.
estimate = min(
    [e for e in estimates if int(e["confidence"]) >= BLOCKNATIVE_CONFIDENCE],
    key=lambda e: int(e["confidence"]),
    default=estimates[0],
)
max_fee = int(float(estimate["maxFeePerGas"]) * 1e9)
max_priority_fee = int(float(estimate["maxPriorityFeePerGas"]) * 1e9)
# With a 75% threshold, the 80% estimate wins: max_fee=2.1 gwei, priority=0.079 gwei.

tx = swap.build_transaction({
    "from": account.address,
    "nonce": web3.eth.get_transaction_count(account.address),
    "gas": int(estimated_gas * 1.5),
    "maxFeePerGas": max_fee,
    "maxPriorityFeePerGas": max_priority_fee,
    "chainId": web3.eth.chain_id,
})

signed_tx = web3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY)
Then send the raw transaction to the builder RPC:
response = httpx.post(
    BUILDER_RPC_URL,
    json={
        "jsonrpc": "2.0",
        "method": "eth_sendRawTransaction",
        "params": [f"0x{signed_tx.raw_transaction.hex()}"],
        "id": 1,
    },
)
print(response.json())
The builder returns a JSON-RPC result with the transaction hash:
{
  "jsonrpc": "2.0",
  "result": "0xa1a46dff1438f59eb332adb778c6b67fd9b8d3ad39968047bc8ad72fe25677ec",
  "id": 1
}
The result is your transaction hash. Track it on a block explorer to confirm it landed in the target block.
Can’t tolerate a same-block miss? The /quote endpoint returns ready swapWithFallback calldata that tries BopAMM first and settles via RFQ if the on-chain book can’t fill. See Falling back to RFQ.

Next steps

Coming soon. Guides on simulating swaps, same-block execution, and cross-pair routing, plus the full state schema and contract reference, will publish here as the closed beta opens to API access.