The trading terminal
One entry point, one approval, every venue: how buy() and sell() execute, price, and account for a trade.
Every trade on hood.dev goes through one contract, V4TradeManager. It takes the platform fee, hands the swap to a venue adapter, checks what actually came back, and pays you. Venues live behind an append-only adapter registry, so the manager address — and the approval you gave it — never changes when a new DEX is added.
#buy and sell
function buy(
address token,
Route calldata route,
uint256 minAmountOut,
uint256 deadline,
address ref, // referrer, or address(0)
bytes calldata refData // unused today; forward-compat
) external payable returns (uint256 tokenOut);
function sell(
address token,
uint256 amountIn,
Route calldata route,
uint256 minEthOut, // checked against what YOU receive, post-fee
uint256 deadline,
address ref,
bytes calldata refData
) external returns (uint256 ethToUser);Buys are ETH-in: send msg.value, the fee comes off the top, the remainder is routed, and tokens are delivered directly to you — they are never staged in the manager. Sells are the mirror image: tokens are pulled from you, swapped, and the ETH minus the fee is sent to your address in the same transaction.
#Approvals
Approve V4TradeManager for the token you want to sell. Buys need no approval at all — they are payable.
If you are trading through a hood.dev wallet, the approval and the trade are both signed inside the enclave without prompting you, by a key whose policy allowlist includes this manager and excludes arbitrary destinations — so one-click trading costs you no custody. That mechanism is on Wallets & keys.
#How the manager accounts for a trade
Adapter return values are informational. The manager measures its own balances around every adapter call and uses the deltas, which is what makes the accounting hold across venues that behave differently:
- Sell proceeds are what actually arrived. The ETH credited to you is the manager’s balance delta, not a number the adapter claimed.
- Unspent buy ETH is refunded, whoever produced it. A bonding-curve venue that crosses its threshold mid-fill hands ETH back; the manager forwards it to you and emits
EthRefunded. You never have to ask for it. - Fee-on-transfer tokens cannot desync it. Sells measure the token balance before and after the pull, and the adapter trades whatever survived the hop.
- The manager ends every trade with zero balances. It is not a custodian and holds nothing between transactions.
#Slippage and deadlines
| Parameter | Checked how |
|---|---|
| minAmountOut (buy) | The manager re-checks the tokens delivered against your floor and reverts SlippageExceeded — independently of any venue-level check. |
| minEthOut (sell) | Checked against the post-fee ETH you receive, not the pre-fee amount. What you set is what you get, or the trade reverts. |
| deadline | Unix seconds. Past it, the call reverts Expired before anything moves. |
#Quoting
There is no separate quoter contract, and deliberately so — a quoter is a second implementation that can drift from the executor. Instead, simulate the real trade with eth_call and a zero floor. A buy simulation returns the tokens that would be delivered; a sell simulation returns the post-fee ETH that would be sent. What you simulate is the code that will run.
const tokenOut = await publicClient.simulateContract({
address: TRADE_MANAGER,
abi: tradeManagerAbi,
functionName: 'buy',
args: [token, route, 0n, deadline, zeroAddress, '0x'],
value: ethIn,
account: trader,
}).then((r) => r.result)#Events
event TradeExecuted(
address indexed user,
address indexed token,
bool indexed isBuy,
uint8 dex, // the dexId that executed it
uint256 ethAmount, // gross ETH side: buys = msg.value, sells = ETH out pre-fee
uint256 tokenAmount,
uint256 fee, // TOTAL platform fee (protocol + referral)
address ref, // address(0) if none
uint256 refFee // the referral portion of the fee above
);
event EthRefunded(address indexed user, uint256 amount);These two events are all trade history, per-wallet PnL and the leaderboard are built from (see Data & indexing). Because ethAmount is the gross side and fee is broken out, net cost is derivable without reading receipts.
#Common revert reasons
| Error | Means |
|---|---|
| Expired | The deadline passed before inclusion. |
| SlippageExceeded | Output fell below your floor — retry with a fresh quote or a wider tolerance. |
| NoSwapOutput | The sell produced zero ETH. Usually an unroutable or dead pool. |
| UnknownDex(dexId) | No adapter is registered under that dexId. Your route is malformed or stale. |
| DexPaused(dexId) | That venue is temporarily disabled. Route elsewhere. |
| PartialFill() | A V4 hop could not consume its full input — the size exceeds usable liquidity. |
| BrokenChain() | A multi-hop route does not connect end to end. Rebuild the path. |
Every contract in the system uses custom errors, so decode revert data against the ABI rather than string-matching. Amounts everywhere are raw base units.
#Fees and hooks
The platform fee is 1% of the ETH side with a 2% hard cap — full breakdown on Fees. It is pushed as native ETH to the manager’s fee receiver with the trader’s address attached, which is what makes cash back possible without touching the trading path: the live receiver credits 25% of each fee back to the wallet that paid it and banks the rest. Any referral share is pushed to the ReferralManager in the same call, before the receiver sees the remainder.
The receiver is swappable by the manager’s owner (setFeeReceiver) and is the one moving part in the fee path — the manager binding inside each receiver is immutable, and no receiver has ever been able to reach a trader’s funds, only the fee it was handed.
The manager also has an optional tradeTracker hook for future reward accounting. It is unset today, and when set its reverts are swallowed — a broken tracker can never break trading.
