> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bebop.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Fetch the live BopAMM state, quote and simulate a swap mid-block, and submit it through a block builder.

This guide walks you through a complete BopAMM swap on Ethereum: approving the router, fetching the current state, quoting against the live on-chain book, simulating the router swap to catch reverts, and submitting it through a supported block builder. You'll sell 1 USDC for WETH.

<Info>
  **What you'll build:** A USDC to WETH `swapWithAllowance` transaction against BopAMM, quoted and simulated mid-block, then submitted through a block builder.

  **Time required:** 15-20 minutes

  **Prerequisites:** Basic EVM knowledge and web3.py, a BopAMM API key, a funded signing key, and the ability to submit transactions through a supported builder RPC.
</Info>

## Setup

Install `web3`, `eth-account`, and `httpx`, then define the constants and contract handles used throughout. This quickstart uses `BopAmm.quote()` to quote the swap and `BopAmmRouter.swapWithAllowance()` to execute it.

```python theme={null}
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"

# Send the signed transaction to a builder route that supports BopAMM.
BUILDER_RPC_URL = "<supported-builder-rpc-url>"

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

BOPAMM_CORE_ADDRESS = Web3.to_checksum_address("0xB09AAA8933626d7E4C48D65dAd2D77021CFBCA9a")
BOPAMM_ROUTER_ADDRESS = Web3.to_checksum_address("0xB098881c587f623FAC85eAe60809bEE7A174CeE7")
NATIVE_ETH = Web3.to_checksum_address("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE")

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
FEE_BPS = 0        # no integrator fee in this quickstart
FEE_RECIPIENT = "0x0000000000000000000000000000000000000000"

CORE_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"}],
    }
]

ROUTER_ABI = [
    {
        "name": "swapWithAllowance",
        "type": "function",
        "stateMutability": "payable",
        "inputs": [
            {"name": "tokenIn", "type": "address"},
            {"name": "tokenOut", "type": "address"},
            {"name": "amountIn", "type": "uint256"},
            {"name": "minAmountOut", "type": "uint256"},
            {"name": "recipient", "type": "address"},
            {"name": "deadline", "type": "uint256"},
            {"name": "fee", "type": "uint256"},
        ],
        "outputs": [{"name": "amountOut", "type": "uint256"}],
    }
]

web3 = Web3(Web3.HTTPProvider(RPC_URL))
account = Account.from_key(PRIVATE_KEY)
core = web3.eth.contract(address=BOPAMM_CORE_ADDRESS, abi=CORE_ABI)
router = web3.eth.contract(address=BOPAMM_ROUTER_ADDRESS, abi=ROUTER_ABI)


def pack_router_fee(recipient: str, fee_bps: int) -> int:
    if fee_bps == 0:
        return 0
    if fee_bps > 1_000:
        raise ValueError("BopAMM router fees are capped at 1,000 bps")
    return (fee_bps << 160) | int(Web3.to_checksum_address(recipient), 16)


def amount_after_router_fee(amount_out: int, fee_bps: int) -> int:
    return amount_out - (amount_out * fee_bps // 10_000)


FEE = pack_router_fee(FEE_RECIPIENT, FEE_BPS)
amount_in = 1 * 10**USDC_DECIMALS  # sell 1 USDC
```

<Note>
  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.
</Note>

## 1. Approve the BopAMM router

Before swapping ERC-20 input, the router needs permission to pull your USDC. Approve `BopAmmRouter` for the scope of this demo (10 USDC). This is the standard check-and-approve pattern. See [Token Approvals](/core-concepts/token-approvals) for the canonical helper and the max-vs-exact tradeoffs.

```python theme={null}
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, BOPAMM_ROUTER_ADDRESS).call()

if current < DEMO_SCOPE:
    approve_tx = usdc.functions.approve(BOPAMM_ROUTER_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 {BOPAMM_ROUTER_ADDRESS}")
```

A few BopAMM-specific notes:

* The approval target is the **BopAmmRouter contract** (`BOPAMM_ROUTER_ADDRESS`). The core pool does not pull taker funds on the router path.
* **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 contracts.

<Tip>
  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](/core-concepts/token-approvals#approval-strategies).
</Tip>

## 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 update lands. Use REST for a single swap and the stream for continuous quoting.

<Tabs>
  <Tab title="Polling over REST">
    The operator exposes the current aggregated book at `GET /state`.

    ```text theme={null}
    GET https://api.bebop.xyz/bopamm/ethereum/v1/state
    Authorization: Bearer <your-api-key>
    ```

    ```python theme={null}
    response = httpx.get(f"{API_BASE}/state", headers={"Authorization": f"Bearer {API_KEY}"})
    response.raise_for_status()
    state = response.json()

    state_contract = Web3.to_checksum_address(state["contract"])
    state_override = {state_contract: {"stateDiff": state["state_overrides"]}}
    print(
        f"state_block={state['stateBlock']} "
        f"target_block={state['targetBlock']} "
        f"overrides={len(state['state_overrides'])}"
    )
    ```

    Example response (abridged):

    ```json theme={null}
    {
      "stateBlock": 25203086,
      "stateBlockTimestamp": 1780382800,
      "targetBlock": 25203087,
      "targetBlockTimestamp": 1780382812,
      "contract": "0xda7afeed021eafc1c1af9c362de477dad0396b81",
      "state_overrides": {
        "0x99131f24ab938b63c7f74c5439520f0db9ce5184595a612398a2fe92fa728537": "0x01d02000000000000000000000000001dfff027fff0000000000000000000000",
        "0x99131f24ab938b63c7f74c5439520f0db9ce5184595a612398a2fe92fa728536": "0x6a19e77b02008187ff8000000000000000000000000000000000000000000000"
      },
      "books": {
        "1": {
          "bids": [["2006.7400000000", "0.0320000000"]],
          "asks": [["2007.3200000000", "4.0950000000"], ["2007.4200000000", "4.0950000000"]]
        }
      }
    }
    ```
  </Tab>

  <Tab title="Streaming over WebSocket">
    The operator streams `StateSnapshot` protobuf messages over a binary WebSocket. On connect, the server sends the current cached snapshot immediately, so you don't wait for the next tick.

    ```text theme={null}
    wss://api.bebop.xyz/bopamm/ethereum/v1/state
    Authorization: Bearer <your-api-key>
    ```

    The wire format is protobuf. Save the schema as `state_ws.proto` and generate a Python stub:

    ```protobuf state_ws.proto theme={null}
    syntax = "proto3";
    package bopamm;

    message Level {
      string price = 1;  // decimal string, USDC per base unit
      string size = 2;   // decimal string, base-token units
    }

    message AggregatedBook {
      repeated Level bids = 1;  // sorted descending by price
      repeated Level asks = 2;  // sorted ascending by price
    }

    message StateSnapshot {
      uint64 state_block = 1;      // latest confirmed block
      uint64 target_block = 2;     // next block the signed books target
      map<string, AggregatedBook> books = 6;     // assetId -> aggregated book
      map<string, string> state_overrides = 7;   // slot hex -> value hex
      uint64 state_block_ts = 8;   // unix seconds for state_block
      uint64 target_block_ts = 9;  // unix seconds for target_block
    }
    ```

    ```bash theme={null}
    protoc --python_out=. state_ws.proto
    ```

    This produces `state_ws_pb2.py` next to the proto file. Subscribe, skip the metadata-only frames, and take the first snapshot with populated levels:

    ```python theme={null}
    import asyncio
    import websockets
    import state_ws_pb2 as state_pb

    WS_URL = "wss://api.bebop.xyz/bopamm/ethereum/v1/state"

    async def get_latest_snapshot():
        async with websockets.connect(
            WS_URL, additional_headers={"Authorization": f"Bearer {API_KEY}"}
        ) as ws:
            while True:
                snap = state_pb.StateSnapshot()
                snap.ParseFromString(await ws.recv())
                if any(b.bids or b.asks for b in snap.books.values()):
                    return snap

    snap = asyncio.run(get_latest_snapshot())

    # The stream carries the per-block book and overrides but not the state
    # override target, which doesn't change block to block. Read it once from
    # GET /state and reuse it for every frame.
    state_contract = Web3.to_checksum_address(
        httpx.get(f"{API_BASE}/state", headers={"Authorization": f"Bearer {API_KEY}"}).json()["contract"]
    )

    # Normalize into the same shape the REST path produces, so the quote,
    # simulate, and submit steps below are identical.
    state = {
        "stateBlock": snap.state_block,
        "state_overrides": dict(snap.state_overrides),
    }
    state_override = {state_contract: {"stateDiff": state["state_overrides"]}}
    print(f"state_block={state['stateBlock']} overrides={len(state['state_overrides'])}")
    ```

    <Note>
      The first cached frame the server sends on connect is sometimes metadata-only: the `books` map has entries but the `bids` and `asks` arrays are empty until the next live tick. Iterate until you see populated levels (or apply a short timeout) before quoting.
    </Note>

    On the stream the fields arrive as `state_block`, `target_block`, `books`, and `state_overrides`, plus `state_block_ts` and `target_block_ts` (unix seconds). The `contract` field is not in the stream, so it's read once from `GET /state` above and reused for every frame.
  </Tab>
</Tabs>

Key fields:

| Field | Description |
| - | - |
| `stateBlock` | The block the snapshot is valid for. Use it as `block_identifier` when you quote and simulate. |
| `stateBlockTimestamp` | Unix timestamp of `stateBlock`. |
| `targetBlock` | The next block the signed books target. |
| `targetBlockTimestamp` | Unix timestamp expected for `targetBlock`. |
| `contract` | The address to apply `state_overrides` to in `eth_call` stateDiff. |
| `state_overrides` | Slot-to-value pairs you pass as `stateDiff` to evaluate against the live book. |
| `books` | The aggregated book per asset id, with `bids` and `asks` as `[price, size]` pairs. This quickstart quotes through `quote()` rather than reading `books` directly. |

## 3. Get a quote

`quote()` is a view function on the core pool that walks the book and returns the expected output for a given input. 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.

```python theme={null}
gross_amount_out = core.functions.quote(USDC, WETH, amount_in).call(
    block_identifier=state["stateBlock"],
    state_override=state_override,
)

amount_out = amount_after_router_fee(gross_amount_out, FEE_BPS)
amount_after_slippage = amount_out * (10_000 - SLIPPAGE_BPS) // 10_000

print(f"Quoted amount after fee: {amount_out / 10**WETH_DECIMALS} WETH")
# Quoted amount after fee: 0.000498176673375445 WETH
```

<Note>
  For native ETH at the router boundary, quote the core pool with WETH. The router normalizes the native ETH sentinel to WETH before calling `BopAmm`.
</Note>

## 4. Add a fee (optional)

To charge an integrator fee on the BopAMM leg, set `FEE_BPS` and `FEE_RECIPIENT` before building the router calldata:

```python theme={null}
FEE_BPS = 25  # 0.25%
FEE_RECIPIENT = "0x<your-fee-recipient>"
FEE = pack_router_fee(FEE_RECIPIENT, FEE_BPS)
```

The fee is taken from successful BopAMM output before `minAmountOut` is checked, so quote and simulate with `amount_after_router_fee(...)` as shown above. A configured percentage of the fee goes to the protocol; the rest goes to `FEE_RECIPIENT`.

## 5. Simulate and size gas

Before broadcasting, run the router swap as an `eth_call` against the same override to catch reverts without spending gas, then estimate gas with the override to set a real gas limit.

```python theme={null}
deadline = web3.eth.get_block("latest")["timestamp"] + 120

swap = router.functions.swapWithAllowance(
    USDC,
    WETH,
    amount_in,
    amount_after_slippage,
    account.address,
    deadline,
    FEE,
)

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

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
```

<Note>
  Pass the same `state_override` to `estimate_gas`. A plain `estimate_gas` at the chain head can revert before the current book has landed on-chain.
</Note>

## 6. Submit through a block builder

This is the step that differs from RFQ and Aggregation. A plain BopAMM swap settles reliably when a builder that supports BopAMM includes it in the same block as the matching book update.

Current supported builders are:

* Titan
* BuilderNet
* Quasar
* Bombora

<Warning>
  For a plain `swapWithAllowance`, submit directly to a supported builder RPC. A public-mempool submission only goes through when a supporting builder happens to win the block. To drop this constraint, use [`swapWithFallback`](/bopamm/guides/falling-back-to-rfq), which settles via RFQ in the same transaction when the BopAMM leg can't land.
</Warning>

Fetch EIP-1559 fees from Blocknative:

```python theme={null}
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:

```json theme={null}
{
  "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:

```python theme={null}
# 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,
    "value": 0,
})

signed_tx = web3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY)
raw_tx = Web3.to_hex(signed_tx.raw_transaction)
```

Then send the raw transaction to the builder RPC:

```python theme={null}
response = httpx.post(
    BUILDER_RPC_URL,
    json={
        "jsonrpc": "2.0",
        "method": "eth_sendRawTransaction",
        "params": [raw_tx],
        "id": 1,
    },
)
response.raise_for_status()
print(response.json())
```

The builder returns a JSON-RPC result with the transaction hash:

```json theme={null}
{
  "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.

<Tip>
  Can't tolerate a same-block miss? The `/quote` endpoint returns ready `swapWithFallback` calldata that tries BopAMM first and settles via RFQ if the BopAMM leg can't land. See [Falling back to RFQ](/bopamm/guides/falling-back-to-rfq).
</Tip>

## Next steps

<CardGroup cols={3}>
  <Card title="Fees" icon="badge-percent" href="/bopamm/guides/fees">
    Add optional integrator fees to router calldata or `/quote`.
  </Card>

  <Card title="Direct pool integration" icon="code" href="/bopamm/introduction#direct-pool-integration">
    Use push-payment `swap` or callback settlement directly against `BopAmm`.
  </Card>

  <Card title="Falling back to RFQ" icon="rotate-ccw" href="/bopamm/guides/falling-back-to-rfq">
    Get ready `swapWithFallback` calldata from the `/quote` endpoint.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.