Implements issues #72 — automatic re-balancing script for lending pool liquidations and #259 — automated liquidation bot for undercollateralized loans (background worker that monitors prices every minute and liquidates automatically).
scripts/liquidation-keeper.ts is an automated bot that finds under-collateralized
loans and liquidates them on-chain before they become bad debt.
- Fetch open loans — either
--source=db(theloanstable, resolving the on-chain loan id from the funding ledger entry) or--source=chain(iterates the LendingContract directly viaget_loan_count/get_loan). - Read authoritative on-chain state — collateral + remaining debt from
lending.get_loan, the borrower'sreputation.get_reputation_score, and the contract's own dynamiclending.calculate_liquidation_threshold. - Price the position — debt is always XLM; collateral price/decimals come from
LIQUIDATION_PRICE_TABLE_JSON(or an on-chain oracle, if wired in later). Unpriced assets are skipped and logged, never guessed. - Liquidate — if
LTV bps >= threshold bps, submitslending.mark_defaultedsigned byADMIN_SECRET_KEY. - Alert — posts to Slack/Discord webhooks on every liquidation and every failure.
npm run liquidation:keeper # one-shot (cron)
npm run liquidation:keeper -- --dry-run # evaluate only
npm run liquidation:keeper -- --source=chain
npm run liquidation:keeper -- --interval=60 # background service
npm run liquidation:keeper:service # shorthand: poll every minuteConfig is documented in .env.example (LIQUIDATION_* section). Every step is
individually error-handled — one bad loan never aborts the run — matching the
default-management scheduler's conventions.
Three ways to run the keeper as a background worker:
- GitHub Actions (default deployment).
.github/workflows/keepers.ymlcallsPOST /api/cron/liquidationevery 5 minutes. Set theKEEPER_BASE_URLandCRON_SECRETrepository secrets to enable it; without them the workflow is a no-op. The route (app/api/cron/liquidation/route.ts) authenticates the caller withBearer ${CRON_SECRET}(same scheme as thepayment-due/default-managementcrons), loads the keeper config from env, runs a full scan, and returns the per-run summary. - Vercel Cron (safety net).
vercel.jsonalso schedules the route once a day. Vercel Hobby rejects deployments that declare any cron more frequent than daily, which is why the 5-minute cadence lives in GitHub Actions rather than here. - Self-hosted service.
npm run liquidation:keeper:servicerunsscripts/liquidation-keeper.ts --interval=60— a long-lived loop that rescans prices and liquidates every 60 seconds. Wrap it in Docker/systemd/PM2 on any always-on host. One-shot cron invocations (item 1 of Usage) remain supported for schedulers with coarser granularity.
Either way, ADMIN_SECRET_KEY is the real control: the on-chain
mark_defaulted call require_auth()s the admin address, so a leaked cron URL
cannot liquidate anything without the signing key.
__tests__/scripts/liquidation-keeper.test.ts— 27 tests covering the pure LTV/threshold math, alert delivery (incl. webhook failure isolation), CLI/env config parsing (incl. the every-minute 60s interval), and full run orchestration (liquidate, dry-run, healthy, skipped, failed, bothdbandchainsources).__tests__/api/cron/liquidation.test.ts— 7 tests covering the cron route (success, 401 on missing/wrong bearer, unset-secret dev mode, 500 on keeper failure, GET alias) and thevercel.json* * * * *schedule.