BGP history AS graph

GET /bgp/history/as-graph

Optional selection_limitations reports coverage limitations of selections contributing to the graph. Both best and candidate lanes reuse best-path provenance at the same scope and instant; no current target mode classifies a historical selection. Inferred contributors add local_origination_not_observed; missing provenance adds selection_source_unavailable. Known Loc-RIB-only selections omit these codes. These are graph-scope qualifications, not proof that a particular edge is wrong; adjacency counts, ranks and paths are unchanged.

connected_exits (best lane only) reports a forwarding check on inferred selections. A live best graph (at omitted: the server's own now) omits an inferred best-path entry when its exact prefix is directly connected on the selecting router in the same routing domain: the router's own OSPFv2 or OSPFv3 stub/intra-area prefix, or its default-VRF EIGRP Connected/Rconnected route not marked uninstalled, from an area that is not stale. A router does not advertise a stub prefix for an interface in state Down. IS-IS reachability, another router's advertisement, a bare interface address, another VRF and a more-specific subnet never qualify; Loc-RIB selections are never omitted. The response then carries {"check": "applied", "excluded": n, "entries": [...]}, with at most 100 entries of device_id, hostname, routing_domain_id, prefix, exit_asn and evidence (ospf_stub | eigrp_connected); excluded is always the full count. Omitted entries leave every count, edge, origin space and ranking of the graph, but the received BGP route, its selection and the candidate graph are unchanged. A best graph at an explicit at, even a near-now one, reads the recorded state and carries {"check": "not_applied_historical", "excluded": 0}: no as-of connected evidence exists, so its exits are kept, and snapshots compared at explicit instants stay like for like. The candidate lane keeps its ±60s live window. Candidate graphs omit the field. The live check adds one scoped read per request; the dossier and movers endpoints remain BGP-selection views without this check.

AS-Flow snapshot at an instant: autonomous systems as sized bubbles and AS-path adjacencies as weighted edges. The aggregate, AS-centric lens (complements the per-prefix scenario/candidates replay). source=best (default) assembles from the best-path intervals open at at and counts best-path entries (prefix × vantage device); source=candidates assembles from the per-peer RIB — every path the router learned from every peer — and counts candidate entries (prefix × vantage device × peer path). Candidates read live bgp_rib_entry when at is absent or within ±60s of server time (any rib_mode ≠ none), and bgp_rib_entry_history for a historical at behind a scope-aware full-RIB capability gate (as/rd/device/af — a scope the recording never covered answers available:false, not an empty graph). The live lane also answers available:false when no enabled BMP target feeds the scope. A prefix filter is rejected in candidate mode (the candidate lanes do not implement it). Either way the fan is seen from the operator's own routers, never an internet-wide AS graph.

Auth: all authenticated roles

Query params: as_id (required); optional routing_domain_id, device_id, af (ipv4|ipv6), at (RFC 3339, default now), top_origins (default 30, max 200), source (best|candidates, default best), and the optional presence-window pair presence_from/presence_to (RFC 3339, both or neither, presence_from < presence_to) with presence_buckets (default 96, max 200) — see the edge-barcode paragraph below. Ranking and folding: rank_by (origins default | via_transit), rank_at (RFC 3339 — pins fold membership/naming to that instant; lanes that cannot answer there fall back and say so in rank_note), center_asn (re-roots the graph on that AS: only paths traversing it, trimmed at its first occurrence — "AS flow from here"), pin_asns (comma-separated always-named origins, max 50 — the Find control; an organisation is often several ASNs).

Response: 200 OK

{
  "at": "2026-07-03T12:10:00Z",
  "self_asn": 64500,
  "nodes": [
    { "asn": 64500, "role": "self", "count": 850, "reach": 1218, "reach_space": 3121065344 },
    { "asn": 3356, "role": "neighbor", "count": 309, "sessions_down": false, "parent": 64500, "hop": 1, "reach": 811, "reach_space": 2011065344 },
    { "asn": 2914, "role": "transit", "count": 310, "parent": 3356, "hop": 2, "originated": 12, "space": 393216, "deagg": 1.4, "reach": 640, "reach_space": 1811065344 },
    { "asn": 13335, "role": "origin", "count": 214, "parent": 2914, "hop": 3, "originated": 214, "space": 1704960, "deagg": 2.1 },
    { "asn": 0, "role": "other", "count": 414 }
  ],
  "edges": [
    { "from": 64500, "to": 3356, "count": 309 },
    { "from": 3356, "to": 2914, "count": 310 }
  ],
  "other": { "origins": 1204, "count": 414 },
  "folds": [
    { "anchor_asn": 2914, "origins": 802, "count": 291, "space": 88211456 },
    { "anchor_asn": 3356, "origins": 402, "count": 123, "space": 40211456 }
  ],
  "spine": [
    { "asn": 2914, "reach": 640, "reach_space": 1811065344, "coverage_pct": 58.0, "folded_origins": 802, "folded_count": 291, "folded_space": 88211456 },
    { "asn": 6453, "reach": 301, "reach_space": 811065344, "coverage_pct": 84.0 }
  ],
  "off_tree_pct": 17.5,
  "deep_pct": 38.0,
  "origins_total": 1218,
  "origin_mass": 764,
  "unit": "best_path_entries",
  "truncated": true,
  "source": "best",
  "available": true,
  "rank_by": "origins",
  "connected_exits": {
    "check": "applied",
    "excluded": 1,
    "entries": [
      { "device_id": "uuid", "hostname": "edge1", "routing_domain_id": "uuid", "prefix": "192.0.2.0/24", "exit_asn": 3356, "evidence": "ospf_stub" }
    ]
  }
}

role is self | neighbor (a directly-peered upstream / first AS-path hop) | transit | origin | other. The other-cloud node uses asn: 0 (RFC 7607 reserved, a safe sentinel) and folds origins beyond top_origins; truncated is then true. Folding hides only the origin bubble — the folded path's transit edges stay visible and its last visible AS edges into the other-cloud (B2); a first-hop (neighbour) AS never folds, even when it originates the prefix itself (such paths are fully drawn and not counted in other). sessions_down marks a neighbour whose sessions to that peer-AS are all down at at. The self node's count is external entries only — locally-originated/internal prefixes (empty AS_PATH) are not exits and are excluded, so it equals the sum of the self → * edges, in both sources.

Ranking fields (all additive; measured over the FULL graph, folded origins included): per node parent (dominant predecessor = heaviest incoming path-adjacency, ties to the lower ASN; absent on the root), hop (BFS distance from the root over ALL observed adjacencies — hop distance over observed adjacencies, not a per-route minimum), originated (entries originating at the AS — 94% of transits originate too), space (unique IPv4 address space it originates: an interval union, never a sum; default routes and AS0-carrying rows excluded), deagg (originated IPv4 prefixes per merged contiguous block), reach/reach_space (distinct originating ASNs in the dominant-tree cone and the TREE-sum of their space — the true per-AS union lives in as-detail's transit_space), and offering_peers (candidate lanes only: distinct peer sessions offering that origin's prefixes). Graph-level: off_tree_pct (share of node-entering mass via a non-dominant predecessor — what a spanning tree cannot draw), deep_pct (origin mass at hop ≥ 5), origins_total/origin_mass (distinct known origins and their entry mass, folded included), spine[] (greedy transit spine with CUMULATIVE coverage_pct; per-vantage — another vantage yields another spine; each member additionally carries folded_origins/folded_count/folded_space: the EXACT rollup of the folds whose anchor's dominant-parent chain first meets that member — the distinct origin union, their additive entry mass, and each distinct origin's union space once — which the reach presentation uses instead of summing the overlapping per-anchor arcs), folds[] (the per-anchor fold list next to the wire-compatible other; fold membership is TRAVERSAL-based — the anchor is the folded path's last visible AS — so one origin can fold under several anchors and Σ folds[].origins ≥ other.origins (measured 1059 vs 968 on the DN42 best lane); consumers must never sum origins/space across anchors — only count partitions (Σ folds[].count = other.count); space per fold sums each anchor's distinct folded origins once — an aggregate claim, never per-origin), rank_by/rank_note/rank_at echoes, and center_asn (on a re-rooted graph the root node carries the center ASN with role self, self_asn stays the true self, and sessions_down is never set — session evidence exists only on the true self's first hop). With rank_by=via_transit the named set is the top-N originating ASNs at hop ≥ 2 by unique IPv4 space (a view, not a fold: only the named candidates' paths are drawn, folds/other stay empty, and the residue is stated in via_transit: {candidates, space_total, named_space}); a scope without IPv4 space figures refuses with rank_note and falls back to origins.

Meta honesty (AG-UNIT/F7): unit is best_path_entries or candidate_entries and MUST be read before comparing counts across captures. source echoes the input lane; candidates additionally carry data_source (live|history), rib_modes (live: distinct enabled bmp_target.rib_mode values in scope; history: omitted because neither is_best nor today's modes establish the historical feed type), and scope_warning, always present in candidate mode: entries multiply by the peers offering each prefix, and additionally by the number of vantage devices when no device_id is given. available is false only for source=candidates with a historical at outside full-RIB history coverage. Errors: 400 on a missing as_id or invalid af/at/source/rank_by/rank_at/center_asn/pin_asns (pins capped at 50), and on a malformed or half-specified presence window.

Edge-barcode presence lane: when presence_from/presence_to are given and the graph resolves to source=best, each edge additionally carries "presence" — a '0'/'1' string of presence_buckets characters, oldest→newest, marking the window buckets in which the adjacency appears on a recorded best-path interval — and the graph carries presence_buckets/presence_from/presence_to echoes. An all-'0' bitmap is an honest "never recorded in this window" (the query ran); an absent presence key means no bitmap was computed (candidate lane, other-cloud edge, or no window requested). Adjacency extraction mirrors the graph's unfolded weave (self→first hop, consecutive external hops with prepends collapsed, last→origin), so folding never changes recorded presence — and it applies the same center_asn trim and AS0 drop as the graph walk. Candidate-lane graphs never carry presence (no window index on bgp_rib_entry_history, by design), and fold arcs never do: a fold is not an adjacency, and an aggregate stripe would imply the per-origin claim the ⬡-dialect forbids.