Skip to content
Developers

Become a keeper

Every maintenance function is permissionless. What you can call, what you are paid for, and what pays nothing at all.

Fyber has no named keeper, no keeper registry, no allowlist and no retainer. Every maintenance function is open to any address, and every one of them is also called at the head of the user operation that depends on it — so the protocol works with zero keepers, and a keeper is a way to be first, not a way to be necessary (Rule R-1.4.2, Rule R-12.6.1).

Two functions pay. Everything else pays nothing, and is worth calling only if you have a reason of your own.

What pays: liquidation

engine.flag(branch, user);
// wait at least 90 seconds, and no more than 1800
engine.liquidate(branch, user, maxDebtToRepay);

You are paid 0.5% of the seized collateral, capped at 200 USD at the execution price, taken from the seizure before it reaches the pool or the direct liquidator, and paid to you in stock token (Rule R-6.4.1). If you also supply the fyUSD — a direct liquidation — you receive the bonus on top of the keeper share, since you are then both keeper and payer.

Before you flag, use the preview:

(uint256 debtLiq, uint256 coll, uint256 bonus, uint256 price, uint8 mode)
    = engine.previewLiquidation(branch, user);
uint256 remaining = engine.bucketAvailable(branch);

Checks you must pass at execution, not at flagging (Rule R-6.1.1):

  • The branch is activated and not shut down.
  • The regime is LIVE24 or DEGRADED, never FROZEN, and there is no quietEdge running.
  • In DEGRADED the fall must be confirmed and the longer confirmation delay must have run. The Stability Pool is the taker in both regimes.
  • No Closer liquidation freeze is in force on the branch.
  • If a token upgrade freeze is running, at least 24 hours have elapsed since it started.
  • In LIVE24, between 90 seconds and 1 800 seconds have passed since your flag; in DEGRADED, the longer confirmation delay has run and the flag is still inside its 6 hours life.
  • The position's ICR at pLiq is still below 115% now.
  • The hourly bucket has room.

The threshold is re-evaluated at execution. If the borrower repaid or added collateral in the interval, your call reverts NotLiquidatable and you have paid gas for nothing. Budget for that.

Sizing. maxDebtToRepay is the fyUSD you are willing to supply yourself, and it applies only after the Stability Pool has absorbed what it can. Pass zero if you only want the keeper share for triggering the pool's absorption. Per-transaction seizure is capped at 0.25 × d2_eff; the branch's hourly bucket at 1.0 × d2_eff in LIVE24; and the global bucket across all branches at 0.6 × Σ d2_eff. An empty bucket reverts BucketEmpty and your flag stays valid until it expires (Rule R-6.3.3).

Bonus. The bonus you or the pool receive is bonus_base adjusted by state and by seizure size, clamped to 15%. See Liquidations for the table.

Risk

The sequencer orders transactions first-come, first-served. Whether a public mempool exists and whether transactions can be reordered determines whether your flag telegraphs your liquidation to a competitor, and whether an immediate sale in the same transaction can be sandwiched. Verify this on-chain before building anything that depends on either.

What pays: buying collateral from the sale

uint256 d = sale.discount();      // 0 to 3%, ramping over six hours from the start of a lot, then flat
uint256 inv = sale.inventory();
sale.buy(collAmount, maxFyIn);

You pay fyUSD at the oracle price less the current discount and receive stock token. Your compensation is the discount — there is no other payment (Rule R-9.5.4).

Conditions: the branch is not in FROZEN; at most 1.0 × d2_eff per hour in the sale's own bucket, distinct from the liquidation bucket; and at most 0.25 × d2_eff per transaction.

The same shape applies to the backstop's residual collateral, at a fixed 3% discount:

backstop.sellCollateral(branch, amount, minFy);   // oracle price less 3%, LIVE24 only, 0.25 × d2_eff per tx

What pays nothing

These exist so the protocol maintains itself. Call them if you run a front end, an integration, or a position that benefits.

OracleAdapter.poke() — refreshes the close snapshot, the pre-pause snapshot, the last accepted price, the last seen multiplier and the degraded-episode counters. Called at the head of every branch operation. A fifteen-minute cadence is reasonable; nothing depends on it (Rule R-3.2.9).

LiquidityOracle.poke() — samples depth into the current hourly slot if it is empty, costing roughly 150 000 to 300 000 gas for three pools. Every open and borrow calls it when the slot is empty, so borrowers fill the buffer themselves. This is the one that matters for dormant branches: a branch cannot activate until its buffer is at least half full over seven days, and if nobody pokes, activation waits indefinitely (Rule R-5.4.2, Rule R-12.3.2).

RateFloor.poke() — writes one daily sample of the reference borrow rate into a 30-slot buffer. Also called by open and setRate when the day's slot is empty (Rule R-5.7.2).

Branch.accrue() — advances the interest clock, mints pending interest to the router and routes it. Useful if you want the Stability Pool credited promptly rather than at the next branch operation.

Branch.updateMintFreeze() — recomputes the freeze from the current TCR, bad debt and upgrade state. It also runs on every operation.

Branch.checkUpgrade() — compares the token's EIP-1967 implementation slot against the snapshot, at roughly 2 600 gas. It runs at the head of open, borrow, withdraw, flag, liquidate and redeem. At the end of an upgrade freeze, calling it is what attempts the automatic clearance — three consistent rounds, no issuer pause, a consistent implied share, and a transferability self-test against the seeded Probe (Rule R-4.3.9).

Branch.flagShutdown() and Branch.shutdown() — available when the branch's TCR has been below its shutdown ratio for an hour, or when a degraded episode has run past seven days (Rule R-6.9.1).

Branch.settleAfterShutdown(user) — settles a remaining position on a shut branch, 30 days after urgent redemptions opened, at the frozen price with zero bonus and zero keeper share (Rule R-6.9.8).

Branch.activate() — activates a dormant branch when all eight criteria pass. canActivate() returns the first failing one, so you can monitor exactly what is missing (Rule R-12.3.2).

BranchRegistry.flagSunset() and enterSunset() — available when two activated branches are shut down, or the aggregate TCR has been below 130% for an hour (Rule R-6.9.7).

InterestRouter.latchFeeSwitch() — the one-way permissionless move to regime 2, once total debt has exceeded 25 M (Rule R-10.2).

Backstop.coverBadDebt(branch, amount) — burns backstop fyUSD to reduce recorded bad debt.

Backstop.redistribute(branch) — after bad debt has stood 72 hours with a backstop below minDebt, distributes it pro rata across remaining positions on that branch (Rule R-6.8.4).

PoolIncentive.checkpoint() and purge() — accumulates in-range liquidity times time; purge() routes the balance to active pools after 180 days with no staked position (Rule R-10.5).

sfyUSD.harvest() — claims the wrapper's gains, hands collateral to the sale, redeposits proceeds. Implicit in every wrapper interaction.

Running a watcher

An open-source watcher is published in the repository, runnable by anyone, and the protocol depends on it in no way whatsoever. It alerts on (Rule R-16.6.1):

  • Σ debt + Σ badDebt + minted[PSM] ≠ totalSupply
  • a degraded state lasting more than four hours on a weekday
  • MINT_FREEZE on any branch
  • more than 2% of a branch's positions taken over one degraded episode
  • d2_eff decaying, meaning 48 hours with no depth sample
  • PSM reserve below 5% of the supply
  • any Closer freeze
  • canActivate() turning true on a dormant branch
  • a capacity tier locking

Run your own copy. Do not assume anybody else is watching.

The economics, stated plainly

The keeper share is 0.5% capped at 200 USD, and the constants are frozen. There is no gas reimbursement, no urgency premium, no keeper fund and no way to add one after deployment (Rule R-6.4.1).

Whether that is enough is a function of gas costs on Robinhood Chain after the launch subsidy expires — an open question re-measured before deployment, and one of the reasons the caps start small.

If liquidation keeping turns out to be unprofitable, the design's answer is that the protocol still functions: the Stability Pool absorbs a liquidation whenever anyone triggers it, borrowers themselves have every reason to trigger a competitor's liquidation, and a branch whose positions go unliquidated eventually falls below its shutdown ratio and closes with urgent redemptions open. That is a degradation, not a failure — and it is the failure mode the small caps are sized for.

Last reviewed: 2026-09-07 · Spec v0.4