Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stellar-fee-forecast

Predicts Stellar network congestion and expected base fee for the next 5/15/60 minutes, so dApps can time submissions proactively — delay a non-urgent batch operation, or warn a user before a costly action ahead of a predicted congestion spike.

This is complementary to StellarCommons/stellar-fee-tracker, not a replacement for it. stellar-fee-tracker answers "what is the fee doing right now / historically" — a reactive dashboard. This tool answers "what is the fee likely to do in the next few minutes" — a predictive signal a dApp can act on before congestion hits, not after. A production integration would reasonably use both: stellar-fee-tracker for display and historical context, stellar-fee-forecast for the timing decision.

Why this exists

Most Stellar fee tooling is reactive: it tells you the current fee_stats snapshot. That's useful for setting a transaction's max_fee right now, but it can't tell a dApp whether it's about to get worse. A batch job, a scheduled claim, an NFT mint countdown, or any operation that isn't time-critical to the second can benefit from a short-horizon forecast: "congestion likelihood in the next 15 minutes is 72% — maybe wait."

Architecture

Horizon (/fee_stats, /ledgers)
        │  poll every 10s (configurable)
        ▼
  backend/app/collector.py  ──►  rolling-window store (Postgres or SQLite)
        │
        ▼
  backend/app/model.py  (Holt-Winters exponential smoothing baseline)
        │
        ▼
  backend/app/api.py  (FastAPI: GET /forecast, /forecast/{5,15,60})
        │
        ▼
  hooks/  (TypeScript, useFeeForecast() — wagmi-style React hook)
  • Ingestion: app/collector.py polls Horizon's fee_stats (percentiles over the last 5 ledgers) and the matching ledgers/{seq} record (close time, operation/transaction counts) on a schedule, and writes a normalized row into a rolling window. SQLite by default (zero setup); point FEEFORECAST_DATABASE_URL at Postgres for production.
  • Model: app/model.py. Deliberately a solid statistical baseline, not a novel architecture — Holt (damped double exponential smoothing, via statsmodels) on the p50 fee and capacity-usage series, extrapolated to the requested horizon. Congestion likelihood blends the trend-extrapolated capacity forecast with the recent frequency of congested readings. See Accuracy & limitations below — this is explicitly not a claim of high-precision congestion prediction.
  • API: app/api.py, FastAPI. GET /forecast returns all configured horizons; GET /forecast/{minutes} returns one. GET /health reports how much history is stored.
  • Hook package: hooks/, a small TypeScript package exporting useFeeForecast(), matching the wagmi-style conventions used elsewhere in the Stellar hooks ecosystem (e.g. dark-princezz/stellar-hooks's usePayment/useClaimableBalance shape: { data, isLoading, isError, error, refetch }), plus forecast-specific conveniences (window, isCongestionLikely).

Data sources — what's real and what isn't (read this before trusting the numbers below)

Horizon's /fee_stats endpoint only ever exposes a live snapshot of the last 5 ledgers — it has no history endpoint. So a genuine training/backtest dataset for the percentile series has to be built by running the collector over time (going forward), or by backfilling the /ledgers endpoint (which does have full history) for base_fee_in_stroops and closed_at, with fee percentiles back-filled equal to the base fee for uncongested ledgers (see the caveat in scripts/backfill_ledgers.py — this backfill approach can't retroactively recover historical percentile spreads during a past congestion event, only the live collector can capture those going forward).

This sandboxed development environment has no network route to horizon.stellar.org, so the backtest numbers below were run against a dataset generated by scripts/generate_synthetic_dataset.py — a script that is explicitly and permanently labeled as synthetic in its own docstring, its CLI output, and here. It's built from the well-documented real shape of Stellar mainnet fee behavior (base fee flat at the 100-stroop network floor the large majority of the time, ~5s ledger close cadence, occasional multi-minute surge-pricing episodes with 2-30x fee jumps) — but the specific values are simulated, not measured. Treat every number in the table below as "the model works and beats a naive baseline on a plausible fee pattern," not as "here is Stellar mainnet's actual forecast accuracy."

To get real numbers: python -m scripts.backfill_ledgers --pages 20 against a live Horizon instance (5 minutes, ~4000 real ledgers), then python -m scripts.backtest. Full commands in SETUP.md. This is listed as issue #1 below — regenerating the backtest against real data is the single highest-priority follow-up for anyone building on this.

Backtest results (SYNTHETIC data — see above)

Rolling-origin backtest: at each origin point, forecast using only data available up to that point, compare against what actually happened at origin + horizon. Compared against a persistence baseline (predict "no change from the last known reading") — a forecaster that can't beat persistence isn't earning its complexity.

Reproduce with: python -m scripts.generate_synthetic_dataset --hours 6 --seed 42 && python -m scripts.backtest --stride 15 --max-origins 200

Horizon n Fee MAE (stroops) vs. persistence MAE Fee RMSE Congestion F1 vs. persistence F1
5 min 140 91.99 99.91 (beats it) 225.26 0.250 0.294 (loses)
15 min 136 125.93 139.69 (beats it) 289.14 0.069 0.059 (beats it, both weak)
60 min 118 121.81 133.53 (beats it) 299.22 0.000 0.071 (loses)

Honest read of these numbers:

  • The fee-magnitude forecast consistently beats naive persistence on MAE across all three horizons, by 6-12%. That's a real, if modest, signal — exponential smoothing's trend component is doing something useful, not just tracking noise.
  • The binary congestion-likelihood classifier is weak and, at the 5- and 60-minute horizons, actually worse than just assuming "whatever is happening now keeps happening." At 60 minutes it never fires a true positive on this dataset (F1 = 0). Congestion events in the synthetic data are short (1-10 minutes) and relatively rare (~11-13% of samples), which is a genuinely hard classification setting for any trend-following model — a spike that starts and ends inside the forecast window looks the same, from the model's vantage point at t=0, as no spike at all.
  • Practical implication for integrators: treat expectedBaseFeeStroops as a soft, directionally-useful estimate, and treat congestionLikelihood as a weak prior, not a reliable trigger for automated action, at least until real-data backtests (issue #1) and the planned model upgrade (issue #4) improve on this baseline. Good current use: nudge a UI warning or a "maybe wait" suggestion. Bad current use: silently auto-delaying a user's transaction based on a single threshold crossing.

Quickstart

cd backend
pip install -r requirements-dev.txt
PYTHONPATH=. pytest -q                     # 9 tests, backend

# zero-setup local run against SQLite:
uvicorn app.api:app --reload &
python -m app.collector                    # polls real Horizon in the background

curl localhost:8000/forecast
cd hooks
npm install
npm test        # 4 tests, hook package
npm run build

Full setup, Postgres/Docker instructions, and the real-data backfill workflow: see SETUP.md.

API

GET /health
GET /forecast              -> { generatedAt, lastObservedLedger, observationsUsed, windows: [...] }
GET /forecast/{5,15,60}    -> single ForecastWindow

ForecastWindow: horizonMinutes, expectedBaseFeeStroops, expectedP50FeeStroops, expectedCapacityUsage, congestionLikelihood (0-1), feeForecastStdStroops.

Hook usage

import { useFeeForecast } from "@stellar-community/use-fee-forecast";

function SubmitButton() {
  const { window, isCongestionLikely, isLoading } = useFeeForecast({
    apiUrl: "https://your-deployed-api.example.com",
    horizon: 15,
    refetchInterval: 30_000,
  });

  return (
    <button disabled={isLoading}>
      {isCongestionLikely
        ? `Heads up: congestion likely soon (~${window?.expectedBaseFeeStroops} stroops expected)`
        : "Submit"}
    </button>
  );
}

Accuracy & limitations

  • Statistical baseline, not a sophisticated architecture. Holt's method is a strong, well-understood baseline, chosen deliberately per the brief rather than reaching for something heavier first. It has no way to learn genuinely nonlinear or multi-signal congestion patterns (e.g. "the last three 15-minute windows before a scheduled Soroban event tend to spike"). Issue #4 tracks a follow-up with a more expressive model once there's enough real data to justify it.
  • No seasonality yet. With only a few hours/days of rolling history, there isn't a reliable daily/weekly cycle to fit. The model uses trend only (Holt, not Holt-Winters seasonal). Once weeks of real data accumulate this should add a daily seasonal component — same issue.
  • congestionLikelihood is not a calibrated probability. It's a blend of recent-frequency and trend-crossing heuristics. Don't feed it directly into an expected-value calculation; use it as a directional signal.
  • Backtest is on synthetic data, per the section above, until someone runs the real backfill + backtest against live Horizon history.
  • Horizon itself is being deprecated in favor of Stellar RPC per Stellar's own docs; this service should be considered a bridge implementation, and a Stellar-RPC-based ingestion path is worth tracking as the ecosystem migrates (not yet filed as an issue below — flag on the tracker if useful).

Repo layout

backend/    FastAPI service, model, ingestion, backtest, tests
hooks/      TypeScript useFeeForecast() package
SETUP.md    Local dev, Docker/Postgres, real-data backfill workflow

License

MIT — see LICENSE.

About

Predicts Stellar network congestion and base fee for the next 5/15/60 minutes using Holt exponential smoothing on Horizon fee_stats, so dApps can proactively delay non-urgent operations or warn users ahead of a predicted spike. Includes a wagmi-style React hook.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages