Sign an order
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.
1. Prepare the input token
Section titled “1. Prepare the input token”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.
2. Build the order
Section titled “2. Build the order”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.
3. Build and sign the Permit2 witness
Section titled “3. Build and sign the Permit2 witness”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.
4. Publish the signed pair
Section titled “4. Publish the signed pair”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.