Guide, FAssets
Build an FAssets liquidator bot on Flare in Python
FAssets agents mint wrapped XRP (FXRP) against vault collateral. When a vault drops below its liquidation threshold, the protocol opens the position to any caller willing to burn FAssets in exchange for discounted vault collateral. That caller is a liquidator, you, and this guide walks through a Python bot that watches for the opportunity, quotes the reward, and executes the liquidation.
How liquidation actually works
Two triggers open an agent to liquidation. Either the on-chain collateral ratio falls below liquidationThresholdBIPS, or the AssetManager itself flags the agent during its periodic settlement check. Once open, any address can call liquidate(agentVault, amountUBA), burning that much FAsset and receiving vault + pool collateral worth amountUBA plus a premium. The premium is the liquidator fee and it grows the longer the vault stays underwater.
Everything the bot needs is on-chain plus the aggregated state the FlareForge indexer already exposes. Pipeline: poll the public risk summary, filter for status in (liquidation, full_liquidation), decide how much FAsset to burn, call the AssetManager.
Step 1, spot the opportunity
The FlareForge risk summary aggregates every agent in one request. We only want the ones already in a liquidation state; the danger and critical bands are interesting but not yet open.
import json
from urllib.request import urlopen
BASE = "https://flareforge.io/api/v1"
def find_liquidatable() -> list[dict]:
with urlopen(f"{BASE}/agents", timeout=10) as resp:
data = json.loads(resp.read())
return [
a for a in data["agents"]
if a["status"] in ("liquidation", "full_liquidation")
]
for agent in find_liquidatable():
print(agent["address"], agent["status"], agent["collateral_ratio_bips"])Step 2, quote the reward before calling
Execute-then-regret is expensive. Quote the liquidation payout in a read-only call first. AssetManager exposes getAgentInfo(address) which returns the live vault state plus the current liquidation premium bps. Compute the gross payout, subtract the FAsset you will burn (at the premium conversion rate) and any gas cost, and only execute if the spread is positive at your target threshold.
from decimal import Decimal
from web3 import Web3
FLARE_RPC = "https://flare-api.flare.network/ext/C/rpc"
ASSET_MGR = Web3.to_checksum_address("0x...") # getAssetManagerFXRP() from the FAssets registry
AGENT_INFO_ABI = [{
"name": "getAgentInfo",
"type": "function",
"stateMutability": "view",
"inputs": [{"name": "agentVault", "type": "address"}],
"outputs": [{
"name": "info", "type": "tuple",
"components": [
# trimmed; the real struct has 40+ fields. keep what you need.
{"name": "status", "type": "uint8"},
{"name": "mintedUBA", "type": "uint256"},
{"name": "vaultCollateralRatioBIPS", "type": "uint256"},
{"name": "liquidationPaymentFactorVaultBIPS",
"type": "uint256"},
{"name": "liquidationPaymentFactorPoolBIPS",
"type": "uint256"},
],
}],
}]
def quote(w3: Web3, agent: str) -> dict:
c = w3.eth.contract(address=ASSET_MGR, abi=AGENT_INFO_ABI)
info = c.functions.getAgentInfo(Web3.to_checksum_address(agent)).call()
status, minted, cr_bips, vault_factor, pool_factor = info[:5]
premium_bps = (vault_factor + pool_factor) - 10_000
return {
"minted_uba": minted,
"cr_bips": cr_bips,
"premium_bps": premium_bps,
}liquidationPaymentFactorVaultBIPS + liquidationPaymentFactorPoolBIPS is the gross payout in basis points of the FAsset burned. Everything over 10000 is the liquidator reward. At 10200 you are getting 2% premium; 11500 is 15%, which is already deep liquidation territory.
Step 3, pre-fund FAsset inventory
Liquidating burns FAsset. You need FXRP in your wallet before the call, the bot cannot conjure it. Three ways to get it.
- Mint against a healthy agent. Pay XRP to an agent and receive FXRP in return. Slowest; bridges an actual XRP position.
- Buy on SparkDEX or an FXRP pair. Fast, has slippage. The right call when you want inventory immediately and the liquidation window is open now.
- Partial liquidation strategy. You do not need to burn the full minted amount. Call with a smaller
amountUBAsized to what you hold and can replenish without dumping price.
Step 4, execute
from eth_account import Account
LIQUIDATE_ABI = [{
"name": "liquidate",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{"name": "agentVault", "type": "address"},
{"name": "amountUBA", "type": "uint256"},
],
"outputs": [
{"name": "liquidatedUBA", "type": "uint256"},
{"name": "amountUBA", "type": "uint256"},
{"name": "poolCollateralAmount", "type": "uint256"},
],
}]
def execute(w3: Web3, priv_key: str, agent: str, amount_uba: int) -> str:
account = Account.from_key(priv_key)
c = w3.eth.contract(address=ASSET_MGR, abi=LIQUIDATE_ABI)
tx = c.functions.liquidate(
Web3.to_checksum_address(agent),
amount_uba,
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": 500_000,
"maxFeePerGas": w3.to_wei("30", "gwei"),
"maxPriorityFeePerGas": w3.to_wei("1", "gwei"),
"chainId": 14,
})
signed = account.sign_transaction(tx)
txh = w3.eth.send_raw_transaction(signed.raw_transaction)
return txh.hex()Partial liquidation is the safer default. Pass amount_uba = min(minted_uba, your_fxrp_balance) so the call never reverts on insufficient FAsset. The return tuple tells you what actually got burned and how much vault + pool collateral you received.
Full loop with state + alerts
import json, time
from pathlib import Path
from urllib.request import Request, urlopen
SEEN_FILE = Path("/var/lib/flareforge-liquidator/seen.json")
DISCORD_WEBHOOK = "https://discord.com/api/webhooks/..." # optional
def load_seen() -> set[str]:
if not SEEN_FILE.exists(): return set()
return set(json.loads(SEEN_FILE.read_text()))
def save_seen(s: set[str]) -> None:
SEEN_FILE.parent.mkdir(parents=True, exist_ok=True)
SEEN_FILE.write_text(json.dumps(sorted(s)))
def notify(message: str) -> None:
req = Request(
DISCORD_WEBHOOK,
data=json.dumps({"content": message}).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
urlopen(req, timeout=5).read()
def tick() -> None:
seen = load_seen()
for agent in find_liquidatable():
key = f"{agent['address']}:{agent['status']}"
if key in seen:
continue
notify(f"New liquidation: {agent['address']} status={agent['status']}")
# ... quote(), decide, execute() ...
seen.add(key)
save_seen(seen)
if __name__ == "__main__":
while True:
try: tick()
except Exception as e: print(f"tick failed: {e}")
time.sleep(60)Things that bite on the first run
- Racing other liquidators. You are not the only bot watching. The AssetManager is first-come-first-served per call. Run close to the chain (low-latency RPC, possibly your own node) and submit with a competitive priority fee. Mempool-level sniping is not possible on Flare today, but block-level race conditions are.
- Status flips back to normal. A vault can leave liquidation mid-run if the price moves back. Quote twice, once when you pick the target and once immediately before submitting. If
status != liquidationon the second quote, skip. - Pool vs vault collateral split. Your reward comes partially as the vault collateral token (usually USDX or WFLR) and partially as pool collateral (WFLR). If you cannot accept one of those, the call still succeeds but you are stuck holding the token.
- Treat FAsset balance as a strategic resource. Burning FXRP is irreversible, you get vault collateral in return. If you hold a small inventory, wait for deep-premium opportunities (
premium_bps > 1000) rather than flipping every 2% event. Track your realized premium in the state file.
Related on the site
- Agent Monitor, live dashboard of every FAssets agent with risk classification, the human-facing version of the loop above.
- FAssets risk guide, read-only companion that builds a Discord alert on the same endpoint without executing any on-chain calls.
GET /api/v1/agents/risk-summary, aggregate counts + worst-offenders list, lighter than fetching the full agent list.