AgentSwap Intents Protocol
Ownerless same-chain intent settlement via IntentSettlerV1
September 2026
This document describes IntentSettlerV1, the current AgentSwap Intents Protocol. Every
mechanism below is verified against the deployed contracts and the facts recorded in the
protocol source tree. An earlier generation — the clone-v1 stack (IntentFactory +
IntentClone) — is retired; its signatures, order shape, and settlement contracts are
incompatible with IntentSettlerV1 by design.
Abstract
Section titled “Abstract”AgentSwap Intents is an on-chain intent protocol in which a user signs a declarative order:
swap amountIn of tokenIn for at least a floor of tokenOut, the floor decaying from a
start price to a reservation price over a time window, with the output delivered to a named
recipient. An open, permissionless set of solvers competes to fill it.
The live protocol has no owner, no admin key, no allowlist, no pause switch, and no
oracle. Safety is achieved by construction: every fill either delivers the signed floor to
recipient or reverts atomically. One immutable IntentSettlerV1 contract per chain
acts as the Permit2 spender; funds are pulled through Permit2 only at fill time; solvers
source liquidity inside a flash callback, so filling requires no principal from the settler.
Authorization is a Permit2 witness signature over a 12-field IntentOrder. Settlement
is same-chain only. A companion read contract, IntentLens, reports screening facts; an
ERC-7683 resolver, IntentSettlerResolver, translates orders for cross-protocol discovery.
Neither holds funds or changes settlement rules.
1. Motivation
Section titled “1. Motivation”Two converging trends motivate the design.
Intents over transactions. Users increasingly express outcomes (“I want at least X of token B for my token A”) rather than execution paths (pool hops, router calldata). Signature-based intent models let solvers compete on execution while the user signs a single authorization.
Minimal on-chain trust. Existing intent systems often retain at least one of: an owner-controlled filler or router allowlist, an upgradable reactor, an off-chain order-flow auction with privileged participants, or a trusted quoting oracle. Each is an admin surface — a censorship point and a liability for automated traders that cannot judge counterparty honesty.
AgentSwap Intents removes all of them. The design question the protocol answers is:
What is the minimal on-chain mechanism such that an arbitrary, unvetted counterparty can execute a user’s order and the recipient provably cannot receive less than the user signed for?
The answer — a hard floor enforced at settlement, combined with revert-or-deliver atomicity — needs no allowlist, no owner, and no oracle. Discovery and routing are off-chain concerns; the contract enforces only what the signature authorizes.
1.1 Why IntentSettlerV1 replaced clone-v1
Section titled “1.1 Why IntentSettlerV1 replaced clone-v1”The retired clone-v1 stack deployed a per-intent minimal proxy via IntentFactory. Each
intent had its own contract address, a 9-field order (no recipient, no decayEndTime,
no appData), and the clone as Permit2 spender. That model remains deployed and fillable until
existing orders expire, but new orders use IntentSettlerV1.
IntentSettlerV1 consolidates settlement into one constant address per chain. The order
gained recipient (output need not return to the signer), decayEndTime (the floor rests
after decay instead of existing for a single instant at expiry), and appData (opaque
attribution bound by the signature). The witness type, order encoding, and Permit2 spender all
changed. A clone-v1 signature cannot verify against IntentSettlerV1 and vice versa; this is
intended, not a compatibility break to patch.
2. Design principles
Section titled “2. Design principles”-
Privilege-less by construction. The settler has no owner, no upgrade path, no pause switch, and no allowlist. The only privileged action on an order belongs to its
owner: cancellation. Removing protocol-level power is strictly stronger than guarding it. -
The signed floor is the sole safety root. There is no price oracle. The user’s signature is the price policy; the contract’s only job is to enforce it atomically.
-
Revert-or-deliver. No partial state exists. Either the
recipient’s balance increases by at least the floor, or the transaction reverts and the owner’s funds never moved. -
Zero custody, zero standing approvals to the protocol. Funds are pulled through Permit2 with a witness signature at fill time. An unfilled intent expires with its signature; nothing needs to be withdrawn because nothing was ever deposited into the settler. The owner must hold a live
tokenInallowance to Permit2 coveringamountInat fill time, however it was granted — that allowance gates whether a fill can pull funds. -
The event log is the discovery feed.
announceemits the complete order and signature for indexers. It is optional, builds no on-chain index, and does not make the feed trustworthy on its own. Solvers must dry-runfillbefore spending gas. -
Immutability is a requirement, not a preference. Permit2 verifies the signer but lets the signed
spenderchooseSignatureTransferDetails.toat execution time. An upgradeable settler could redirect funds that outstanding signatures already authorized, against different bytecode. New semantics ship asIntentSettlerV2at a new address.
3. Protocol overview
Section titled “3. Protocol overview”3.1 Actors
Section titled “3.1 Actors”| Actor | Role | Trust required |
|---|---|---|
| Owner | Signs the Permit2 witness; the only account whose tokenIn moves |
None — self-custody throughout |
| Recipient | Receives the floor of tokenOut; may differ from owner |
None — named in the signed order |
| Solver / filler | Anyone; watches for orders and fills profitable ones | Zero — permissionless |
| Relayer (optional) | Broadcasts announce so the owner pays no gas |
Zero — cannot alter a signed order |
| Protocol admin | — | Does not exist |
3.2 Lifecycle
Section titled “3.2 Lifecycle”sign ──► announce (optional) ──► open ──► fill ──► settled │ │ └── relay may broadcast └── floor → recipient surplus stays with solver (by never sending it)| Step | On-chain function | Who |
|---|---|---|
| Publish | announce(order, permit2Sig) |
Anyone — owner self-submits, or a relay broadcasts |
| Settle | fill(order, permit2Sig, solver, routerData) |
Anyone |
| Cancel (own gas) | cancel(order) |
owner only |
| Cancel (gasless) | cancelSigned(order, deadline, ownerSig) |
Anyone broadcasts the owner’s signature |
| Cancel (backstop) | Permit2.invalidateUnorderedNonces(…) |
owner, directly on Permit2 |
-
Sign. The owner signs one Permit2 witness signature binding the order to a one-time Permit2 unordered nonce. One wallet interaction; no funds move.
-
Announce (optional). Anyone calls
announce, which verifies the signature against the digest Permit2 itself will check and emitsIntentAnnouncedwith the full encoded order and signature. This is the discovery feed, not a settlement step. -
Open. Solvers observe announcements (or obtain orders elsewhere) and price the order as the floor decays.
-
Fill. A solver for whom the order clears calls
fill. In one transaction: the owner’stokenInis pulled via Permit2 to the settler, flash-transferred to the solver, the solver sourcestokenOutin a callback, the settler verifies the floor, and delivers everything received torecipient. -
Settled — or the transaction reverted and nothing happened.
Cancellation does not front-run a fill already in the mempool. Whichever valid transaction lands first wins.
4. The order
Section titled “4. The order”struct IntentOrder { address owner; // the Permit2 signer; the only account whose funds move address recipient; // where output is delivered — not necessarily the owner address tokenIn; uint256 amountIn; address tokenOut; uint256 startAmountOut; // the curve's value at startTime uint256 endAmountOut; // the hard floor uint256 startTime; // decay begins uint256 decayEndTime; // decay reaches endAmountOut here; floor rests until endTime uint256 endTime; // the order dies; also the Permit2 deadline bytes32 appData; // opaque attribution tag, bound by signature, ignored by settlement uint256 nonce; // Permit2 unordered nonce}The struct is frozen at deploy. IntentSettlerV1 is immutable and Permit2 binds this struct
into every signature, so no field can be added, removed, or reordered. New semantics ship as
IntentSettlerV2 at a new address.
| # | Field | Meaning |
|---|---|---|
| 1 | owner |
Permit2 signer; sole source of tokenIn |
| 2 | recipient |
Output delivery target |
| 3 | tokenIn |
Input ERC-20 |
| 4 | amountIn |
Input amount |
| 5 | tokenOut |
Output ERC-20 |
| 6 | startAmountOut |
Curve value at startTime |
| 7 | endAmountOut |
Hard floor |
| 8 | startTime |
Decay begins |
| 9 | decayEndTime |
Decay completes; floor rests until endTime |
| 10 | endTime |
Order expiry; Permit2 deadline |
| 11 | appData |
Opaque attribution; ignored by settlement |
| 12 | nonce |
Permit2 unordered nonce |
startAmountOut == endAmountOutencodes a fixed-price limit order.startAmountOut > endAmountOutencodes a descending Dutch auction.
Order id = keccak256(abi.encode(order)), returned by orderHash. Clients read it from the
chain via eth_call.
The Permit2 witness binds 11 fields; nonce is omitted because Permit2 signs it itself. The
(order, permit2Sig) pair is therefore self-describing.
4.1 The price curve
Section titled “4.1 The price curve” startAmountOut ╲ ╲________ ╲______________________ endAmountOut ├──── decay ────┤──────── floor rests ────────┤✗ expires startTime decayEndTime endTimeAt time t, requiredOut is:
t <= startTime -> startAmountOutt >= decayEndTime -> endAmountOut (rests until endTime)otherwise -> startAmountOut - mulDiv(startAmountOut - endAmountOut, t - startTime, decayEndTime - startTime)Two segments: linear decay, then flat. The resting stretch is why decayEndTime is separate
from endTime. With a single deadline field, the floor existed for one instant at expiry; a
short decay window would leave no time to fill at the floor. requiredOut is defined past
endTime; callers gate expiry themselves.
Fills are accepted on the closed window [startTime, endTime].
4.2 Order invariants
Section titled “4.2 Order invariants”Enforced by IntentLib.validate at announce, fill, and resolve alike:
| Check | Revert |
|---|---|
owner != 0, recipient != 0 |
InvalidOrder |
startTime < decayEndTime <= endTime |
InvalidOrder |
startAmountOut >= endAmountOut, both non-zero |
InvalidOrder |
amountIn != 0 |
InvalidOrder |
neither token is 0x0 or 0xEeee…EEeE (native sentinel) |
NativeNotSupportedInOrder |
tokenIn != tokenOut |
SameToken |
startTime < decayEndTime is strict so the divisor cannot be zero. decayEndTime == endTime
is allowed and reproduces single-deadline behaviour. ERC-20 only; no native ETH on either
leg.
5. Architecture
Section titled “5. Architecture”5.1 IntentSettlerV1 — one contract per chain
Section titled “5.1 IntentSettlerV1 — one contract per chain”Each chain runs one immutable IntentSettlerV1 at a deterministic CREATE2 address. The
contract exports:
| Function | Purpose |
|---|---|
announce(order, permit2Sig) |
Discovery feed; verifies signature |
fill(order, permit2Sig, solver, routerData) |
Permissionless settlement |
cancel(order) |
Owner-paid cancellation |
cancelSigned(order, deadline, ownerSig) |
Gasless cancellation |
orderHash(order) |
Order id |
permitDigest(order) |
Exact digest Permit2 will verify |
cancelled(id) |
Cancellation flag lookup |
The only persistent state is mapping(bytes32 => bool) cancelled. Permit2’s unordered nonce
already makes a second fill impossible, so replay needs no storage; only gasless cancellation
does.
Reentrancy is blocked by an EIP-1153 transient guard, which requires transient storage support on each deployment chain.
5.2 Signature binding: the Permit2 witness
Section titled “5.2 Signature binding: the Permit2 witness”Funds move via canonical Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3)
permitWitnessTransferFrom, only at fill time.
| Parameter | Value |
|---|---|
| Primary type | PermitWitnessTransferFrom |
| Domain | Permit2’s own (name: "Permit2", verifyingContract = Permit2 singleton) |
| Witness type | IntentWitness |
spender |
IntentSettlerV1 address |
deadline |
order.endTime |
permitted |
TokenPermissions(tokenIn, amountIn) |
Witness type string (byte-exact):
IntentWitness witness)IntentWitness(address owner,address recipient,address tokenIn,uint256 amountIn,address tokenOut,uint256 startAmountOut,uint256 endAmountOut,uint256 startTime,uint256 decayEndTime,uint256 endTime,bytes32 appData)TokenPermissions(address token,uint256 amount)The settler reads PERMIT2.DOMAIN_SEPARATOR() rather than mirroring it, so the two cannot
drift. permitDigest(order) returns the exact digest Permit2 will verify; clients sign it;
relays check it before spending gas.
Prerequisite: tokenIn.approve(Permit2, …). IntentLens reports ownerPermit2Allowance for
exactly this reason.
Cancellation uses a separate EIP-712 domain on the settler itself:
EIP712("AgentSwap IntentSettler", "1"), with type CancelIntent(bytes32 orderHash,uint256 deadline).
Because every economic field is inside the signed data, neither a relayer nor a solver can execute the owner’s permission under altered terms. Nonces are Permit2 unordered nonces (a per-owner bitmap), so any number of intents can be open concurrently and cancelled independently.
5.3 The flash-callback fill
Section titled “5.3 The flash-callback fill”fill(order, permit2Sig, solver, routerData) is permissionless and executes under the
transient reentrancy guard:
1. validate IntentLib.validate(o); reject if cancelled, before startTime, past endTime, or solver == 0 2. price required = IntentLib.requiredOut(o, block.timestamp) 3. pull Permit2.permitWitnessTransferFrom: owner's amountIn → settler (transferDetails.to hardcoded to address(this); Permit2 lets the spender choose the recipient, so that choice must never be caller-influenced). Consumes the nonce. 4. exact-in pull must land exactly amountIn, or InexactInputReceived 5. flash transfer amountIn of tokenIn → solver 6. callback IIntentFiller(solver).fillIntentCallback(o, required, routerData) 7. floor receivedOut by balance delta; < required → SlippageExceeded 8. deliver transfer everything received to o.recipient; delivered < required → InsufficientRecipientDelivery; delivered != receivedOut → InexactOutputDelivered 9. conserve settler must end holding exactly what it started with on both tokens, or SettlerBalanceChangedStep 5 is safe because tokenIn != tokenOut is enforced. The solver needs no principal: the
flashed amountIn funds the callback. The callback is shaped like the equivalent hooks in other
intent systems — OIF’s IInputCallback and UniswapX’s reactorCallback — but it is its own
interface, and a filler implements fillIntentCallback against this signature.
Surplus
Section titled “Surplus”Surplus belongs to the filler by construction, not by a payout rule. The solver keeps its
margin by never sending it: fillIntentCallback is expected to return exactly requiredOut.
Anything that does arrive above the floor is the solver overpaying, and the settler pays it on
to o.recipient — it is never refunded to the solver. IntentFilled.aboveFloor reports that
amount and is normally zero.
Refunding surplus to the solver would be a round trip returning the solver its own money that could never reach the user, and it would add a second post-callback token interaction whose ordering hazard in a contract that cannot be patched. Paying it to the recipient makes the invariant one sentence: everything that came in goes to the party the order exists for.
5.4 Announcement and discovery
Section titled “5.4 Announcement and discovery”announce verifies the signature against the digest Permit2 itself will check. That check is
also the relay’s free pre-gas admission gate. It does not make the feed trustworthy: a valid
signature can still name a spent nonce, an unapproved token, or a broken router, and an
ERC-1271 owner’s signature is a snapshot a later wallet-config change can invalidate.
Fillers must dry-run fill.
announce builds no index and is optional. It emits:
event IntentAnnounced( bytes32 indexed id, address indexed owner, bytes32 indexed appData, bytes order, // abi.encode(IntentOrder) — complete and self-describing bytes permit2Sig);The three indexed topics are the three queries not answerable from the payload alone: one order, one user’s history, one integrator’s attribution. Individual fields are deliberately not duplicated in the log.
On fill, IntentFilled records id, owner, solver, recipient, caller, amountIn,
requiredOut, receivedOut, and aboveFloor. On cancel, IntentCancelled records id,
owner, and caller.
5.5 Cancellation
Section titled “5.5 Cancellation”Three layers, in the order a user reaches for them — the most convenient first, the most durable last:
| Method | Mechanism | Who broadcasts |
|---|---|---|
cancel(order) |
Sets cancelled[id] |
owner only (NotOrderOwner) |
cancelSigned(order, deadline, ownerSig) |
Sets cancelled[id] via owner signature |
Anyone |
Permit2.invalidateUnorderedNonces(nonce >> 8, 1 << (nonce & 0xff)) |
Invalidates the nonce in Permit2’s bitmap | owner, on Permit2 directly |
cancelled[id] is the only state the contract keeps beyond what Permit2 enforces.
nonceSpent is not “filled”. Permit2’s bitmap is set by a fill and by the owner
invalidating the nonce. Distinguishing them requires a matching IntentFilled log.
approve(Permit2, 0) is not a cancellation: re-approving reactivates an unspent order.
Nonce invalidation is the durable backstop, but note its granularity: it clears one bit in the
owner’s Permit2 bitmap, so it kills every signed order sharing that owner and nonce. cancel
and cancelSigned are the per-order paths.
5.6 IntentLens — screening, not a verdict
Section titled “5.6 IntentLens — screening, not a verdict”IntentLens is a replaceable read-only contract. It lives outside the settler because the
settler is immutable and a screening surface is exactly the kind of thing whose shape changes.
Replacing the lens needs no change to the settlement core.
preview(order) returns IntentView:
| Field | Meaning |
|---|---|
id |
keccak256(abi.encode(order)) |
cancelled |
Settler cancellation flag |
nonceSpent |
Permit2 nonce bitmap (fill or invalidation) |
inWindow |
startTime <= now <= endTime |
decayComplete |
now >= decayEndTime |
floorNow |
requiredOut at observation time; 0 outside window |
ownerBalance |
Owner’s tokenIn balance |
ownerPermit2Allowance |
Owner’s tokenIn allowance to Permit2 |
observedAt / observedBlock |
Observation stamp |
It deliberately returns no bool fillable. Balance, allowance, nonce state, wall-clock
time, solver inventory, and venue liquidity all move between the call and inclusion, so any
boolean would be a promise it cannot keep. The solver still must eth_call-dry-run fill.
5.7 ERC-7683 resolver
Section titled “5.7 ERC-7683 resolver”IntentSettlerResolver adopts the ERC-7683 resolver surface for discovery and
representation only:
| Adopted | Not adopted |
|---|---|
resolve(bytes) — view-only, returns ResolvedOrder for a single same-chain fill() step |
OIF InputSettler / OutputSettler settlement surface |
encodePayload for building resolver input |
ERC-7930 addresses in the discovery event |
Assumption strings the resolver declares:
swap_solver.intent_settlerswap_solver.payment_recipient_is_step_callerswap_solver.same_chain_onlyswap_solver.intent_required_out
Same-chain only. There is no cross-chain path in V1.
6. Token model
Section titled “6. Token model”Both legs must be balance-stable ERC-20s: no rebasing, no fee-on-transfer, no transfer hooks. Settlement accounts by balance delta, and a delta cannot tell “the solver paid me” from “a balance I already held grew”.
6.1 What is enforced
Section titled “6.1 What is enforced”| Leg measured | Check |
|---|---|
owner → settler |
Pull must arrive exactly amountIn (InexactInputReceived) |
settler → recipient |
Credit must equal what was sent and must clear the floor |
| Settler balances | Both tokens end exactly where they started (SettlerBalanceChanged) |
Within that model, these hold no matter how a solver behaves:
- The owner is never debited more than
amountIn. - The recipient is never credited less than the floor.
- No stray donation can subsidise a fill.
6.2 What is not enforced
Section titled “6.2 What is not enforced”The contract does not claim that every exotic token fails closed. That statement is too strong.
A tokenOut taxed only on the solver → settler hop is invisible here by construction: the
settler sees what it received, never what the counterparty sent, and settles safely. A tokenIn
taxed on settler → solver shortchanges the solver. Neither is checked: the first cannot
be, and the second would spend gas in an immutable contract to protect a sophisticated party
that picks its own tokens and can measure them itself.
Donations sent directly to the settler are permanently trapped. There is deliberately no sweep, because a recovery path would be authority over shared balances. Conservation is what keeps a donation from becoming a subsidy.
7. Trust model
Section titled “7. Trust model”No owner, no admin, no pause, no upgrade path, no allowlist, no oracle, no escrow, no sweep, no custody. Safety is structural: a fill either delivers the signed floor atomically or reverts.
Immutability is a requirement, not a preference: Permit2 verifies the signer but lets the
signed spender choose SignatureTransferDetails.to at execution time, so an upgradeable
settler could redirect funds that outstanding signatures already authorized, against different
bytecode.
External trust anchors:
| Anchor | Role |
|---|---|
| Canonical Permit2 | Authorization and nonce consumption |
| ERC-20 behaviour within the enforced token model | Balance deltas are meaningful |
| EVM atomicity and EIP-1153 support | Transient reentrancy guard |
| User’s signed curve, floor, deadline, and token addresses | Economic terms |
Solvers, fillers, routers, relayers, frontends, and indexers are trusted with nothing.
Accepted residuals:
- An order may simply go unfilled.
- Announcement logs may carry signatures that later became invalid.
- Donations are permanently trapped.
- Unsupported token behaviour can make an order unfillable.
- A user can sign an economically poor floor — the protocol enforces intent, not market quality.
8. Limitations and non-goals
Section titled “8. Limitations and non-goals”Stated plainly:
| Limitation | Detail |
|---|---|
| ERC-20 → ERC-20 only | Native ETH is rejected on either leg; wrap first |
| Same-chain only | No cross-chain settlement path in V1 |
| No partial fills | An intent fills atomically in full or not at all |
| No oracle | The signed floor is the only economic protection |
| Cancellation is not fill-atomic | Whichever valid transaction lands first wins |
announce does not guarantee fillability |
Dry-run fill before spending gas |
nonceSpent ≠ filled |
Check IntentFilled logs to distinguish fill from nonce invalidation |
approve(Permit2, 0) is not cancellation |
Re-approving reactivates an unspent order |
| Trapped donations | No sweep on the settler |
| Exotic token behaviour | May make an order unfillable; not all cases fail closed |
| Clone-v1 incompatibility | Retired stack; different order, witness, and spender |
9. Deployments
Section titled “9. Deployments”Contracts are deployed at identical CREATE2 addresses on four chains. Byte sizes match across chains, consistent with deterministic CREATE2 deployment.
| Contract | Address | Size |
|---|---|---|
IntentSettlerV1 |
0x7eE5E32d16a90BD4ab3d048B3EE04A7D2d174c4E |
10.5 KB |
IntentLens |
0xDa3407688b96AeC7e31Fc937978F951689C401D6 |
3.1 KB |
IntentSettlerResolver |
0xf0E4B200F6214aBb2BAC34e054aF072ec1E5CaEa |
8.0 KB |
| Permit2 (Uniswap, canonical) | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
8.9 KB |
Chains: Base (8453), Arbitrum One (42161), BNB Smart Chain (56), Robinhood Chain (4663).
Clients pin the address, not the runtime codehash. A CREATE2 address match proves the initcode;
the settler’s runtime codehash differs per chain because OpenZeppelin’s EIP712 caches the
chain id.
Appendix A — Integrator notes
Section titled “Appendix A — Integrator notes”Facts an integration must get exactly right:
-
Order hash.
orderHash = keccak256(abi.encode(order))over all 12 fields. Either compute it locally or read it from the chain viaorderHash(order)— AgentSwap’s own client does the latter and ships no keccak. -
Permit2 witness. Rebuild typed data from the witness type string in section 5.2. A clone-v1 witness (9 fields, clone as spender) cannot verify here.
-
Spender.
spender=IntentSettlerV1address, not a per-order clone. -
Solver callback. Implement
fillIntentCallback(IntentOrder calldata order, uint256 requiredOut, bytes calldata data). Return exactlyrequiredOutto the settler; surplus above the floor that reaches the settler goes torecipient, not back to the solver. -
Recipient. Output goes to
order.recipient, not necessarilyorder.owner. -
Three timestamps.
startTime,decayEndTime, andendTimeare all required. The floor rests betweendecayEndTimeandendTime. -
Dry-run discipline. Call
permitDigestto verify signing,IntentLens.previewfor facts, andeth_callonfillbefore submitting.announceverifying a signature does not prove the order is fillable. -
Nonce hygiene. Intent nonces share the owner’s Permit2 unordered-nonce space with any other Permit2 use.
-
Cancellation backstop.
Permit2.invalidateUnorderedNonces(nonce >> 8, 1 << (nonce & 0xff))from the owner’s wallet is the durable revocation path. -
ERC-7683. Use
IntentSettlerResolver.resolvefor cross-protocol discovery; settlement still goes throughIntentSettlerV1.fill.