FlareForge
Oracle LabNetworkAgentsFDCSandboxDocsAbout
API Docs

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 amountUBA sized 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 != liquidation on 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.

← Previous guide

Monitor FAssets agent liquidation risk programmatically

Next guide →

Verify a cross-chain payment in 20 lines of Solidity