Skip to main content
Hooks let you attach custom on-chain logic that the BebopRouter runs in the same transaction as the swap, either before the swap (a pre-hook) or after it (a post-hook). The router executes your hook with your swap legs scaled to the actual filled amount, so partial fills are handled for you.

Use cases

  • Trade assets you don’t hold. Mint or wrap an asset just-in-time as a pre-hook so the PMM can pull it. Examples: RWA tokens, liquid-staking and wrapped tokens, Aave aTokens.
  • Source liquidity from lending protocols. Borrow or redeem from Aave, Morpho, and similar as a pre-hook to free up the token you’re about to sell, then re-supply leftovers as a post-hook.
  • Transform the output. Wrap or convert the token you receive as a post-hook.

Returning a hook

In your WebSocket quote response, add a hooks array alongside the quote. Each entry is:
{
  "target_contract": "0x...",
  "data": "0x...",
  "post_hook": false,
  "revert_on_fail": true,
  "use_bebop_hook": true,
  "needs_approval": false,
  "hook_signature": "0x..."
}
FieldDescription
target_contractThe contract the router calls.
dataThe payload: ABI-encoded arguments for bebopHook, or raw calldata.
post_hookfalse runs the hook before the swap (for example JIT-mint inventory), true runs it after (for example wrap the output).
revert_on_failtrue reverts the whole swap if the hook reverts.
use_bebop_hooktrue calls IBebopHook.bebopHook, false makes a raw call with data.
needs_approvaltrue makes the router approve target_contract before calling it (pre-hook: the from-token; post-hook: the PMM to-token). Set false if your hook pulls funds via the maker’s own pre-approval.
hook_signatureThe maker’s EIP-712 signature over the hook. Omit, or send null or "0x", to skip signature verification.
You send only the booleans; the PMM packs them, together with your maker address, into the on-chain flags value for you.

Signing a hook

If your hook acts on behalf of you (the maker), for example pulling your funds or minting to you, sign it. EIP-712-sign this struct:
BebopHook(address targetContract, bytes32 dataHash, uint256 makerNonce, uint256 flags)
  • Domain: name = "BebopRouter", version = "1", the chain id, and verifyingContract set to the router address 0xBeb0009ACa35087ce7cCF11637E24dd1Aad3bf2A.
  • dataHash = keccak256(data).
  • makerNonce is the same nonce as your PMM order for this quote. Issue a unique maker nonce per order, as you already do for PMM orders, and reuse it here.
  • flags is the same packed flags value the router uses for the hook (see Flags).
This is separate from your PMM order signature, which is unchanged (BebopSettlement v2). Signing binds the hook both ways:
  • The swap can’t run without your hook, or with a different one. The router folds the set of hooks into a hooksHash that is part of the order EIP-712 hash you sign, so the swap executes only with exactly the hooks you authorized.
  • The hook can’t run without the swap, and can’t be replayed. The hook signature is over your maker nonce, which the PMM settlement consumes atomically with the swap. Your authorization is used at most once, and only as part of the swap you signed.
If you omit the signature, the router sets makerAddress = 0 and skips signature verification. Only do this for hooks that don’t act on your behalf and don’t move your funds.

Contract side

A hook is one entry in the Hook[] array the router executes:
struct Hook {
    address targetContract;  // contract the router calls
    bytes   data;            // payload
    bytes   hookSignature;   // maker's EIP-712 signature; empty if makerAddress == 0
    uint256 flags;           // packed, see below
}

Flags

The flags field packs the hook’s configuration. You compute the same value when signing.
BitsFieldMeaning
0-159makerAddressThe maker that signed this hook. address(0) means no signature verification.
160postHook1 runs after the swap, 0 runs before.
161revertOnFail1 reverts the whole swap if the hook reverts.
162useBebopHook1 calls IBebopHook.bebopHook, 0 makes a raw call with data.
163needsApproval1 approves targetContract before calling it.

Implementing the target

You have two ways to implement the target contract. Option A: implement IBebopHook (useBebopHook = 1). Deploy a contract the router calls with the maker address and the scaled swap legs:
struct Swap { uint256 takerAmount; address takerToken; uint256 makerAmount; address makerToken; }

interface IBebopHook {
    /// @param makerAddress  the maker that signed this hook (from Hook.flags), passed by the router
    /// @param data          your arbitrary payload from Hook.data
    /// @param swaps         this maker's swap legs, scaled to the filled amount
    function bebopHook(address makerAddress, bytes calldata data, Swap[] calldata swaps) external;
}
This is the recommended style for anything that acts on behalf of a maker (pulling the maker’s funds, minting to the maker). The router supplies makerAddress itself from the signed hook, so a malicious data can’t redirect it. Option B: raw call (useBebopHook = 0). The router does targetContract.call(data). Use this to call an existing contract that doesn’t implement IBebopHook. The router blocks raw-call payloads whose first 4 bytes equal the bebopHook selector, to prevent bypassing the signed path. For the full contracts, see the bebop-rfqa repository.

Example: JIT-mint an Aave aToken

Source: MakerAaveHelperHook.sol. Suppose you want to stream and trade an Aave aToken (for example aWETH or aPOL) that you don’t actually hold. Instead of pre-minting and holding inventory, mint it just-in-time as a pre-hook:
1

Router calls the hook

Before the PMM swap, the router calls the hook with your maker address and the scaled swap legs.
2

Pull the underlying

The hook pulls the underlying (for example WETH) from your wallet, which you pre-approved.
3

Supply to Aave

The hook supplies the underlying to Aave V3 and the aToken is minted.
4

Forward the aToken

The hook forwards exactly the needed aToken amount to your wallet.
5

PMM pulls the aToken

The PMM then transferFroms the aToken from you to the router as usual.
It’s a single shared deployment: any maker can use the same hook contract. The router passes your makerAddress, so the hook only ever touches your funds for your legs. One-time approvals (not per swap):
  • underlying to MakerAaveHelperHook, so the hook can pull the underlying from you.
  • aToken to the settlement contract, so the PMM can pull the minted aToken from you.
Building the hook from your backend. The bebopHook payload is just the three addresses; the router prepends your maker address when it calls:
# data = abi.encode(underlying, aToken, aavePool)
data = abi_encode(
    ["address", "address", "address"],
    [underlying, a_token, aave_pool],
)

# flags = makerAddress | useBebopHook | revertOnFail
# pre-hook (postHook bit = 0); needsApproval = 0 because the maker pre-approved the hook
flags = int(maker_address, 16) | (1 << 162) | (1 << 161)

# EIP-712 sign BebopHook(targetContract, keccak(data), makerNonce, flags)
hook_signature = sign_bebop_hook(
    target_contract=helper_hook_address,
    data_hash=keccak(data),
    maker_nonce=maker_nonce,   # same nonce as the PMM order
    flags=flags,
    router=ROUTER_ADDRESS,
    chain_id=chain_id,
)

# return in the WebSocket quote response
hook = {
    "target_contract": helper_hook_address,
    "data": "0x" + data.hex(),
    "post_hook": False,
    "revert_on_fail": True,
    "use_bebop_hook": True,
    "needs_approval": False,
    "hook_signature": hook_signature,
}
That’s the whole integration: pre-approve once, then per quote return a signed hook in your WebSocket response. The router handles fill-scaling, ordering (pre versus post), and verifying your signature before it runs anything.