Skip to content

This page describes the client-side path for IntentSettlerV1: build an IntentOrder, sign a Permit2 PermitWitnessTransferFrom, then publish the signed pair with announce.

There is no AgentSwap SDK to install. The examples below are self-contained: the typed data is the whole contract between a client and Permit2, and any EIP-712 signer can produce it.

The order uses ERC-20 tokens on one chain. Before signing, the owner must give the canonical Permit2 contract an allowance for tokenIn:

tokenIn.approve(Permit2, allowance)

That approval is the on-chain gate for the later Permit2 pull. It is separate from the order signature. The settler pulls exactly amountIn from owner only when a solver calls fill.

See signing and authorization for the domain and approval model, and the order reference for the frozen 12-field struct.

The client helper derives raw integer amounts and Unix-second timestamps. Its recipient is set explicitly; it is not inferred by the settler.

// Amounts are integer strings in the token's own decimals; times are Unix seconds.
const order = {
owner, // the Permit2 signer
recipient, // where the output lands; need not be the owner
tokenIn,
amountIn, // exact, not a ceiling
tokenOut,
startAmountOut, // the floor at startTime
endAmountOut, // the floor the curve rests at
startTime, // decay begins
decayEndTime, // decay ends; the floor rests from here
endTime, // the order dies; also the Permit2 deadline
appData, // bytes32 attribution tag, or 32 zero bytes
nonce, // an unused Permit2 unordered nonce
};

The invariants the settler enforces on these values are on the curve page; an order that violates one reverts at announce, at fill and at resolve alike.

startAmountOut == endAmountOut represents a fixed-price limit order. startAmountOut > endAmountOut represents a descending Dutch auction. The nonce is a Permit2 unordered nonce and is included in the Permit2 wrapper, not in the witness fields.

The primary type is PermitWitnessTransferFrom, under Permit2’s own EIP-712 domain. It is not a plain EIP-712 message and it does not use the settler’s domain.

const typedData = {
primaryType: 'PermitWitnessTransferFrom',
domain: {
name: 'Permit2',
chainId,
verifyingContract: '0x000000000022D473030F116dDEE9F6B43aC78BA3',
},
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
PermitWitnessTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
{ name: 'witness', type: 'IntentWitness' },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
IntentWitness: [
{ name: 'owner', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'tokenIn', type: 'address' },
{ name: 'amountIn', type: 'uint256' },
{ name: 'tokenOut', type: 'address' },
{ name: 'startAmountOut', type: 'uint256' },
{ name: 'endAmountOut', type: 'uint256' },
{ name: 'startTime', type: 'uint256' },
{ name: 'decayEndTime', type: 'uint256' },
{ name: 'endTime', type: 'uint256' },
{ name: 'appData', type: 'bytes32' },
],
},
message: {
permitted: { token: order.tokenIn, amount: order.amountIn },
spender: '0x7eE5E32d16a90BD4ab3d048B3EE04A7D2d174c4E',
nonce: order.nonce,
deadline: order.endTime,
witness: {
owner: order.owner,
recipient: order.recipient,
tokenIn: order.tokenIn,
amountIn: order.amountIn,
tokenOut: order.tokenOut,
startAmountOut: order.startAmountOut,
endAmountOut: order.endAmountOut,
startTime: order.startTime,
decayEndTime: order.decayEndTime,
endTime: order.endTime,
appData: order.appData,
},
},
};
const permit2Sig = await provider.request({
method: 'eth_signTypedData_v4',
params: [owner, JSON.stringify(typedData)],
});

The Permit2 domain carries no version field. The witness omits nonce, which Permit2 signs itself in the wrapper.

The typed data must contain the following bindings:

Binding Value
domain.name Permit2
domain.verifyingContract the canonical Permit2 address
primaryType PermitWitnessTransferFrom
permitted TokenPermissions(tokenIn, amountIn)
spender the IntentSettlerV1 address
nonce order.nonce
deadline order.endTime

The IntentWitness contains the other 11 order fields, including recipient, decayEndTime, and appData. permitDigest(order) is a view that returns the exact digest the settler will ask Permit2 to verify.

announce(order, permit2Sig) is permissionless and optional. The owner can self-submit it, or another broadcaster can publish the same signed pair. The signature is carried in IntentAnnounced; it is consumed later by fill.

// announce((O),bytes) — selector 0x294470ec. Any account may send it.
await provider.request({
method: 'eth_sendTransaction',
params: [{ from: account, to: settler, data: encodeAnnounce(order, permit2Sig) }],
});

Verify what you built before you rely on it: permitDigest(order) (0x9946e08f) is a view returning the exact digest Permit2 will check, and orderHash(order) (0xd94a1dc6) returns the id the events are keyed by. Both are eth_calls against the settler.

The announcement verifies the signature, but it does not make the order fillable. A spent nonce, a missing Permit2 allowance, a changed ERC-1271 wallet configuration, or an unusable route can still prevent settlement. A solver must screen the order and eth_call-dry-run fill immediately before submitting it; see fill an order.