Skip to main content
In gasless mode, the user signs a quote and Bebop submits the settlement transaction on-chain. The user never pays gas - making it ideal for wallets and super-apps where UX simplicity matters. The trade-off: market makers retain last look, meaning they can reject a quote before settlement. In return, pricing is tighter than self-execution.
When to use gasless: You’re building a consumer-facing product where users shouldn’t think about gas. For solvers and aggregators executing trades programmatically, see the self-execution quickstart.

How It Differs from Self-Execution

Self-executionGasless (API default)
Quote requestgasless=falsegasless=true (default) + approval params
Who submits txYou broadcast via your RPCBebop submits on-chain
Order endpointNot used - tx comes from /v3/quotePOST signature to /v3/order
Last lookNo - quote is firm once signedYes - maker can reject
Gas costPaid by takerPaid by Bebop
Token approvalsStandard approve on settlement contractStandard or Permit2

1. Request a Gasless Quote

Add gasless=true and the relevant approval parameters to your quote request:
import httpx

NETWORK = "ethereum"
URL = f"https://api.bebop.xyz/pmm/{NETWORK}/v3/quote"

params = {
    "sell_tokens": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",  # WETH
    "buy_tokens": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",   # USDC
    "sell_amounts": "1000000000000000000",  # 1 WETH
    "taker_address": "0xYourWalletAddress",
    "gasless": "true",
}

resp = httpx.get(URL, params=params)
quote = resp.json()

Approval Parameters

ParameterDescription
gaslessSet to true to enable gasless mode
approval_typeStandard (default) or Permit2 - controls how token approvals are handled
Which approval type? Standard (the default) requires the user to have already approved the settlement contract. Permit2 removes the need for a separate approval transaction but adds a second signature step. For bundled approvals without a separate transaction, see Using Permit2 Approvals below.

2. Sign the Quote

Sign the EIP-712 typed data from the quote response - the same signing flow as self-execution:
from eth_account import Account
from eth_account.messages import encode_typed_data

PRIVATE_KEY = "0x<your_private_key_hex>"

typed_data = {
    "types": RFQ_EIP712_TYPES,  # see below
    "domain": {
        "name": "BebopSettlement",
        "version": "2",
        "chainId": quote["chainId"],
        "verifyingContract": quote["settlementAddress"],
    },
    "primaryType": quote["onchainOrderType"],
    "message": quote["toSign"],
}

signable = encode_typed_data(full_message=typed_data)
signed = Account.sign_message(signable, private_key=PRIVATE_KEY)
signature = signed.signature.hex()
The RFQ_EIP712_TYPES dictionary contains the EIP712Domain and all three order type definitions. The API selects the order type automatically based on the trade structure - use the onchainOrderType from the quote response as your primaryType.
TypeWhen usedKey differences
SingleOrderOne-to-one tradesScalar fields: maker_address, taker_token, taker_amount
MultiOrderSingle maker, multiple tokensArray fields: taker_tokens[], taker_amounts[]
AggregateOrderMultiple makersNested arrays: taker_tokens[][], maker_addresses[]
SingleOrder - simple one-to-one swaps (e.g. WETH → USDC):
"SingleOrder": [
    {"name": "partner_id", "type": "uint64"},
    {"name": "expiry", "type": "uint256"},
    {"name": "taker_address", "type": "address"},
    {"name": "maker_address", "type": "address"},
    {"name": "maker_nonce", "type": "uint256"},
    {"name": "taker_token", "type": "address"},
    {"name": "maker_token", "type": "address"},
    {"name": "taker_amount", "type": "uint256"},
    {"name": "maker_amount", "type": "uint256"},
    {"name": "receiver", "type": "address"},
    {"name": "packed_commands", "type": "uint256"},
]
MultiOrder - single maker fills a multi-token trade:
"MultiOrder": [
    {"name": "partner_id", "type": "uint64"},
    {"name": "expiry", "type": "uint256"},
    {"name": "taker_address", "type": "address"},
    {"name": "maker_address", "type": "address"},
    {"name": "maker_nonce", "type": "uint256"},
    {"name": "taker_tokens", "type": "address[]"},
    {"name": "maker_tokens", "type": "address[]"},
    {"name": "taker_amounts", "type": "uint256[]"},
    {"name": "maker_amounts", "type": "uint256[]"},
    {"name": "receiver", "type": "address"},
    {"name": "commands", "type": "bytes"},
]
AggregateOrder - multiple makers fill a multi-token trade:
"AggregateOrder": [
    {"name": "partner_id", "type": "uint64"},
    {"name": "expiry", "type": "uint256"},
    {"name": "taker_address", "type": "address"},
    {"name": "maker_addresses", "type": "address[]"},
    {"name": "maker_nonces", "type": "uint256[]"},
    {"name": "taker_tokens", "type": "address[][]"},
    {"name": "maker_tokens", "type": "address[][]"},
    {"name": "taker_amounts", "type": "uint256[][]"},
    {"name": "maker_amounts", "type": "uint256[][]"},
    {"name": "receiver", "type": "address"},
    {"name": "commands", "type": "bytes"},
]

3. Submit the Order

Unlike self-execution, gasless orders are submitted to Bebop’s /v3/order endpoint. Bebop handles the on-chain settlement:
order_resp = httpx.post(
    f"https://api.bebop.xyz/pmm/{NETWORK}/v3/order",
    json={
        "quote_id": quote["quoteId"],
        "signature": f"0x{signature}",
    },
)
order = order_resp.json()
tx_hash = order["txHash"]
print(f"Bebop submitted transaction: {tx_hash}")
At this point Bebop has your signed order and will submit the settlement transaction on-chain.

4. Monitor Settlement

Poll the order status endpoint to track progress:
import time

while True:
    status_resp = httpx.get(
        f"https://api.bebop.xyz/pmm/{NETWORK}/v3/order-status",
        params={"quote_id": quote["quoteId"]},
    )
    status = status_resp.json()
    print(f"Status: {status['status']}")

    if status["status"] in ("Settled", "Confirmed", "Failed"):
        break

    time.sleep(2)

Order Statuses

StatusMeaning
PendingBebop received the order and is preparing to submit
SuccessMaker accepted - transaction is being broadcast
SettledTransaction confirmed on-chain - tokens have been transferred
ConfirmedFinal success state - settlement fully confirmed
FailedOrder failed. Covers last look rejections, on-chain failures, expiry, and validation errors.
The order-status response also includes txHash (when available) and amounts (received token amounts after settlement).
Last look rejections are expected in gasless mode. When a maker rejects, the status will be Failed. Your integration should handle this gracefully - request a new quote and retry. The user’s tokens are never at risk during a rejection.

Full Example

import time

import httpx
from eth_account import Account
from eth_account.messages import encode_typed_data

PRIVATE_KEY = "0x<your_private_key_hex>"
NETWORK = "ethereum"

# --- 1. Request a gasless quote ---

taker_address = "0x2e7E7cc62919eAf4c502dAC34753cFc5A29e9693"

quote_resp = httpx.get(
    f"https://api.bebop.xyz/pmm/{NETWORK}/v3/quote",
    params={
        "sell_tokens": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
        "buy_tokens": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "sell_amounts": "1000000000000000000",
        "taker_address": taker_address,
        "gasless": "true",
    },
)
quote = quote_resp.json()
buy_token = list(quote["buyTokens"].values())[0]
print(f'Quote: sell 1 WETH for {buy_token["amount"]} {buy_token["symbol"]}')

# --- 2. Sign the EIP-712 typed data ---

RFQ_ORDER_TYPES = {
    "SingleOrder": [
        {"name": "partner_id", "type": "uint64"},
        {"name": "expiry", "type": "uint256"},
        {"name": "taker_address", "type": "address"},
        {"name": "maker_address", "type": "address"},
        {"name": "maker_nonce", "type": "uint256"},
        {"name": "taker_token", "type": "address"},
        {"name": "maker_token", "type": "address"},
        {"name": "taker_amount", "type": "uint256"},
        {"name": "maker_amount", "type": "uint256"},
        {"name": "receiver", "type": "address"},
        {"name": "packed_commands", "type": "uint256"},
    ],
    "MultiOrder": [
        {"name": "partner_id", "type": "uint64"},
        {"name": "expiry", "type": "uint256"},
        {"name": "taker_address", "type": "address"},
        {"name": "maker_address", "type": "address"},
        {"name": "maker_nonce", "type": "uint256"},
        {"name": "taker_tokens", "type": "address[]"},
        {"name": "maker_tokens", "type": "address[]"},
        {"name": "taker_amounts", "type": "uint256[]"},
        {"name": "maker_amounts", "type": "uint256[]"},
        {"name": "receiver", "type": "address"},
        {"name": "commands", "type": "bytes"},
    ],
    "AggregateOrder": [
        {"name": "partner_id", "type": "uint64"},
        {"name": "expiry", "type": "uint256"},
        {"name": "taker_address", "type": "address"},
        {"name": "maker_addresses", "type": "address[]"},
        {"name": "maker_nonces", "type": "uint256[]"},
        {"name": "taker_tokens", "type": "address[][]"},
        {"name": "maker_tokens", "type": "address[][]"},
        {"name": "taker_amounts", "type": "uint256[][]"},
        {"name": "maker_amounts", "type": "uint256[][]"},
        {"name": "receiver", "type": "address"},
        {"name": "commands", "type": "bytes"},
    ],
}

order_type = quote["onchainOrderType"]

typed_data = {
    "types": {
        "EIP712Domain": [
            {"name": "name", "type": "string"},
            {"name": "version", "type": "string"},
            {"name": "chainId", "type": "uint256"},
            {"name": "verifyingContract", "type": "address"},
        ],
        order_type: RFQ_ORDER_TYPES[order_type],
    },
    "domain": {
        "name": "BebopSettlement",
        "version": "2",
        "chainId": quote["chainId"],
        "verifyingContract": quote["settlementAddress"],
    },
    "primaryType": order_type,
    "message": quote["toSign"],
}

signable = encode_typed_data(full_message=typed_data)
signed = Account.sign_message(signable, private_key=PRIVATE_KEY)
signature = signed.signature.hex()

# --- 3. Submit the order to Bebop ---

order_resp = httpx.post(
    f"https://api.bebop.xyz/pmm/{NETWORK}/v3/order",
    json={"quote_id": quote["quoteId"], "signature": f"0x{signature}"},
)
order = order_resp.json()
print(f'Order submitted - tx: {order["txHash"]}')

# --- 4. Poll for settlement ---

while True:
    status_resp = httpx.get(
        f"https://api.bebop.xyz/pmm/{NETWORK}/v3/order-status",
        params={"quote_id": quote["quoteId"]},
    )
    status = status_resp.json()
    print(f'Status: {status["status"]}')

    if status["status"] in ("Settled", "Confirmed"):
        print(f'Trade settled! tx: {status.get("txHash")}')
        break
    elif status["status"] == "Failed":
        print("Order failed - request a new quote and retry.")
        break

    time.sleep(2)
Dependencies: pip install httpx eth_account (or uv add httpx eth_account)

Using Permit2 Approvals

With approval_type=Standard, the user must have approved the settlement contract before trading (a one-time on-chain transaction per token). Permit2 removes this requirement by using an off-chain signature for token approvals instead.
This differs from the Aggregation API’s Permit2 flow. The Aggregation API wraps the order inside PermitBatchWitnessTransferFrom - you sign a single combined message. The RFQ API keeps order signing unchanged and adds a separate PermitBatch signature for token approvals.

How it works

  1. One-time setup: Approve the Permit2 contract (0x000000000022D473030F116dDEE9F6B43aC78BA3) for your sell tokens. This is a standard ERC-20 approve() - the same kind of transaction you’d do for the settlement contract with Standard approvals, but targeting the Permit2 contract instead.
  2. Request a quote with approval_type=Permit2:
params = {
    "sell_tokens": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "buy_tokens": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "sell_amounts": "1000000000000000000",
    "taker_address": taker_address,
    "gasless": "true",
    "approval_type": "Permit2",
}
  1. Sign the order the same way as Standard - BebopSettlement domain, same order types. The toSign data is identical.
  2. Check requiredSignatures in the quote response. If it contains token addresses, generate a PermitBatch signature:
from web3 import Web3

PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"

PERMIT2_ABI = [
    {
        "inputs": [
            {"name": "owner", "type": "address"},
            {"name": "token", "type": "address"},
            {"name": "spender", "type": "address"},
        ],
        "name": "allowance",
        "outputs": [
            {"name": "amount", "type": "uint160"},
            {"name": "expiration", "type": "uint48"},
            {"name": "nonce", "type": "uint48"},
        ],
        "stateMutability": "view",
        "type": "function",
    }
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
permit2 = w3.eth.contract(
    address=Web3.to_checksum_address(PERMIT2_ADDRESS), abi=PERMIT2_ABI
)

required_sigs = quote.get("requiredSignatures", [])

if required_sigs:
    settlement = quote["settlementAddress"]
    approvals_deadline = quote["expiry"]  # use quote expiry
    token_nonces = []
    token_addresses = []
    details = []

    for token_addr in required_sigs:
        amount, expiration, nonce = permit2.functions.allowance(
            Web3.to_checksum_address(taker_address),
            Web3.to_checksum_address(token_addr),
            Web3.to_checksum_address(settlement),
        ).call()
        token_nonces.append(nonce)
        token_addresses.append(token_addr)
        details.append({
            "token": token_addr,
            "amount": 2**160 - 1,  # max amount
            "expiration": approvals_deadline,
            "nonce": nonce,
        })

    permit_typed_data = {
        "types": {
            "EIP712Domain": [
                {"name": "name", "type": "string"},
                {"name": "chainId", "type": "uint256"},
                {"name": "verifyingContract", "type": "address"},
            ],
            "PermitBatch": [
                {"name": "details", "type": "PermitDetails[]"},
                {"name": "spender", "type": "address"},
                {"name": "sigDeadline", "type": "uint256"},
            ],
            "PermitDetails": [
                {"name": "token", "type": "address"},
                {"name": "amount", "type": "uint160"},
                {"name": "expiration", "type": "uint48"},
                {"name": "nonce", "type": "uint48"},
            ],
        },
        "domain": {
            "name": "Permit2",
            "chainId": quote["chainId"],
            "verifyingContract": PERMIT2_ADDRESS,
        },
        "primaryType": "PermitBatch",
        "message": {
            "details": details,
            "spender": settlement,
            "sigDeadline": approvals_deadline,
        },
    }

    permit_signable = encode_typed_data(full_message=permit_typed_data)
    permit_signed = Account.sign_message(permit_signable, private_key=PRIVATE_KEY)
    permit2_signature = permit_signed.signature.hex()
  1. POST to /order with the additional permit2 field:
order_body = {
    "quote_id": quote["quoteId"],
    "signature": f"0x{signature}",
}

if required_sigs:
    order_body["permit2"] = {
        "signature": f"0x{permit2_signature}",
        "approvals_deadline": approvals_deadline,
        "token_addresses": token_addresses,
        "token_nonces": token_nonces,
    }

order_resp = httpx.post(
    f"https://api.bebop.xyz/pmm/{NETWORK}/v3/order",
    json=order_body,
)
On subsequent trades for the same token, requiredSignatures will be empty once the Permit2 allowance is active. In that case, no permit2 field is needed in the POST body - it works the same as Standard.

permit2 Field Reference

FieldTypeDescription
signaturestringHex-encoded PermitBatch EIP-712 signature
approvals_deadlineintegerUnix timestamp - must match sigDeadline in the signed PermitBatch
token_addressesstring[]Token addresses from requiredSignatures
token_noncesinteger[]Permit2 nonces for each token (from allowance() call)