For builders
Run a keeper
Montra's fees reach the treasury, the buyback and holders through a few calls that anyone can make, and two of them pay: buying the protocol's fees for HYPE as they come up for sale, and pushing holder rewards to wallets. Montra runs a keeper; outside keepers add redundancy for taking sales, pushing rewards and the upkeep, though every sale still depends on Montra's reporter keys to post its price.
On this page
- Hub sales
- Open to anyone
- Buy the protocol's fees in a currency for HYPE, for about the floor's discount under the reference at best: 1% for most currencies.
- Reward pushes
- 0.25%
- Of each payout pushed to a holder's wallet.
- Upkeep
- Unpaid
- Sweeping, collecting, allocating, distributing: open to anyone, paid nothing, and what keeps the fees flowing.
- Not open
- 2 jobs
- Posting the sales' references, and filling the MONTRA buyback.
The jobs, and what each pays
Job
- Take a hub sale
- Who mayAnyone
- PaysWhat your route returns over the sale's price
- Call
FeeMarket.takeorRouteFiller.fill - Push holder rewards
- Who mayAnyone
- Pays0.25% of each payout
- Call
LaunchToken.pushRewards(address[] holders) - Sweep a pool's fees
- Who mayAnyone
- PaysNothing
- Call
LaunchHook.sweep(bytes32 poolId) - Collect the protocol's share
- Who mayAnyone
- PaysNothing
- Call
LaunchHook.collectProtocol(address currency) - Divide it among the lanes
- Who mayAnyone
- PaysNothing
- Call
ProtocolFeeVault.allocate(address currency) - Deliver the treasury's HYPE
- Who mayAnyone
- PaysNothing
- Call
ProtocolFeeVault.deliver(uint256 lane) - Start a holders' stream
- Who mayAnyone
- PaysNothing
- Call
LaunchToken.distribute() - Divide a split
- Who mayAnyone
- PaysNothing; each recipient collects its own
- Call
FeeSplitter.distribute() - Harvest a seed's fees
- Who mayAnyone
- PaysNothing
- Call
LaunchHook.harvestSeed(bytes32 poolId)
| Job | Who may | Pays | Call |
|---|---|---|---|
| Take a hub sale | Anyone | What your route returns over the sale's price | FeeMarket.take or RouteFiller.fill |
| Push holder rewards | Anyone | 0.25% of each payout | LaunchToken.pushRewards(address[] holders) |
| Sweep a pool's fees | Anyone | Nothing | LaunchHook.sweep(bytes32 poolId) |
| Collect the protocol's share | Anyone | Nothing | LaunchHook.collectProtocol(address currency) |
| Divide it among the lanes | Anyone | Nothing | ProtocolFeeVault.allocate(address currency) |
| Deliver the treasury's HYPE | Anyone | Nothing | ProtocolFeeVault.deliver(uint256 lane) |
| Start a holders' stream | Anyone | Nothing | LaunchToken.distribute() |
| Divide a split | Anyone | Nothing; each recipient collects its own | FeeSplitter.distribute() |
| Harvest a seed's fees | Anyone | Nothing | LaunchHook.harvestSeed(bytes32 poolId) |
The unpaid calls are how fees move along: a trade's fee waits in its pool's hook until someone sweeps it, the protocol's part waits in the hook until someone collects it to the vault, and it is for sale only once someone allocates it to the lanes. A keeper taking hub sales does these first, in the same transaction, because each grows the lot it buys.
Not open to keepers
- Posting a sale's reference.
FeeMarket.postAnchorandpostAnchorsare for reporters the team's multisig approves (NotReporterfor anyone else). A sale whose reference is more than itsmaxAgeold sells nothing, so while no reporter posts, there is nothing to take. - The MONTRA buyback. Its sales name
BuybackFilleras their only taker (NotTakerfor anyone else), and the filler serves reporters only; it buys MONTRA in MONTRA's own pool and burns everything it buys. Once the buyback is on, 70% of the protocol's share ends in these sales, and none of them is a third party's to fill; the hub sales stay open to anyone. - Pausing, retuning or resetting a sale. The multisig's alone; a guardian key can only stop things, by pausing a sale or removing a reporter, and removing a reporter that has posted stops every sale until another posts.
Which sales are open is itself a setting. The multisig can name a sale's only taker (setTaker, as the buyback's do; NotTaker for anyone else) or cap what it releases a day (setDrip; listing a quote may set one for its hub sale). Read takerOf(id) and drip(id) before you count on a sale.
How a hub sale prices
There is one standing sale per quote currency (FeeMarket.hubMarket(currency)), selling what the protocol has earned in it for HYPE. Its price per unit is the reporter's reference (the anchor: what the sale's route returns, net of every pool's and hook's fee on it) times a level. The level starts 3% above the reference, halves every 30 minutes from where the last take left it, and never falls under the floor.
- Starts
- +3%
- Over the reference, when a sale opens or after a spell it could not sell; a take lifts it from wherever it is.
- Reaches the reference
- ≈77 s
- Halving every 30 minutes from the top.
- Reaches the floor
- ≈103 s
- 1% under the reference for most currencies; ≈156 s to the 3% floor of memes and stock tokens.
- Sells on a reference
- ≤ 5 min old
- Older, nothing sells until a reporter posts again.
- A take lifts the price. Taking a share of the lot multiplies the level by one plus the bump times that share (the whole lot: 3% up from wherever it was), never past twice the reference, and it falls again from there. A take that lands ahead of yours makes your
maxPricetoo low:TooExpensive, and you read again. - The lot grows as fees arrive. Whatever the lanes hold in the currency is for sale (
ProtocolFeeVault.sellable), once it has been swept, collected and allocated. A take may be partial. - The reference moves slowly. A post may lower it by at most half every 6 hours on mainnet's terms (the team's multisig may set a sale to fall as fast as half every 20 minutes, never faster) and raise it at most fourfold over an hour; a reading a little under is followed as far as it may fall, and one above its reach, or far under it, halts the sale (
MarketHalted) until one in reach comes. After any spell it could not sell (stale, halted, paused), the level starts again from the top. - Prices are scaled by 1e36 (
PRICE_ONE): HYPE wei per base unit of the currency sold.FeeMarket.price(id)returns the price now, the floor price and whether the reference is fresh.
Where the edge comes from. You buy at the sale's price and sell through your own route: your margin per unit is what your route returns less the price. Because the reference is what the sale's route returns, the margin is about the floor's discount once the level has fallen that far; references are posted about once a minute, so in a rising market it can be more, by up to about the bump.
Busy and quiet currencies. On a busy currency, Montra's own keeper fills a sale as soon as its route returns about 0.2% over the price (today; this may change) and sends that surplus to the protocol, so a third party wins there by having a better route or venue, or by being faster. On a quiet one, the keeper's round sweeps, collects and allocates every currency about every 30 seconds and its fill step runs about every 5 (today; this may change): fees swept into an idle sale already at its floor can be taken by a keeper that watches every block, for up to about the floor's discount (1%, or 3% for memes and stock tokens). That is the incentive for outside keepers, by design.
A sale sits at its floor with no taker only when its reference stands above what the pools pay. A currency with no route the keeper can read gets no reference at all: its sale goes stale, and nothing sells until a reporter posts again.
Taking a hub sale
- Read the sale
id = hubMarket(currency);price(id)for the price, the floor and freshness;available(id)for the lot;market(id)forpaused,halted,anchorAtandmaxAge. Skip one that is not fresh, paused or halted. - Grow the lot
LaunchHook.pendingProtocol(poolId)says what a sweep would move in each pool;sweepthe pools worth it,collectProtocol(currency)on each hook, thenProtocolFeeVault.allocate(currency). Each is open to anyone; batch them with aRouteFiller.fill(Multicall3, at its canonical address0xcA11bde05977b3631167028862bE2a173976CA11), which sends no value. - Price your routeQuote selling the lot along your route back to HYPE (Uniswap's V4Quoter, or an
eth_callof the fill itself). What the sale costs isowe = ceil(amount × price / 1e36). - Take itThrough
RouteFiller.fill(id, maxAmount, maxPrice, route, minRate, surplusTo, deadline): it takes the lot, sells it alongroute(v4 pools on Uniswap's PoolManager, the one Montra's pools live in, the first holding the currency, the last paying HYPE), pays the sale and sends what is left over tosurplusTo, you, in one transaction with no capital of your own. Or with inventory:FeeMarket.take(id, maxAmount, maxPrice, recipient, deadline, "")paying HYPE as the call's value, the currency sent torecipientto sell wherever you like. Calltakedirectly, not through Multicall3: it returns any value unused to its caller, which Multicall3 cannot take, so a batched take fails unless it pays the exact price. - Set the guards
maxPriceat the price you read, so a take ahead of yours makes yours fail rather than pay more;minRatefrom your own quote less your slippage (scaled by 1e36), so a sandwich of your route makes it fail rather than lose; a shortdeadline.
Simulate before you send: a fill reverts on a sale that is stale (StaleAnchor), halted, paused, empty (NothingToSell), dearer than your maxPrice (TooExpensive) or on a route that returns less than your minimum (ShortRoute) or that a pool fills only partly (PartialFill), and a reverted fill still costs its gas.
Pushing holder rewards
A token that pays its holders streams what arrives over an hour; each holder can claim, and anyone can push what a holder has earned to its wallet for a fee. pushRewards(holders) pays each listed wallet its earnings less the pusher's fee, the larger of pushBountyBps of the payout (25, so 0.25%) and pushFloor (zero in the mainnet configuration). A payout under PUSH_GATE (20) floors is left to accrue, so with a zero floor only an empty one is.
- Wallets only. An address with code (other than an EIP-7702 delegation) is passed over; a wallet that cannot take the payment is skipped (
RewardsPushSkipped) and keeps its balance. Nothing in a batch blocks the rest. An address with no code is pushed to as a wallet, a contract not yet deployed included, such as a counterfactual Safe or an escrow that held the token first; once a contract is deployed there it is passed over and claims for itself. - The fee is paid to the caller in the token's quote asset, once per batch (
RewardsPushedfor each holder); if the caller cannot take it, it is credited to claim later (RewardsCredited). - Find the holders from the token's
Transferevents, read what each has earned withearned(holder)orLaunchLens.earnedMany(token, holders), and calldistribute()first when new fees are owed to the token. - Gas has two parts, so a small batch costs far more a wallet. Over 451 real batches it was about 78k a batch, plus about 30k for each wallet pushed before: about 108k for one wallet, 199k for four and 745k for 22, which is 33.9k a wallet. A wallet's first push adds about 20k as a rule and up to about 85k: one new wallet alone is about 128k, and a batch of four with two new wallets took 222,871. The contracts' gas snapshot, 86k a wallet the first time and 33k once settled, is for ten wallets a batch.
- Push a batch when it pays. Set 0.25% of the whole batch's payouts against the gas of the whole batch, the batch's own cost included. A wallet pushed alone pays only if 0.25% of its payout covers about 108k, so gather small payouts into a batch or leave them to accrue.
What can I make?
Honest arithmetic, not a promise: every figure below scales with how much trades on Montra, and competition, Montra's own keeper first, drives margins down. Gas is assumed at 0.1 gwei and HYPE at $40: a hub fill through one pool (about 156k gas) then costs about $0.00062, a sweep in a batch (about 47k) less, so on Elysium gas rarely decides a fill; check the chain's price when you run.
Hub sales. The protocol takes 0.4% of every trade. Fees in HYPE need no sale; fees in every other currency go through its hub sale, before MONTRA and after, and ProtocolFeeVault.sellable(currency) says how much of a currency is for sale. A taker's margin is about the floor's discount on what it buys (1%; 3% for memes and stock tokens) on fees swept into an idle sale, less its route's slippage beyond the reference and its gas; on a busy currency, where Montra's keeper competes, nearer 0.2%. Per day, across every taker together, with all of it at one rate or the other:
Scroll sideways for every column
| Traded a day in non-HYPE pairs | Protocol fees sold through hubs | Quiet currencies, about the 1% floor | Busy currencies, Montra's keeper at ≈0.2% (today; may change) |
|---|---|---|---|
| $100,000 | $400 | $4.00 | $0.80 |
| $1,000,000 | $4,000 | $40 | $8.00 |
| $10,000,000 | $40,000 | $400 | $80 |
Reward pushes. The bounty is 0.25% of what is pushed. What holders are paid depends on trading in tokens that pay their holders: $1M a day traded in such tokens on the 1% tier, below the LP line, pays their holders 0.6% of it, $6,000 a day, and pushing all of it would earn $15. Holders also claim for themselves, and Montra's keeper pushes where the fee covers its gas, so this is a ceiling:
Scroll sideways for every column
| Rewards pushed a day | At 0.25% | Least payout worth a push alone | Least payout in a batch of 22 |
|---|---|---|---|
| $1,000 | $2.50 | $0.17 | $0.05 |
| $10,000 | $25 | $0.17 | $0.05 |
| $100,000 | $250 | $0.17 | $0.05 |
Risks
- Competition. Montra's keeper and every other keeper read the same sales; the first valid transaction wins a lot, and a take ahead of yours lifts the price and fails your fill. A failed transaction still costs its gas.
- Ordering. Elysium has a single sequencer, and how it orders transactions is the sequencer's to set and may change. Nothing here promises you the same treatment as Montra's keeper, or that pending transactions stay unseen: assume a competitor sees what you see, and keep your guards (
maxPrice,minRate,deadline) tight. - Stale or stopped sales. With no fresh reference, or a sale paused or halted, nothing sells. Your keeper cannot fix that: only reporters post.
- Your route. Quote at the size you take; a thin route returns less than its probe price. A sandwich of your route is stopped by your
minRate, at the cost of the fill. - Holding inventory. Taking with
takeleaves you holding the currency until you sell it: a stock token's market can close, a meme can fall, and an issuer can pause or take back its token.
The keeper spec
Everything above in one block, to hand to a developer or paste into an AI.
MONTRA KEEPER SPEC
Chain: Elysium, gas in HYPE. Every address comes from the LaunchpadRegistry (/docs/contracts):
get("feeMarket"), get("routeFiller"), get("protocolVault"), get("factory"), get("lens"), get("quoteRegistry"), hooks().
Fee-market prices are scaled by PRICE_ONE = 1e36: HYPE wei per base unit of the currency sold.
PAID JOBS
1. Hub sales (FeeMarket, taker open to anyone)
hubMarket(address sell) pure returns (bytes32) the sale's id
price(bytes32 id) view returns (uint256 current, uint256 floor, bool fresh) current = anchor * level
available(bytes32 id) view returns (uint256) the lot
market(bytes32 id) view returns (Market)
Market fields: anchor, anchorAt, maxAge, level, startedAt, halfLife, bumpBps, floorBps, paused, halted, ...
take(bytes32 id, uint256 maxAmount, uint256 maxPrice, address recipient, uint256 deadline, bytes data) payable returns (uint256 amount, uint256 owe)
RouteFiller.fill(bytes32 id, uint256 maxAmount, uint256 maxPrice, PoolKey[] route, uint256 minRate, address surplusTo, uint256 deadline) returns (uint256 amount, uint256 owe)
terms on mainnet: level starts at 1 + 0.03, halves every 1800 s, floor 1 - 0.01 (1 - 0.03 for memes and stock tokens),
a take sets level = level * (1 + 0.03 * amount / lot) (max level 2), nothing sells on an anchor older than 300 s,
the anchor falls at most by half every 21600 s (fallHalfLife; never under 1200 s on any sale)
takerOf(bytes32 id) view returns (address): the sale's only taker (zero: anyone)
drip(bytes32 id) view returns (uint256 perDay, uint256 left): a daily cap (perDay zero: none)
events: MarketOpened, Take, AnchorPosted, AnchorReset, PausedSet, ParamsSet, TakerSet, DripSet, ReporterSet (FeeMarket);
Filled (RouteFiller); HubSold, Allocated (vault)
errors: StaleAnchor, MarketHalted, MarketPaused, NothingToSell, TooExpensive, ZeroPrice, NotTaker, Expired (FeeMarket);
ShortRoute, PartialFill (RouteFiller)
2. Holder-reward pushes (each LaunchToken; launches from the factory's TokenLaunched)
earned(address holder) view returns (uint256)
distribute() returns (uint256 received)
pushRewards(address[] holders) returns (uint256 pushed, uint256 bounty)
fee per holder = max(payout * pushBountyBps / 10000, pushFloor); pushBountyBps = 25, pushFloor = 0 on mainnet
payout < PUSH_GATE (20) * pushFloor is skipped; only accounts with no code or an EIP-7702 delegation are pushed
events: Transfer, RewardsDistributed, RewardsPushed, RewardsPushSkipped, RewardsCredited
UNPAID UPKEEP (anyone)
LaunchHook: pendingProtocol(bytes32 poolId), sweep(bytes32 poolId), collectProtocol(address currency), harvestSeed(bytes32 poolId)
ProtocolFeeVault: sellable(address currency), unallocated(address currency), allocate(address currency), deliver(uint256 lane)
LaunchToken.distribute(), FeeSplitter.distribute()
NOT OPEN
FeeMarket.postAnchor/postAnchors (approved reporters only; every sale depends on them)
a sale whose takerOf(id) is not you (the MONTRA buyback's, or any the multisig names)
BuybackFiller.fill (reporters only; the MONTRA buyback's sales name it their only taker; everything bought is burned)
LOOP (every block: on a quiet currency the edge goes to whoever sees new fees first)
for each listed quote currency c other than HYPE:
id = hubMarket(c); skip unless price(id).fresh, not paused or halted, and takerOf(id) is zero or you
lot = available(id) + what sweeping, collecting and allocating c would add,
at most drip(id).left where drip(id).perDay is not zero
out = your route's quote for selling lot of c into HYPE
owe = ceil(lot * price / 1e36)
if out - owe > gas cost * your margin:
one transaction (Multicall3 at 0xcA11bde05977b3631167028862bE2a173976CA11, no value): sweep(pool...) on pools with pendingProtocol, collectProtocol(c) on each hook,
allocate(c), RouteFiller.fill(id, lot, price, route, minRate = 1e36 * out * (1 - slippage) / lot, you, now + 60)
(with inventory instead: FeeMarket.take called directly, value = owe; never through Multicall3, which cannot take a refund)
on TooExpensive (a take landed first): read again, retry
for each token paying its holders:
if fees are owed to it: distribute()
batch = wallets by earned, largest first, taken while the whole batch still pays:
the batch's earned * 25 / 10000 > (78,000 + 30,300 * wallets) gas * gas price * your margin
(a wallet never pushed before adds about 20,000 gas, up to about 85,000)
if batch is not empty: pushRewards(batch)