Skip to main content

Post-Intent Hooks

Overview​

note

OrchestratorV3 was retired on 2026-11-05. This page preserves its historical hook interfaces and examples; do not use them to signal or fulfill new intents. OrchestratorV4, which serves new intents, has no hooks: it transfers funds to to without a callback, or holds the payout under Stake to Take. Run follow-up actions such as bridging or splitting after settlement.

A post-intent hook runs when an OrchestratorV3 intent is fulfilled, before funds reach the recipient, for example to split a payout or forward funds to another contract. The taker sets the hook when signaling (SignalIntentParams.postIntentHook). Without a hook, funds go to intent.to.

Interface​

interface IPostIntentHookV2 {
struct HookIntentContext {
address owner;
address to;
address escrow;
uint256 depositId;
uint256 amount;
uint256 timestamp;
bytes32 paymentMethod;
bytes32 fiatCurrency;
uint256 conversionRate;
bytes32 payeeId;
bytes signalHookData; // SignalIntentParams.data
}

struct HookExecutionContext {
bytes32 intentHash;
address token; // deposit token
uint256 executableAmount; // amount after all fees
HookIntentContext intent;
}

function execute(
HookExecutionContext calldata _ctx,
bytes calldata _fulfillHookData // FulfillIntentParams.postIntentHookData
) external;
}

Source: IPostIntentHookV2.sol

Execution model​

On a successful fulfillIntent, OrchestratorV3:

  1. Pays protocol, manager and referral fees from the release amount, leaving executableAmount.
  2. Approves the hook for exactly executableAmount of the deposit token.
  3. Calls hook.execute(ctx, postIntentHookData).
  4. Checks that the hook pulled exactly executableAmount and that the orchestrator's token balance did not increase, then resets the allowance to zero.

If a check fails or the hook reverts, the whole fulfillment reverts.

Hook data​

  • SignalIntentParams.data is stored with the intent and passed as signalHookData. Use it for fixed configuration such as split recipients.
  • FulfillIntentParams.postIntentHookData is passed at fulfillment. Use it for inputs known only then.

Example: split payout​

contract SplitHookV2 is IPostIntentHookV2 {
address public immutable orchestrator;
address public immutable builder;

constructor(address _orchestrator, address _builder) {
orchestrator = _orchestrator;
builder = _builder;
}

function execute(HookExecutionContext calldata ctx, bytes calldata) external override {
require(msg.sender == orchestrator, "only orchestrator");
uint256 fee = ctx.executableAmount * 5 / 100;
IERC20(ctx.token).transferFrom(msg.sender, ctx.intent.to, ctx.executableAmount - fee);
IERC20(ctx.token).transferFrom(msg.sender, builder, fee);
}
}

Pulling less than executableAmount, or sending tokens back to the orchestrator, reverts the fulfillment.

Guidelines​

  • Restrict execute to the orchestrator and pull exactly executableAmount. Use OpenZeppelin SafeERC20 for tokens that return false.
  • Keep execution short and deterministic; a reverting hook blocks fulfillment.
  • Do not rely on re-entering the orchestrator; fulfillIntent is non-reentrant.
  • The legacy V1 IPostIntentHook and PostIntentHookRegistry apply only to the deprecated V1 orchestrator.