Skip to content

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.


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. Privilege-less settlement by construction. The settler has no owner, upgrade path, pause switch, or allowlist. SystemFillerGate is the explicit caller gate for the floor-rate system filler, and FeeVault is the explicit fee custody boundary. The only privileged action on an order belongs to its owner: cancellation.

  2. 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.

  3. Revert-or-deliver. No partial state exists. Either the recipient’s balance increases by at least the floor and the fee reaches FeeVault, or the transaction reverts and the owner’s funds never moved.

  4. 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 tokenIn allowance to that proxy covering amountIn at fill time — that allowance gates whether a fill can pull funds.

  5. The event log is the discovery feed. announce emits 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-run fill before spending gas.

  6. 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.


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
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
  1. Sign. The owner signs one flat Order signature binding all 12 fields to the V3 settler domain. One wallet interaction; no funds move.

  2. Announce (optional). Anyone calls announce, which asks the owner’s proxy to verify the auth envelope and emits IntentAnnounced with the full encoded order and authorization. This is the discovery feed, not a settlement step.

  3. Open. Solvers observe announcements (or obtain orders elsewhere) and price the order as the floor decays. At or before startTime, only SystemFillerGate fills at the floor; outsiders may fill at floor + 25 bps. After startTime, everyone fills at the floor. The filler also pays the 3 bps fee on top in every case.

  4. Fill. A solver for whom the order clears calls fill. In one transaction: the owner’s tokenIn is pulled through the owner’s proxy to the settler, flash-transferred to the solver, the solver sources tokenOut in a callback, the settler verifies the gross requirement, sends the fee to FeeVault, and delivers the remainder to recipient.

  5. 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.


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 == endAmountOut encodes a fixed-price limit order.
  • startAmountOut > endAmountOut encodes 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.

startAmountOut ╲
╲________
╲______________________ endAmountOut
├──── decay ────┤──────── floor rests ────────┤✗ expires
startTime decayEndTime endTime

At time t, requiredOut is:

t <= startTime -> startAmountOut
t >= 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.

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.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.

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.

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 SettlerBalanceChanged

Step 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 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.

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.

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.

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”.

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.

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.


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.

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

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.