Open the app

Shipping a correction executor

Read the interface a partner executor implements, how an adapter binds to one forever, and how anyone verifies the binding from outside.

What a partner team implements, deploys and binds to put its own executor behind a Hookr pool's correction lane, and how anyone outside checks the result. One executor is live today, WTH's; everything below is what it took, stated as the procedure rather than as that one integration's history. Arbitrage recapture is the same mechanism described for a reader who is not shipping one.

What the Lane Is

A Hookr market opens on a root hook, and the root it opens on is frozen into the pool. Most pools open on the default root, 0xb3cA29cF721380CEe8b8e4755F3865Ebc68Fe8cC, which runs the five native rules and nothing else. A pool can instead open on a recapture root, 0xb914f955294799de4b891bd2EA8AF628Fa1c68CC, which runs the same five rules in the same order and adds one step: the swap kernel hands a correction executor the pool and the swap, once in beforeSwap before the swap is priced and once in afterSwap after it settles. The executor trades the gap between this pool's price and another venue's inside the same transaction, reports the quote-denominated profit it realised, and pays it out.

Hookr's own contract in that path is an adapter. It holds no funds, decides no amounts, and names exactly one executor for the life of its deployment. What to trade, how much of it, against which venue, and at what price limit are the partner's decisions from end to end.

The Contracts in the Path

RoleAddressWritten by
Recapture root hook, the hooks field of every pool on it0xb914f955294799de4b891bd2EA8AF628Fa1c68CCHookr
Stack registry, which admits an executor and freezes it into a pool0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3Hookr
Executor adapter, bound once to one executor0x28AF7A3645080e926a3101461e0Ec0594D42D806Hookr
Correction executor0xc356cf51134e0DF02BFE880115DD8c66Ead45803WTH

The root's kernel id is 0xd8b6c165b82efc3b7498081f071ea4f2476e2fb9c61b2e614aba2a016f94555a, and its profile read sealed on 2026-09-12. The root carries the same fourteen-bit flag word as the default root, 0x28cc: a correction needs no additional hook permission, because the executor settles its own trade and the kernel returns a zero delta from the correction callback.

The Interface an Executor Implements

Three functions, published as src/interfaces/IHookrArbExecutorV3.sol in the public contracts repository:

interface IHookrArbExecutorV3 {
    function routeAdmissionOpen() external view returns (bool);

    function executionBlockNumber() external view returns (uint64);

    function executeArbitrage(HookrArbTypesV3.ExecutionRequest calldata request)
        external
        returns (uint256 realizedProfitQuote, bytes32 planDigest);
}

realizedProfitQuote is the gross the root reports in CorrectionAttemptSucceeded, denominated in the pool's quote currency. planDigest is that event's third indexed topic, so an executor that wants its plans traceable from logs makes the digest mean something.

One more function is required and is not on that interface. The contract a pool names in correctionExecutor, which is the adapter, must answer feePolicyId() returns (bytes32), and the registry admits the pool only when that answer equals the correctionFeePolicyId the pool is freezing. The live value is 0xd2653e091cb7002585fd8b1192b58f11b9c951061dc797123e37d5e2ccb45cef, which HookrWthFeePolicyV2.FEE_POLICY_ID builds in the public contracts repository from a string naming all five shares.

There Is Exactly One Lawful Waterfall

Read this before designing an executor around its own split, because nothing else on this page will stop you. The deployed registry does not admit a second policy id at all. HookrStackRegistryV1.sol:1150 reverts InvalidStackLimits() unless a pool's correctionFeePolicyId equals HookrWthFeePolicyV2.FEE_POLICY_ID literally, and the comparison against the executor's own feePolicyId() at :1153-1157 runs only after that constant has already matched. HookrStackRegistryV2 is a 52-line subclass and does not override _validateLimits, and all five roots share one registry: stackRegistry() on 0xb914f955294799de4b891bd2EA8AF628Fa1c68CC returns 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3, the same address the default root reads. A newly mined root cannot escape the constant.

So the only split a pool can carry today is creator 40%, trader 20%, trigger-pool LP 20%, partner 10%, Hookr 10%. An executor answering a different feePolicyId() cannot be reached: the failure arrives at the first createMarket, as InvalidStackLimits(), after the adapter, the binding, the registration, the mined root and the sealed profile are all spent. A different waterfall needs a registry change, not a new root, and that is a conversation to have before step 1 rather than after step 4.

What the Executor Is Handed

HookrArbTypesV3 in the public contracts repository carries the request types. ExecutionRequest is what the hook builds: the pool being corrected, the direction and size of the swap that triggered it, the recipient to rebate, the split to pay, the pool's two frozen correction fields, and an optional signed plan with its signature.

That is not what the executor is handed. The adapter narrows it. HookrWthExecutorAdapterV1.sol:189-193 encodes three arguments and nothing else:

function executeArbitrage(PoolKey calldata triggeringPool, address rebateRecipient, ProfitSplit calldata split)
    external
    returns (uint256 profit);

That is IWthArbitrageExecutorV1, published beside the adapter. Everything else on the request is dropped at the boundary: the swap's direction and trigger size, the pool's frozen volume and profit fields, and the plan with its r, s and v. An executor sizing its trade from the triggering swap, or honouring the pool's frozen fields, cannot do either from this call. Sizing and the profit floor are its own, which is the same fact stated from the other side in the section below.

The phase is not recoverable either, even indirectly. HookrArbTypesV3 fixes PHASE_BEFORE_SWAP = 1 and PHASE_AFTER_SWAP = 2, and those are the indexed phase on all three events, but they reach the correction library and stop there. On the external route there is no plan to carry one: HookrModularCorrectionLibV3.sol:94 declares plan and assigns it only inside if (raw.length != 0) at :98-128, so plan.phase is zero on exactly the flow this page calls ordinary. An executor that must price a pre-existing gap differently from one the trader just opened cannot be written against this boundary as it stands.

The Adapter Binds Once and Is Spent

The adapter's binding is a single call:

function setExecutorOnce(address executor) external;

It succeeds once. Afterwards it reverts ExecutorAlreadySet(), selector 0xa1002dd1, from the owner who made the first call; onlyOwner is evaluated first, so a call from any other address reverts NotOwner(), 0x30cd7471, and demonstrates nothing. The live adapter 0x28AF7A3645080e926a3101461e0Ec0594D42D806 was bound to 0xc356cf51134e0DF02BFE880115DD8c66Ead45803 on 2026-09-12, so that adapter can never point anywhere else. A different executor means a different adapter, and because the adapter address is part of a sealed root profile, it also means a different root.

The binding runs in one direction only. The executor is configured with the adapter it will accept the correction call from, so the adapter has to exist and have a known address before the executor can be deployed or pointed at it, and the adapter has to be bound before a pool naming it can be admitted. The order is adapter, then executor, then the binding, then the sealed profile, then the first pool. Hookr cannot demonstrate the executor's side of that from published source, because the executor's source is not verified on the explorer.

Getting an Executor Admitted

Four steps, the first of them the partner's, in this order.

  1. The partner deploys the executor and gives Hookr its address, its feePolicyId(), and the waterfall the policy id commits to.
  2. Hookr deploys an adapter for it, and the adapter is bound to that executor with setExecutorOnce.
  3. The Hookr owner registers the adapter on HookrStackRegistryV2 with registerIntegration, as a correction-executor integration. The registration round-trips the implementation's own integrationFamilyId() and integrationVersion() and must match them exactly. See HookrStackRegistryV2.
  4. The Hookr owner seals a root profile naming that integration id, on a newly mined root hook. sealRootProfile succeeds once per kernel, so an existing root cannot acquire a new executor: a new executor is a new root hook, mined to the same flag word, with its own kernel id and its own profile.

Then a pool can open on that root with the five correction fields set. Leaving them at zero opens a pool that can never correct, permanently, because the fields are inside the stack hash. Open a market with arbitrage recapture is that launch from the creator's side.

There is no self-service path and no application flow inside the contracts. registerIntegration and sealRootProfile are owner-only. Publishing and partner review is where a partner starts.

The Five Fields a Pool Freezes

StackLimits carries them, and the registry validates them at admission when correctionExecutor is non-zero:

FieldWhat it must be
correctionExecutorthe adapter registered for this root's sealed profile
correctionCreatornon-zero; the address the creator share is paid to
correctionMaxVolumeBpsgreater than zero and at most MAX_CORRECTION_VOLUME_BPS, which is 5000
correctionMinProfitQuotenon-zero
correctionFeePolicyIdequal to the feePolicyId() the executor contract answers with

Admission also requires a native-ETH quote, meaning currency0 == address(0). That is the whole reason recapture serves tokens paired with ETH and no other pair today: the deployed registry admits a correction executor on native-quoted pools only.

correctionMaxVolumeBps and correctionMinProfitQuote are frozen and not enforced by Hookr. The registry requires the fields to be present and inside those bounds, and nothing downstream reads them as a cap: the executor's interface takes neither number, so sizing and the profit floor are the partner's. The live pools carry the registry's permissive maximum and its smallest legal floor of 1 wei rather than numbers that would read as a limit Hookr applies and does not.

The MEV View, and Why the Hook Calls It

Before it corrects, beforeSwap on 0xb914f955294799de4b891bd2EA8AF628Fa1c68CC staticcalls the executor's MEV view and reverts the swap when it returns a clean true. True means a registered v3 pool for this pair is locked for re-entrancy, so the caller is inside that pool's callback: an arbitrage bot that moved the reference venue first and is closing its leg here. The root accepts both spellings the executor may carry, checkV3PoolsMev at selector 0x6075521e and checkV3PoolsMEV at selector 0x85c30352, and treats anything other than a clean true as no answer.

The check sits in the hook and not in the adapter on purpose. The adapter is called from inside the kernel's try/catch, where a revert becomes a skipped correction rather than a failed swap, so a refusal expressed there would be silently swallowed. In the hook it is a refusal, which is the intended outcome. A router that trips it should read Routing a correcting pool.

The Registry of Reference Venues

A correction needs somewhere to trade against. The executor keeps its own registry of venues per (token, quote) pair, and exposes it:

function getTokenPools(address token, address quote) external view returns (PoolKey[] memory); // 0xeb1af5d2
function registeredPools(bytes32 poolId) external view returns (uint256);                     // 0xea509a64

getTokenPools returns full v4 PoolKey tuples, not ids. registeredPools answers non-zero for a pool id that is a registered venue. For HOOKR against ETH the registry holds one hookless v4 pool at fee 2500 and tick spacing 25.

Two consequences a partner should state to creators rather than leave to be discovered. A token with no registered venue has nothing to correct against, so the lane is idle until the token trades somewhere the executor registers; choosing a recapture root at launch is a bet on that happening later, not a feature that works on day one. And registration is the partner's, not Hookr's: nothing in the Hookr graph adds, removes or reads a venue.

Verifying a Binding From Outside

These are the reads that settle each claim, without asking either team for anything.

ClaimThe read
The adapter points at this executorwthExecutor() on the adapter, selector 0x477a9d7c
The binding is spenta second setExecutorOnce reverts ExecutorAlreadySet(), 0xa1002dd1
The policy the pool committed tofeePolicyId() on the adapter, selector 0xc893dd97, against the pool's correctionFeePolicyId
This pool actually carries the lanestack(poolId) on 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3: kernel is the root, limits.correctionExecutor is the adapter, and correctionExecutorIntegrationId is non-zero
The registry's admission read will passrouteAdmissionOpen(), selector 0xc93b3f0f, on the adapter; see the caveat below
Which pools run on this root, and what to watchGET /api/wth/pools on hookr.fun, which returns the root, its kernel id, the coordinator, and the signature and topic0 of all four events, each read off the deployed ABI

routeAdmissionOpen() proves less than its name suggests, and it is the one row here worth reading twice. On this adapter it is pure and returns a hard-coded true (HookrWthExecutorAdapterV1.sol:157-160, whose own comment says admission is the partner's to decide inside their own executor). It answers true against a paused, broken or hostile executor just as readily. It belongs in the admission table, because the registry really does call it, and not in a list of boundary checks.

The one thing that cannot be checked this way is the executor's behaviour. Its source is not verified on the explorer. The root hook, its swap kernel, its correction library and the adapter are all published in the public contracts repository now, so the Hookr side of the boundary is readable; The correction trust boundary states what is still unproven and why the manifest's provenance pin is not yet checkable from outside.