Source your own order flow
A permissionless solution to any AMM's discoverability problem
DeFi promises to tokenize everything. Smart contract platforms offer flexibility to codify asset-specific trading strategies that execute trustlessly. With liquidity open to anyone, anywhere, markets compete on price alone. The best quote should win the trade.
But it doesn’t play out that way. A new AMM can go on-chain with deep liquidity and competitive quotes, and still never trade, because none of it matters without order flow.
Traders move through established channels: aggregators and routers your AMM isn’t wired into. They usually stick with the venues they know, even when they get a worse quote.
In 2024 we built a bespoke AMM, the Bill Broker, with pricing logic that traded SPOT (the flatcoin) around its fair market value. Bootstrapping volume proved hard. There are a few options to try, but no silver bullet:
Aggregators don’t let you list yourself. Their engineers have to write the connector, and the group that owns the source list (Matcha’s, for instance, runs through the 0x API) prioritizes venues that already carry volume. An unfamiliar AMM with no track record sits in a backlog they may never get to.
Searchers extract their pound of flesh. Their bot keeps your price aligned with the market, taking real risk to do it: unaudited code and a winner-take-all race. They’re paid from the arbitrage spread, your value leaking to a mercenary. And their incentive runs against yours: the better your quote, the wider the spread, the more they take.
Self-arbitrage needs liquidity you don’t have. Running the bot yourself keeps the spread in-house, but only works when the asset already trades deep elsewhere. Newer assets don’t: the bot has nothing to arbitrage against until you’ve seeded liquidity on established AMMs.
A solution
In our latest iteration we built an integration with CoW Swap using Programmatic Orders and flash loans. The approach can work for any on-chain swap protocol facing the discoverability problem.
How it works
Instead of waiting for order flow, the AMM publishes its own intent to trade into CoW's batch auction for any solver to fill.
Flash loans handle atomic, multi-party settlement between the AMM and counterparties from the CoW network.
1) The AMM. Your pool or trading strategy: it exposes a swap method over a pair, say BTC ↔ USD. It also exposes how much it can trade right now and at what price.
2) The publisher. An on-chain broker contract that reads the AMM’s capacity and publishes a matching order, and an off-chain keeper that submits that order and its execution recipe to CoW’s orderbook.
3) Settlement. The winning solver executes everything in one atomic transaction: the broker flash-borrows USD (from an on-chain pool like Morpho, or one you run yourself), swaps that USD for BTC on the AMM, settles the trade against the CoW batch, and repays the loan from the proceeds.
Benefits
Permissionless. No connector, no listing, no approval. Your order goes straight into CoW’s public orderbook, live the moment you deploy.
Surplus capture. CoW settles in a batch auction where solvers compete to return the most surplus. Because every fill has to clear the AMM’s own price test and the order names your own contract as receiver, a solver wins only by beating that floor, and everything above it flows back to the pool. Incentives stay aligned: the solver takes its nominal batch fee, and the surplus goes to your LPs.
No upfront capital. Someone has to front the USD to buy the BTC the pool is selling. The flash loan supplies it just in time and is repaid from the swap proceeds within the same settlement, so no one (not the AMM, not the solver) has to lock up inventory to be the counterparty. CoW supports this natively via CIP-66.
Conclusion
The approach clears all three dead ends: it’s permissionless, the swap surplus stays with your LPs, and it needs no capital up front.
/// @notice One swap, from the vault's side of the trade: it takes `tokenInAmt`
/// of `tokenIn` and pays out `tokenOutAmt` of `tokenOut`.
struct SwapParams {
// token the vault takes in
IERC20 tokenIn;
// token the vault pays out
IERC20 tokenOut;
// `tokenIn` handed to the vault (raw token units)
uint256 tokenInAmt;
// `tokenOut` it pays out for that (raw token units)
uint256 tokenOutAmt;
}
/// @title ISwapVault
/// @notice A vault that swaps between two tokens with a whitelisted
/// counterparty: it quotes a size in each direction, judges a price,
/// and settles at the price the caller names.
interface ISwapVault {
/// @notice Returns the two tokens the vault swaps between, ascending by
/// address.
/// @return token0 The lower-addressed of the pair.
/// @return token1 The higher-addressed one.
function swapTokens() external view returns (IERC20 token0, IERC20 token1);
/// @notice Sizes the swap the vault would settle now in each direction — the
/// read-side mirror of `swap`. Sides follow `swapTokens`.
/// @return token0In What the vault takes `token0` in for, paying `token1` out.
/// @return token1In The reverse: `token1` in, `token0` out.
function swapCapacity()
external
view
returns (SwapParams memory token0In, SwapParams memory token1In);
/// @notice Swaps `s.tokenInAmt` of `s.tokenIn` for exactly `s.tokenOutAmt` of
/// `s.tokenOut`.
/// @param s The swap to settle, from the vault's side of the trade.
function swap(SwapParams calldata s) external;
/// @notice Whether the vault will part with `s.tokenOutAmt` of `s.tokenOut`
/// for `s.tokenInAmt` of `s.tokenIn`, judged on rate alone.
/// @param s The swap to price, from the vault's side of the trade.
function isAcceptableSwap(SwapParams calldata s) external view returns (bool);
}
interface IMorpho {
function flashLoan(
address token,
uint256 assets,
bytes calldata data
) external;
}
contract Broker is IConditionalOrder, IERC1271 {
using SafeERC20 for IERC20;
bytes4 internal constant MAGIC_VALUE = IERC1271.isValidSignature.selector;
bytes32 public constant SALT_TOKEN0_IN = keccak256('Broker.token0In');
bytes32 public constant SALT_TOKEN1_IN = keccak256('Broker.token1In');
uint32 public constant VALID_TO_BUFFER_SEC = 30 minutes;
ISwapVault public immutable vault;
IERC20 public immutable token0;
IERC20 public immutable token1;
// … remaining immutables, errors and modifiers elided …
address private transient _loanToken;
uint256 private transient _loanAmount;
constructor(/* … */) {
// … wiring and standing approvals to the vault and CoW's relayer elided …
composableCow_.create(
ConditionalOrderParams({
handler: IConditionalOrder(address(this)),
salt: SALT_TOKEN0_IN,
staticInput: abi.encode(token0_)
}),
true
);
composableCow_.create(
ConditionalOrderParams({
handler: IConditionalOrder(address(this)),
salt: SALT_TOKEN1_IN,
staticInput: abi.encode(token1_)
}),
true
);
}
// -------------------------------------------------------------------------
// Pokes — permissionless
function settleToVault(IERC20 token) public {
uint256 bal = token.balanceOf(address(this));
if (bal > 0) token.safeTransfer(address(vault), bal);
}
// -------------------------------------------------------------------------
// Settlement — CoW borrower
function flashLoanAndCallBack(
address lender_,
IERC20 token,
uint256 amount,
bytes calldata callBackData
) external onlyCowRouter onlyPinnedLender(lender_) whenNoLoanInFlight {
_loanToken = address(token);
_loanAmount = amount;
IMorpho(lender).flashLoan(address(token), amount, callBackData);
settleToVault(token0);
settleToVault(token1);
_loanToken = address(0);
_loanAmount = 0;
}
function onMorphoFlashLoan(
uint256 assets,
bytes calldata data
) external onlyLender {
if (assets > _loanAmount) revert UnexpectedLoanAmount();
ICowFlashLoanRouter(cowRouter).borrowerCallBack(data);
IERC20(_loanToken).forceApprove(lender, _loanAmount);
}
// -------------------------------------------------------------------------
// Settlement — pre-hook
function provideInventory(
IERC20 tokenIn,
IERC20 tokenOut,
uint256 tokenInAmt,
uint256 tokenOutAmt
) external onlyTrampoline whenLoanInFlight {
vault.swap(
SwapParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
tokenInAmt: tokenInAmt,
tokenOutAmt: tokenOutAmt
})
);
}
// -------------------------------------------------------------------------
// ERC-1271 — authorize the fill
function isValidSignature(
bytes32 hash,
bytes calldata signature
) external view returns (bytes4) {
GPv2Order.Data memory order = abi.decode(signature, (GPv2Order.Data));
if (GPv2Order.hash(order, settlement.domainSeparator()) != hash) {
revert InvalidOrder('hash');
}
_checkOrder(order);
return MAGIC_VALUE;
}
// -------------------------------------------------------------------------
// ComposableCoW handler — discovery
function getTradeableOrder(
address /* owner */,
address /* sender */,
bytes32 /* ctx */,
bytes calldata staticInput,
bytes calldata offchainInput
) external view returns (GPv2Order.Data memory order) {
bytes32 appData = offchainInput.length > 0
? abi.decode(offchainInput, (bytes32))
: bytes32(0);
order = _buildOrder(appData, _decodeTokenIn(staticInput));
if (order.sellAmount <= 0 || order.buyAmount <= 0) {
revert InvalidOrder('no capacity');
}
}
function verify(
address /* owner */,
address /* sender */,
bytes32 /* hash */,
bytes32 /* domainSeparator */,
bytes32 /* ctx */,
bytes calldata staticInput,
bytes calldata /* offchainInput */,
GPv2Order.Data calldata order
) external view {
if (order.buyToken != _decodeTokenIn(staticInput)) {
revert InvalidOrder('direction');
}
_checkOrder(order);
}
// -------------------------------------------------------------------------
// Internal
function _checkOrder(GPv2Order.Data memory order) internal view {
if (order.receiver != address(this)) revert InvalidOrder('receiver');
if (order.kind != GPv2Order.KIND_SELL) revert InvalidOrder('kind');
if (order.partiallyFillable) revert InvalidOrder('partial');
if (order.feeAmount > 0) revert InvalidOrder('fee');
if (order.validTo < block.timestamp) revert InvalidOrder('expired');
if (
order.sellTokenBalance != GPv2Order.BALANCE_ERC20 ||
order.buyTokenBalance != GPv2Order.BALANCE_ERC20
) revert InvalidOrder('balance');
// … token-pair check elided …
if (
!vault.isAcceptableSwap(
SwapParams({
tokenIn: order.buyToken,
tokenOut: order.sellToken,
tokenInAmt: order.buyAmount,
tokenOutAmt: order.sellAmount
})
)
) {
revert InvalidOrder('price');
}
}
function _decodeTokenIn(
bytes calldata staticInput
) internal view returns (IERC20 takes) {
if (staticInput.length != 32) revert InvalidOrder('direction');
takes = IERC20(abi.decode(staticInput, (address)));
if (takes != token0 && takes != token1) revert InvalidOrder('direction');
}
function _buildOrder(
bytes32 appData,
IERC20 takes
) internal view returns (GPv2Order.Data memory order) {
(SwapParams memory token0In, SwapParams memory token1In) = vault
.swapCapacity();
SwapParams memory s = takes == token0 ? token0In : token1In;
order = GPv2Order.Data({
sellToken: s.tokenOut,
buyToken: s.tokenIn,
receiver: address(this),
sellAmount: s.tokenOutAmt,
buyAmount: s.tokenInAmt,
validTo: uint32(block.timestamp + VALID_TO_BUFFER_SEC),
appData: appData,
feeAmount: 0,
kind: GPv2Order.KIND_SELL,
partiallyFillable: false,
sellTokenBalance: GPv2Order.BALANCE_ERC20,
buyTokenBalance: GPv2Order.BALANCE_ERC20
});
}
}
const SETTLEMENT = '0x9008D19f58AAbD9eD0D60971565AA8510560ab41';
const RELAYER = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110';
const MORPHO = '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb';
const API = 'https://api.cow.fi/mainnet/api/v1';
// The one contract you deploy for this vault. It is its own CoW Borrower, so it
// is also the order's receiver.
const brokerAddr = '0x…';
const ORDER_TUPLE =
'tuple(address sellToken,address buyToken,address receiver,uint256 sellAmount,uint256 buyAmount,uint32 validTo,bytes32 appData,uint256 feeAmount,bytes32 kind,bool partiallyFillable,bytes32 sellTokenBalance,bytes32 buyTokenBalance)';
// The vault sizes at most one direction, and which one moves with its target, so
// read it live rather than assuming. It pays out what the order sells and takes
// in what the order buys — the leg the loan funds.
const [token0In, token1In] = await vault.swapCapacity();
const capacity = token0In.tokenOutAmt > 0n ? token0In : token1In;
const sellTokenAddr = getAddress(capacity.tokenOut);
const buyTokenAddr = getAddress(capacity.tokenIn);
const fullSell: bigint = capacity.tokenOutAmt;
const fullDraw: bigint = capacity.tokenInAmt;
if (fullSell === 0n) throw new Error('vault advertises no capacity');
// Orders are fill-or-kill — the loan is drawn in full in the pre-hook, so a
// partial fill couldn't repay it. Cap the slice to work a large rebalance as
// several self-contained orders.
let sellAmount = fullSell;
if (args.maxSell) {
const cap = BigInt(args.maxSell);
if (cap < sellAmount) sellAmount = cap;
}
// Mirrors `_buildOrder`: the order buys back the counter-leg the vault takes in,
// pro-rated when the slice is capped, so the fill covers the draw rather than
// only clearing the ERC-1271 gate.
const buyAmount: bigint = (fullDraw * sellAmount) / fullSell;
// The draw the pre-hook hands the vault — the same figure, so the fill repays it.
const loanAmount: bigint = args.loan ? BigInt(args.loan) : buyAmount;
// The pre-hook that sources the inventory, run by the solver inside the batch.
const hooks = {
pre: [
{
target: brokerAddr,
callData: broker.interface.encodeFunctionData('provideInventory', [
buyTokenAddr,
sellTokenAddr,
loanAmount,
sellAmount,
]),
gasLimit: '400000',
},
],
post: [],
};
// The execution recipe: the lender to draw from, the borrower that adapts its
// callback and receives the funds, and the hooks. CIP-66 lets a solver pick
// this up natively.
const appDataDoc = {
version: '1.3.0',
appCode: 'amm-cow-broker',
metadata: {
flashloan: {
liquidityProvider: MORPHO,
protocolAdapter: brokerAddr,
receiver: brokerAddr,
token: buyTokenAddr,
amount: loanAmount.toString(),
},
hooks,
},
};
const appDataStr = JSON.stringify(appDataDoc);
const appDataHash = keccak256(toUtf8Bytes(appDataStr));
const order = {
sellToken: sellTokenAddr,
buyToken: buyTokenAddr,
receiver: brokerAddr,
sellAmount: sellAmount.toString(),
buyAmount: buyAmount.toString(),
validTo,
appData: appDataHash,
feeAmount: '0',
kind: 'sell',
partiallyFillable: false,
sellTokenBalance: 'erc20',
buyTokenBalance: 'erc20',
};
const digest = TypedDataEncoder.hash(DOMAIN, ORDER_TYPE, order);
// ERC-1271: the broker decodes this ABI-encoded order in isValidSignature.
// There's no private key anywhere — the contract IS the signer.
const signature = AbiCoder.defaultAbiCoder().encode(
[ORDER_TUPLE],
[
[
sellTokenAddr,
buyTokenAddr,
brokerAddr,
sellAmount,
buyAmount,
validTo,
appDataHash,
0n,
keccak256(toUtf8Bytes('sell')),
false,
keccak256(toUtf8Bytes('erc20')),
keccak256(toUtf8Bytes('erc20')),
],
]
);
// … preflight: the broker is pinned to the lender named in appData, the buy
// side clears `isAcceptableSwap`, the fill repays the draw, the vault still
// offers this direction and size, the relayer allowance stands, and the
// broker's ERC-1271 accepts this exact order …
// POST /orders registers the appData document itself when `appData` carries the
// full JSON, so a separate PUT /app_data is redundant.
const res = await fetch(`${API}/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...order,
signingScheme: 'eip1271',
signature,
from: brokerAddr,
appData: appDataStr,
appDataHash,
}),
});