AgentSwap Intents Protocol
Ownerless same-chain intent settlement via IntentSettlerV3 and UserProxyV6
The protocol is deployed on Base 8453, Arbitrum One 42161, BNB Smart Chain 56, and Robinhood Chain 4663.
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 protocol has no settler owner, no admin key, no pause switch, and no oracle. The
system filler gate and fee vault are explicit operational contracts with their own owner-controlled
allowlists and fee sweep. Safety is achieved by construction: every fill either delivers the
applicable floor to recipient, sends the 3 bps fee to FeeVault, or reverts atomically. One
immutable IntentSettlerV3 contract per chain works with each owner’s UserProxyV6; the
proxy pulls input to the settler; solvers source liquidity inside a flash callback, so filling
requires no principal from the settler.
Authorization is a flat Order signature over a 12-field IntentOrder on the settler’s
AgentSwap IntentSettler version 3 domain. Settlement is same-chain only. A companion read
contract, IntentLensV3, reports screening facts including gross fill terms.
1. Design principles
Section titled “1. Design principles”-
Privilege-less settlement by construction. The settler has no owner, upgrade path, pause switch, or allowlist.
SystemFillerGateis the explicit caller gate for the floor-rate system filler, andFeeVaultis the explicit fee custody boundary. The only privileged action on an order belongs to itsowner: cancellation. -
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 and the fee reachesFeeVault, or the transaction reverts and the owner’s funds never moved. -
Zero custody, owner-scoped approval. Funds are pulled through the owner’s proxy at fill time. An unfilled intent expires with its signature; nothing needs to be withdrawn because nothing was deposited into the settler. Every V6 user needs a new proxy and a fresh
tokenInallowance to that proxy coveringamountInat fill time — 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. The owner’s proxy is bound to the V6 factory-named settler and sends intent input only to that caller. An upgradeable authorization boundary could redirect funds that outstanding signatures already authorized.
2. Protocol overview
Section titled “2. Protocol overview”2.1 Actors
Section titled “2.1 Actors”| Actor | Role | Trust required |
|---|---|---|
| Owner | Owns the proxy and signs the flat Order; 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 |
2.2 Lifecycle
Section titled “2.2 Lifecycle”sign ──► announce (optional) ──► open ──► fill ──► settled │ │ └── relay may broadcast └── floor → recipient; fee → FeeVault surplus stays with solver (by never sending it)| Step | On-chain function | Who |
|---|---|---|
| Publish | announce(order, auth) |
Anyone — owner self-submits, or a relay broadcasts |
| Settle | fill(order, auth, solver, routerData) |
Anyone |
| Cancel (own gas) | cancel(order) |
owner only |
| Cancel (gasless) | cancelSigned(order, deadline, ownerSig) |
Anyone broadcasts the owner’s signature |
| Cancel (backstop) | UserProxyV6.invalidateOwnerNonces(…) |
owner, directly on their own proxy |
-
Sign. The owner signs one flat
Ordersignature binding all 12 fields to the V3 settler domain. One wallet interaction; no funds move. -
Announce (optional). Anyone calls
announce, which asks the owner’s proxy to verify theauthenvelope and emitsIntentAnnouncedwith the full encoded order and authorization. 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. At or before
startTime, onlySystemFillerGatefills at the floor; outsiders may fill at floor + 25 bps. AfterstartTime, everyone fills at the floor. The filler also pays the 3 bps fee on top in every case. -
Fill. A solver for whom the order clears calls
fill. In one transaction: the owner’stokenInis pulled through the owner’s proxy to the settler, flash-transferred to the solver, the solver sourcestokenOutin a callback, the settler verifies the gross requirement, sends the fee toFeeVault, and delivers the remainder 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.
3. The order
Section titled “3. The order”struct IntentOrder { address owner; // the proxy owner and flat Order signer 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 bytes32 appData; // opaque attribution tag, bound by signature, ignored by settlement uint256 nonce; // owner's proxy nonce}The struct is frozen at deployment. IntentSettlerV3 is immutable and the flat Order signature
binds all 12 fields, so no field can be added, removed, or reordered.
| # | Field | Meaning |
|---|---|---|
| 1 | owner |
Proxy owner; 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 |
| 11 | appData |
Opaque attribution; ignored by settlement |
| 12 | nonce |
Owner proxy 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 flat Order signature binds every field, including nonce. The auth envelope carries that
owner signature to the proxy.
3.1 The price curve
Section titled “3.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.
V6 fills are accepted through endTime, including before startTime. At or before startTime, the
system filler has the floor-rate path and outsiders must pay the 25 bps override.
3.2 Order invariants
Section titled “3.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.
4. Architecture
Section titled “4. Architecture”4.1 IntentSettlerV3 — one contract per chain
Section titled “4.1 IntentSettlerV3 — one contract per chain”Each chain runs one immutable IntentSettlerV3 at its deployed address. The
contract exports:
| Function | Purpose |
|---|---|
announce(order, auth) |
Discovery feed; asks the owner’s proxy to verify authorization |
fill(order, auth, solver, routerData) |
Permissionless settlement through the owner’s proxy |
cancel(order) |
Owner-paid cancellation |
cancelSigned(order, deadline, ownerSig) |
Gasless cancellation |
orderHash(order) |
Order id |
orderDigest(order) |
Exact flat-order digest |
cancelled(id) |
Cancellation flag lookup |
filled(id) |
Single-fill marker |
The persistent order state is cancelled[id] and filled[id]. The owner’s proxy also consumes
the order nonce, so replay protection is held by both systems.
Reentrancy is blocked by an EIP-1153 transient guard, which requires transient storage support on each deployment chain.
4.2 Signature binding: the flat Order
Section titled “4.2 Signature binding: the flat Order”Funds move through the owner’s UserProxyV6, only at fill time. Every user needs a new V6 proxy
and a fresh approval for each input token. The owner approves that proxy.
| Parameter | Value |
|---|---|
| Primary type | Order |
| Domain | EIP712("AgentSwap IntentSettler", "3") |
| Verifying contract | IntentSettlerV3 address |
| Fields | All 12 fields of IntentOrder, including nonce |
The flat type string is:
Order(address owner,address recipient,address tokenIn,uint256 amountIn,address tokenOut,uint256 startAmountOut,uint256 endAmountOut,uint256 startTime,uint256 decayEndTime,uint256 endTime,bytes32 appData,uint256 nonce)orderDigest(order) returns the exact digest; the owner’s proxy recomputes it before pulling.
The auth envelope is abi.encode(AuthKind, bytes payload). For an owner authorization,
AuthKind.Owner = 0 and payload = abi.encode(bytes ownerSignature).
Prerequisite: tokenIn.approve(ownerProxy, …). IntentLensV3 reports ownerProxyAllowance.
Cancellation uses a separate EIP-712 domain on the settler itself:
EIP712("AgentSwap IntentSettler", "3"), 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. The proxy nonce and V6’s filled[id] marker
block replay.
4.3 The flash-callback fill
Section titled “4.3 The flash-callback fill”fill(order, auth, solver, routerData) is permissionless and executes under the
transient reentrancy guard:
1. validate IntentLib.validate(o); reject if cancelled, past endTime, or solver == 0 2. price floor = IntentLib.requiredOut(o, block.timestamp); apply the outsider override inside the exclusive window; fee = 3 bps of that floor; requiredOut = floor + fee 3. pull owner's UserProxyV6 → settler: the proxy verifies auth, pulls amountIn from the owner to the settler, and consumes the owner's proxy nonce. 4. exact-in pull must land exactly amountIn, or InexactInputReceived 5. flash transfer amountIn of tokenIn → solver 6. callback IIntentFiller(solver).fillIntentCallback(o, requiredOut, routerData) 7. gross receivedOut by balance delta; < requiredOut → SlippageExceeded 8. deliver send the fee to FeeVault and the rest to o.recipient; recipient floor or fee delivery shortfall → revert 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 the gross
requiredOut. The 3 bps fee is paid by the filler on top of the applicable floor and sent to
FeeVault; it is never taken from the owner’s proceeds. Anything that does arrive above the gross
requirement 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.
4.4 Announcement and discovery
Section titled “4.4 Announcement and discovery”announce asks the owner’s proxy to verify the authorization. That check is also the relay’s free
pre-gas admission gate. It does not make the feed trustworthy: a valid authorization can still
become unusable because of nonce state, allowance, wallet configuration, or route conditions, 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 auth);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,
gross requiredOut, fee, receivedOut, and aboveFloor. On cancel, IntentCancelled records id,
owner, and caller.
4.5 Cancellation
Section titled “4.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 |
UserProxyV6.invalidateOwnerNonces(nonce >> 8, 1 << (nonce & 0xff)) |
Invalidates nonce bits on the owner’s proxy | owner, on their own proxy directly |
cancelled[id] and filled[id] are the V6 settler’s persistent order state.
nonceSpent is not “filled”. The owner’s proxy bitmap is set by a fill and by the owner
invalidating the nonce. Use filled[id] or a matching IntentFilled log.
Nonce invalidation is the durable backstop, at nonce granularity, so it kills every owner order
sharing those nonce bits. cancel and cancelSigned are the per-order paths.
4.6 IntentLensV3 — screening, not a verdict
Section titled “4.6 IntentLensV3 — screening, not a verdict”IntentLensV3 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 |
filled |
Settler single-fill marker |
nonceSpent |
Owner proxy nonce bitmap (fill or invalidation) |
killedByOwner |
Owner’s exact-order proxy kill switch |
proxyDeployed |
Whether the owner’s proxy exists |
inWindow |
now <= endTime; V6 fills may occur before startTime |
exclusiveWindow |
now <= startTime; system filler at floor, outsiders at floor + 25 bps |
decayComplete |
now >= decayEndTime |
floorNow |
system-filler recipient floor at observation time; 0 outside window |
feeNow |
3 bps of the applicable floor, paid by the filler on top |
requiredNow |
gross system-filler requirement: floorNow + feeNow |
floorForOutsider |
outsider recipient floor, including the pre-start override |
requiredForOutsider |
gross outsider requirement: floor plus fee |
ownerBalance |
Owner’s tokenIn balance |
ownerProxyAllowance |
Owner’s tokenIn allowance to their proxy |
proxy |
Owner’s deployed proxy, or zero |
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. Token model
Section titled “5. 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”.
5.1 What is enforced
Section titled “5.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.
5.2 What is not enforced
Section titled “5.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.
6. Trust model
Section titled “6. Trust model”No settler owner, no admin, no pause, no upgrade path, no oracle, no escrow, no custody. The
system filler gate and fee vault are explicit owner-controlled operational contracts. Safety is
structural: a fill either delivers the signed floor, sends the 3 bps fee to FeeVault, and
completes atomically, or reverts.
Immutability is a requirement, not a preference: the V6 factory fixes the settler named by each owner proxy, and the proxy sends intent input only to that settler. An upgradeable authorization boundary could redirect funds that outstanding signatures already authorized.
External trust anchors:
| Anchor | Role |
|---|---|
| Owner’s UserProxyV6 | Order authorization, nonce consumption, and the input pull |
SystemFillerGate and FeeVault |
Operator/solver allowlists and protocol-fee custody |
| 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.
7. Limitations and non-goals
Section titled “7. 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 V6 |
| 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 filled[id] or IntentFilled logs to distinguish fill from nonce invalidation |
| Proxy approval is not cancellation | Use invalidateOwnerNonces on the owner’s proxy |
| Trapped donations | No sweep on the settler |
| Exotic token behaviour | May make an order unfillable; not all cases fail closed |
8. Deployments
Section titled “8. Deployments”The V6 contracts use one address per contract across Base (8453), Arbitrum One (42161), BNB Smart Chain (56), and Robinhood Chain (4663).
| Contract | Address | Role |
|---|---|---|
UserProxyFactoryV6 |
0xc1660e4BbC825f8367dA92b60dccc17E4E10bc26 |
Builds the settler, digest helper, implementation, and user proxies |
IntentSettlerV3 |
0x2dd81c4fD1FC38b009Ab10D5C9b1f01Ca51cE462 |
Announces, fills, cancels, and enforces fee and exclusivity terms |
UserProxyV6 implementation |
0x6E4065a3E864Acf36BfaB95cEE31713b48F8dBd6 |
Proxy implementation |
IntentDigestV6 |
0x0098faB79A70c6a6346EB5d5BC81484C107261F0 |
Stateless order-hash helper |
IntentLensV3 |
0x3AFfAafAF3Ec0A8A535723CBf0891482680F30C3 |
Floor, fee, gross, and exclusivity preview |
SystemFillerGate |
0xb0aF82263C512662610B33827DfbC1b9fBc7f3D4 |
System filler caller gate |
FeeVault |
0x2450618aC0B57ae371715eCEdEBbE6780E876271 |
Protocol fee recipient and sweep boundary |
Integrations select these addresses by chain ID and derive each owner’s proxy from the factory.