An agent that finds the lift a platform depends on, before it fails, and files the ticket after a person approves.
▶ Demo video (2:32) · Live site · Judge's tour · Play the walkthrough · Issues the agents filed · Blog post
Unofficial decision-support prototype. Not affiliated with or endorsed by the MBTA or MassDOT. Uses MBTA GTFS and V3 API data under the MassDOT Developers License Agreement. Data may be incomplete or out of date. Always check mbta.com/alerts/access before travelling. Elevator status line 617-222-2828; customer support 617-222-3200 (TTY 711).
Results describe the open-data model and may differ from conditions on the ground.
| of 366 step-free platforms in Boston's MBTA station data hang on a single lift | lift pairs are harmless alone but cut platforms off together | lifts can each cut a whole station off from the step-free network | of 734 FalkorDB max-flow results certified and equal to NetworkX |
LastLift turns the MBTA's published station layouts into a flow network in FalkorDB, so for every platform it knows how many lift failures it takes to cut riders who need step-free access off, which lift that is, and what to do when it happens. An agent answers questions about it through tools that any MCP client can call, and operations agents turn a closure into a ticket a person approves.
Graph Hacks track: Agents That Act on Connected Data.
- Every step-free route through a station is a path through lifts, walkways and fare gates. Give each lift a capacity of one and max-flow is the number of lifts that must fail before a platform is cut off. One means a single point of failure.
- Today, with lift 961 closed at Alewife, lift 813 is newly the only lift for the Red Line, Track 1 and Track 2 platforms. No alert says that; the graph does.
- 74 of the single-lift platforms are at stations with two or more lifts: counting lifts is not enough.
- Every number is a certified FalkorDB result, cross-checked against NetworkX, and every agent answer cites the exact Cypher it ran.
![]() Command. Every open lift alert, what it means, and a map of the network coloured by MBTA line. |
![]() Station x-ray. Alewife's levels in 3D, every platform's independent lift routes, and the table twin for screen readers. |
![]() One closure, end to end. Lift 869 closes at Downtown Crossing; the agents find 891 is now the last lift; a person approves; the issue is filed. |
![]() Answers that expire. A live answer flips to out of date the moment a lift it read changes. |
![]() Walk through Alewife. A seated first-person walkthrough built from LastLift's own data: lift 961 is closed, then lift 813 fails in front of you. Play it, or watch it play itself. |
|
A recorded run from web/public/replay/whatif-813.json: a language model reaching LastLift's tools only through MCP, every claim in its answer checked against what the tools returned. The website replays it on the landing page and lets you verify its hash chain in the browser.
Q What happens if lift 813 fails too?
1 resolve_place query: lift 813 via MCP · 36 ms
→ lift 813 (Alewife Elevator 813 (Platform to main lobby))
2 what_if_close facility_ids: [813] via MCP · 1070 ms
→ simulated closing 813: 3 more platforms lose step-free access; 0 lifts become the last lift
(simulation on a copy; live state unchanged)
3 step_free_route from_station: place-alfcl · to_platform: Red Line
also_closed: [813] via MCP · 67 ms
→ no step-free route in the open-data model
✓ grounded: every claim checked against the tool results
A If lift 813 fails, the Alewife Red Line platform loses step-free access in the open-data model.
No step-free route exists in the open-data model to board that platform via other stations.
Check mbta.com/alerts/access.
And the agents act. These two issues were filed by the Dispatcher, after a person approved each proposal, as lift dossiers with a drawing of the station's step-free graph:
| Issue | Why the graph proposed it | |
|---|---|---|
| Protect | lastlift-ops#1: lift 891, Downtown Crossing | Still working, but with 869 closed it is the last lift for the Oak Grove and Alewife platforms (minimum cut {869, 891}) |
| Repair | lastlift-ops#2: lift 885, Revere Beach | Closed, and the Wonderland platform has no other step-free route |
The agents' memory lives in the graph: every chat run is written as (:Run)-[:CALLED]->(:ToolCall)-[:READ]->(:Lift) with its (:Answer). When a lift changes state, the same step marks every answer that read it stale, and only those; the chat flips from "✓ Still true" to "⚠ Out of date", with Ask again. The memory, the operations agents and every tool: docs/AGENTS.md.
| The alert | LastLift |
|---|---|
| "Revere Beach Elevator 885 is unavailable." | The Wonderland platform loses step-free access in the open-data model. To board step-free: lift 884, ride the Blue Line to Beachmont, change platforms there step-free, ride back (about 17 minutes). There is no backup avoiding lifts 884, 886 and 887. |
| "Alewife Elevator 961 is unavailable." | No platform loses access, but lift 813 is now the only lift for three platforms. Protect it. |
| "Elevator 782 at Worcester is unavailable." | Cannot assess: MBTA publishes no station layout for Worcester. |
FalkorDB 4.22's algo.maxFlow can return a wrong value on some networks: on an 11-edge reproduction it returns 300 where the true max-flow is 453 (reported by the Chokepoint project; reproduced in docs/falkordb-maxflow-note.md). So no FalkorDB flow is trusted on its own:
- each result is certified by checking that the returned flow is a feasible flow whose residual graph has no augmenting path, and repaired with an exact Dinic run if it is not;
- the minimum cut names the lifts; each lift on a single route is removed in turn to find every single point of failure;
- an independent checker validates the k lift-disjoint routes as a proof.
On LastLift's network: 734 max-flow calls, 734 certified on the first pass, 0 repaired, 734 equal to NetworkX, 333 route proofs accepted and 0 rejected. scripts/verify_claims.py re-derives every headline number from the committed snapshot with NetworkX alone.
| Layer | What it is | Where |
|---|---|---|
| Data | MBTA GTFS-Pathways, V3 alerts, MassDOT alert archives and LAMP | ingest/, data/ |
| Proof | A pinned snapshot, the flow certifier, an independent NetworkX re-derivation | lastlift/flow.py, lastlift/checker.py, scripts/verify_claims.py |
| Graph | FalkorDB 4.22: the step-free network, closures as relationship swaps, what-ifs on GRAPH.COPY, memory and provenance |
lastlift/queries.py, lastlift/memory.py |
| Tools | Thirteen read-only tools over fixed, parameterised Cypher | lastlift/tools.py |
| MCP and API | /mcp (public, read-only), /mcp-ops (approvals), REST /v1, SSE /api/ask |
lastlift/mcp_server.py, lastlift/main.py |
| Agents | A grounded chat agent with a provider chain; Monitor, Analyst and Dispatcher | lastlift/agent/, lastlift/agents/ |
| Surfaces | React and Vite on Vercel (pages open from a snapshot FalkorDB computed; live answers from the graph), the 3D walkthrough, lastlift-ops |
web/, web/public/concepts/alewife.html |
| What | Result | Details |
|---|---|---|
| The agent with and without the graph (same model, same 30 pre-registered questions) | right lift and platform ids in 23/24 answers with the graph, 5/24 without | Evaluation |
| Checked against MBTA's own alert text, labelled blind by the author | live alerts: 6 of 8 agree, 0 disagree; archived closures: 16 of 17 agree | Evaluation |
| Every reported closure since 2024, replayed through the graph | 251 platform-directions lost step-free access; 95% of the lost hours trace to a single lift | Evaluation |
| The whole network | 69 lifts can each cut a platform off; only 15 can cut a station off from the network | Network |
| Does it travel? | the same rules on 14 open feeds: 2,884 of 2,884 FalkorDB results equal an exact solver | Network |
| Quality, security and accessibility | the tests, the safeguards, axe in light and dark, and the defects we found in the published data | Quality |
Point any MCP client that supports streamable HTTP at the endpoint (Cursor, VS Code, desktop agents):
{ "mcpServers": { "lastlift": { "url": "https://lastlift-api.onrender.com/mcp/" } } }Thirteen read-only tools with output schemas, plus resources (station graphs as Mermaid, lift dossiers, open incidents) and prompts. The free host sleeps when idle, so the first call may take a minute. Every tool, resource and curl example: docs/AGENTS.md.
On the site: the Judge's tour walks through everything in seven stops, each one the real thing. From a terminal:
POST https://lastlift-api.onrender.com/v1/tools/platform_redundancywith{"station_id": "place-alfcl"}: Alewife, with the lift that became the last lift and why.POST .../v1/tools/what_if_closewith{"facility_ids": ["813"]}: close a working lift on a copy of the graph; three platforms lose step-free access; the live state is untouched.POST .../api/askwith{"question": "Can I get step-free onto the Wonderland-bound platform at Revere Beach right now?"}: the agent's answer and every tool call it made, via MCP.GET .../v1/queries: every Cypher statement LastLift runs.python scripts/smoke.py --prodchecks all of the above from your machine.
| Rule 10 requirement | Where |
|---|---|
| Public source-code repository | this repository |
| README with setup instructions and the graph data model | Run it yourself, The graph data model |
| The Cypher queries or graph algorithms the product depends on | The Cypher and the algorithms (generated from lastlift/queries.py) |
| A demo video | youtu.be/XLDNu4cGaN8 (2:32, narrated by the author, English subtitles) |
| A live deployment, or complete local setup | lastlift.vercel.app, the API at lastlift-api.onrender.com/v1/docs, and Run it yourself |
| The track | Track 1, Agents That Act on Connected Data (how it fits) |
Track 1, Agents That Act on Connected Data.
- The agents act on the graph. Operations agents work every closure: the Monitor reads live MBTA alerts, the Analyst picks a playbook from certified max-flow, the Dispatcher proposes only while the graph is unchanged, and a person's approval files a real ticket (lastlift-ops). The chat agent answers only through the tools, over MCP, with every claim checked.
- They act on context that is still true. The agents' memory lives in FalkorDB: every run, tool call and answer is a node with what it read; an answer is marked stale the moment a lift it read changes (Answers that know when they are out of date, docs/AGENTS.md); the agents coordinate by writing assessments and proposals into the graph, guarded by its state version, never by messaging each other.
Rule 3 (the graph does real work): see Turn FalkorDB off. Rule 6 (data you may use): public MBTA open data only. Rule 11 (AI disclosure): AI assistance.
Every node in a station complex carries cx; step-free walkways, gates and lift-access edges have capacity 1,000,000 and lift edges capacity 1, so a platform's max-flow from its station's street node is its number of lift-disjoint step-free routes. A closed lift's edge is retyped SF_CLOSED, which takes it out of every algorithm at once. Model rules, including five evidence-backed overrides of the published data, are in docs/model_rules.md.
Graph counts: every node label and relationship in the snapshot
| Node labels | Count |
|---|---|
Lift |
42 |
Node:Entrance |
330 |
Node:Generic |
1,924 |
Node:Lift |
202 |
Node:LiftOut |
202 |
Node:Platform |
388 |
Node:Street |
89 |
Platform |
312 |
Station |
276 |
| Relationship | Count |
|---|---|
HAS_ENTRANCE |
330 |
HAS_LIFT |
244 |
HAS_PLATFORM |
388 |
RIDE |
650 |
SF |
9,248 |
STEP |
1,069 |
Step-free (SF) edge kind |
Count |
|---|---|
gate |
274 |
lift |
202 |
lift_access |
856 |
street |
424 |
walk |
7,492 |
Snapshot 2026-10-15, GTFS feed "Fall 2026, 2026-10-14T18:47:38+00:00, version D", seed sha256 8277887f216a…: 3,765 nodes, 11,929 relationships.
The Cypher and the algorithms: every statement LastLift runs
FalkorDB algorithms used: algo.maxFlow (redundancy, certified), algo.WCC (load verification), full-text index (station search), GRAPH.COPY (what-ifs). Every statement LastLift runs, generated from lastlift/queries.py:
| Query | Cypher |
|---|---|
Q_CLEAR_DEPENDS_NOW |
MATCH (p:Node:Platform {cx:$cx})-[d:DEPENDS_NOW]->() DELETE d |
Q_CLEAR_INCIDENTS |
MATCH (i:Incident) DETACH DELETE i |
Q_CLOSED_AT_COMPLEX |
MATCH (l:Lift {cx:$cx})-[:SF_CLOSED]->() RETURN collect(DISTINCT l.facility_id) |
Q_CLOSED_FACILITIES |
MATCH (i:Lift)-[:SF_CLOSED]->() RETURN collect(DISTINCT i.facility_id) |
Q_CLOSE_LIFTS |
UNWIND $fids AS fid MATCH (i:Lift {facility_id:fid})-[r:SF {kind:'lift'}]->(o) CREATE (i)-[c:SF_CLOSED]->(o) SET c = properties(r), i.closed = true DELETE r RETURN count(c) |
Q_COMPLEX_FLOW_EDGES |
MATCH (a:Node {cx:$cx})-[e:SF]->(b:Node) RETURN e.eid, a.uid, b.uid, e.cap, e.kind, e.facility_id |
Q_COMPLEX_PLATFORMS |
MATCH (p:Node:Platform {cx:$cx}) RETURN p.stop_id, collect(p.uid), head(collect(p.name)), head(collect(p.wheelchair_boarding)), head(collect(p.platform_name)) |
Q_COMPLEX_STREETS |
MATCH (s:Street {cx:$cx}) RETURN collect(s.uid) |
Q_CONDITIONAL |
MATCH (a:Node:Lift)-[c:CRITICAL_PAIR]->(b:Node:Lift) WHERE c.scope = 'network' RETURN a.facility_id, b.facility_id, b.station_id, c.journeys, c.same_station ORDER BY c.journeys DESC, a.facility_id, b.facility_id |
Q_CREATE_DEPENDS |
UNWIND $rows AS r MATCH (p:Node:Platform) WHERE p.stop_id = r.stop MATCH (l:Node:Lift {{facility_id:r.fid, cx:p.cx}}) MERGE (p)-[d:{rel} {{dir:r.dir}}]->(l) |
Q_CRITICAL_PAIRS |
MATCH (a:Node:Lift)-[c:CRITICAL_PAIR]->(b:Node:Lift) WHERE c.scope = 'platform' RETURN a.facility_id, b.facility_id, a.station_id, c.n, c.dirs ORDER BY c.n DESC, a.facility_id, b.facility_id |
Q_DOMINATES_FOR |
MATCH (l:Node:Lift {facility_id:$fid})-[:DOMINATES_FOR]->(s:Station) RETURN collect(DISTINCT s.uid) |
Q_LIFT_BLAST |
MATCH (l:Node:Lift) WHERE l.facility_id IN $fids WITH l.facility_id AS fid, head(collect(l)) AS l RETURN fid, l.name, l.station_id, l.od_cut_base, l.od_cut_off, l.od_slow_base, l.detour_max_s, l.detour_mean_s ORDER BY fid |
Q_LIFT_COMPLEXES |
MATCH (l:Node:Lift) WHERE l.facility_id IN $fids RETURN collect(DISTINCT l.cx) |
Q_LIFT_HISTORY |
MATCH (l:Lift {facility_id:$fid})-[:CLOSED_IN]->(i:Incident) RETURN DISTINCT i.alert_id, i.effect, i.header, i.started, i.first_seen, i.last_seen, i.ongoing, i.ended_by, i.versions ORDER BY i.first_seen |
Q_LIFT_INCIDENT_COUNT |
MATCH (l:Lift {facility_id:$fid}) OPTIONAL MATCH (l)-[:CLOSED_IN]->(i:Incident) RETURN count(DISTINCT i) |
Q_LIFT_INFO |
MATCH (l:Lift {facility_id:$fid}) RETURN l.facility_id, l.name, l.short_name, l.station_id, l.cx, l.modelled, coalesce(l.closed, false) LIMIT 1 |
Q_LIFT_RECORD |
MATCH (l:Lift) WHERE l.facility_id = $fid WITH l ORDER BY l.uid LIMIT 1 OPTIONAL MATCH (o:Outage)-[:OF]->(l) RETURN l.avail_12m, l.closed_h_12m, l.outages_12m, l.mttr_h, l.single_svc_h, count(o), sum(o.hours), max(o.end), sum(CASE WHEN o.imputed THEN 1 ELSE 0 END) |
Q_LOST_NOW |
MATCH (p:Node:Platform) WHERE (p.k_board = 0 AND (p.k_board_base > 0 OR p.at_grade_board)) OR (p.k_exit = 0 AND (p.k_exit_base > 0 OR p.at_grade_exit)) RETURN p.stop_id, p.name, p.platform_name, p.station_id, p.k_board, p.k_exit, p.deps_day |
Q_MAXFLOW |
MATCH (s:Node) WHERE s.uid IN $src WITH collect(s) AS S MATCH (t:Node) WHERE t.uid IN $dst WITH S, collect(t) AS T CALL algo.maxFlow({sourceNodes:S, targetNodes:T, relationshipTypes:['SF'], capacityProperty:'cap'}) YIELD edges, edgeFlows, maxFlow RETURN [e IN edges | e.eid], edgeFlows, maxFlow |
Q_META |
MATCH (m:Meta) RETURN m.snapshot_id, m.seed_sha256 LIMIT 1 |
Q_MODELLED_LIFTS |
MATCH (l:Node:Lift) WHERE l.facility_id IN $fids RETURN collect(DISTINCT l.facility_id) |
Q_NETWORK_SPOF |
MATCH (l:Node:Lift) WHERE l.od_cut_base > 0 WITH l.facility_id AS fid, head(collect(l)) AS l RETURN fid, l.name, l.station_id, l.od_cut_base, l.od_cut_off ORDER BY l.od_cut_base DESC, fid |
Q_NEWLY_LAST |
MATCH (p:Node:Platform)-[n:DEPENDS_NOW]->(l:Lift) WHERE NOT (p)-[:DEPENDS_ON {dir:n.dir}]->(l) RETURN l.facility_id, l.name, l.station_id, collect(DISTINCT p.stop_id + ' ' + n.dir) |
Q_PAIR_OVERLAP |
MATCH (x:Outage)-[:OF]->(a:Lift {facility_id:$a}) MATCH (y:Outage)-[:OF]->(b:Lift {facility_id:$b}) WHERE x.start < y.end AND y.start < x.end RETURN count(*), collect([CASE WHEN x.start > y.start THEN x.start ELSE y.start END, CASE WHEN x.end < y.end THEN x.end ELSE y.end END])[0..20] |
Q_PLATFORM_CUTS |
MATCH (p:Node:Platform {cx:$cx}) RETURN p.stop_id AS stop, head(collect(p.mincuts_board)), head(collect(p.mincuts_exit)), head(collect(p.rel_counts_board)), head(collect(p.rel_counts_exit)), head(collect(p.rel_relevant_board)), head(collect(p.rel_relevant_exit)) ORDER BY stop |
Q_PRIORITY |
MATCH (p:Node:Platform)-[d:DEPENDS_NOW]->(l:Lift) OPTIONAL MATCH (p)-[o:DEPENDS_ON]->(l) WHERE o.dir = d.dir WITH l, p, d, count(o) AS before WITH l, count(DISTINCT p.stop_id + d.dir) AS deps, sum(p.deps_day) AS departures, toInteger(sum(CASE WHEN before = 0 THEN 1 ELSE 0 END)) AS newly OPTIONAL MATCH (l)-[:CLOSED_IN]->(i:Incident) WITH l, deps, departures, newly, count(DISTINCT i) AS incidents RETURN l.facility_id, l.name, l.station_id, coalesce(l.closed, false), deps, newly, departures, incidents |
Q_REACHES_FROM |
MATCH (a:Station {uid:$sid})-[r:REACHES]->(b:Station) RETURN b.uid, b.name, r.base_s ORDER BY r.base_s, b.uid |
Q_REOPEN_LIFTS |
MATCH (i:Lift)-[r:SF_CLOSED]->(o) WHERE NOT i.facility_id IN $fids CREATE (i)-[c:SF]->(o) SET c = properties(r), i.closed = false DELETE r RETURN count(c) |
Q_ROUTE_CHECK_RIDE |
UNWIND $pairs AS p MATCH (a:Platform {uid:p[0]})-[e:RIDE]->(b:Platform {uid:p[1]}) RETURN a.uid, b.uid, e.wt |
Q_ROUTE_CHECK_SF |
UNWIND $pairs AS p MATCH (a:Node {uid:p[0]})-[e:SF]->(b:Node {uid:p[1]}) RETURN a.uid, b.uid, e.wt |
Q_ROUTE_EDGES |
MATCH (a)-[e:SF|RIDE]->(b) RETURN a.uid, b.uid, e.eid, type(e), e.kind, e.wt, e.facility_id, e.routes |
Q_ROUTE_NODES |
MATCH (n) WHERE n:Node OR n:Platform RETURN n.uid, labels(n), n.name, n.station_id, n.platform_name, n.short_name |
Q_SET_PLATFORM_NOW |
MATCH (p:Node:Platform) WHERE p.stop_id = $stop SET p.k_board = $kb, p.k_exit = $ke, p.at_grade_board_now = $ab, p.at_grade_exit_now = $ae, p.cut_board_now = $cb, p.cut_exit_now = $ce |
Q_STATION |
MATCH (s:Station {uid:$sid}) RETURN s.uid, s.name, s.cx, s.has_pathways, s.has_street |
Q_STATION_EDGES |
MATCH (a:Node {cx:$cx})-[e]->(b:Node) WHERE type(e) IN ['SF','SF_CLOSED','STEP'] RETURN e.eid, a.uid, b.uid, type(e), e.kind, e.facility_id, e.pathway_ids |
Q_STATION_NODES |
MATCH (n:Node {cx:$cx}) RETURN n.uid, labels(n), n.name, n.platform_name, n.facility_id, n.unit, coalesce(n.closed, false), n.stop_id, n.wheelchair_boarding |
Q_STATION_PLATFORMS |
MATCH (p:Node:Platform {cx:$cx}) RETURN p.stop_id AS stop, collect(p.uid), head(collect(p.name)), head(collect(p.platform_name)), min(p.k_board), min(p.k_exit), min(p.k_board_base), min(p.k_exit_base), head(collect(p.at_grade_board)), head(collect(p.at_grade_exit)), head(collect(p.at_grade_board_now)), head(collect(p.at_grade_exit_now)), head(collect(p.cut_board_now)), head(collect(p.cut_exit_now)), coalesce(head(collect(p.no_accessible_entrance)), false) ORDER BY stop |
Q_STATION_SEARCH |
CALL db.idx.fulltext.queryNodes('Station', $q) YIELD node, score RETURN node.uid, node.name, node.has_pathways, node.has_street, score ORDER BY score DESC, node.uid LIMIT 5 |
Q_STATION_UNMODELLED_LIFTS |
MATCH (l:Lift) WHERE l.station_id = $sid AND NOT l:Node RETURN l.facility_id, l.name ORDER BY l.facility_id |
Q_WRITE_INCIDENTS |
UNWIND $rows AS r CREATE (i:Incident) SET i = r.p WITH i, r UNWIND r.fids AS fid MATCH (l:Lift {facility_id:fid}) MERGE (l)-[:CLOSED_IN]->(i) |
Requirements: Python 3.14 (3.12+ works), Node 24, a FalkorDB 4.22 instance (Docker falkordb/falkordb-server:v4.22.0, or FalkorDB Cloud).
python -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt # Windows: .venv\Scripts\python
.venv/bin/python dev.py doctor # checks Python, FalkorDB and .env key names
.venv/bin/python -m lastlift.seed --verify # load the committed snapshot into FalkorDB
.venv/bin/python -m uvicorn lastlift.main:app --port 8000 # API, MCP at /mcp/, agent at /api/ask
cd web && npm ci && npm run dev # website (VITE_API_BASE=http://localhost:8000)No API key is needed: without an LLM key the agent answers through its deterministic template. The website shows the frozen demo alerts by default; its Live toggle switches to the live scope, which follows the MBTA's current alerts when the server runs with LIVE_FEED=on (polled every LIVE_POLL_S seconds; GET /v1/live reports the last poll and marks it stale after five minutes) and otherwise uses the latest committed alert harvest. Configuration comes from environment variables or a .env file one level above the repository (names in .env.example); secrets are never committed.
Reproduce every number: python scripts/analyze.py (the max-flow sweep in FalkorDB), python scripts/verify_claims.py (an independent NetworkX re-derivation), python scripts/eval.py (the validation), python scripts/readme_sync.py --check (this README against claims.json).
- Fare gates are passable for riders who need step-free access; only reaching the platform is modelled, not boarding the train; widths and surfaces are not in the data.
- MBTA publishes no level data; the station drawings infer levels from lift and stair connections.
- Where MBTA publishes no station layout (many commuter-rail stations), LastLift says it cannot assess.
- Lift status comes from MBTA's alerts, which can differ from a lift's actual state. The judged demo uses alerts frozen on 15 Oct 2026.
- Five evidence-backed overrides correct the published layout, and a self-audit of the feed found more: docs/QUALITY.md.
Code: MIT. Data: MBTA GTFS (feed "Fall 2026, 2026-10-14T18:47:38+00:00, version D") and MBTA V3 API alerts, Massachusetts Department of Transportation open data, used under the MassDOT Developers License Agreement. LastLift is unofficial and not affiliated with or endorsed by the MBTA or MassDOT.
This project was built with the help of an AI coding assistant. Design decisions and the final review are the author's own. In the demo video, the narration is the author's own voice and every number and screen comes from the project's data and renders; a few short background shots were generated with Kling. The validation keys (validation/gold_live_author.csv and validation/gold_history_author.csv) were labelled by the author, blind, from MBTA's alert text only; a second label set made by the AI assistant (validation/gold_live.csv) is kept only to report labeller agreement.
The algo.maxFlow defect reproduction is credited to the Chokepoint project. Built on FalkorDB, FastAPI, FastMCP, NetworkX, React, Vite, deck.gl, MapLibre and three.js. The walkthrough's materials and lighting are CC0 scans from Poly Haven.
Found a problem with a route or a station? Report an accessibility problem with LastLift (every field is optional). Prefer not to post publicly? Email mrslvt.dev@gmail.com. How LastLift handles people's experiences is set out in docs/ETHICS.md.




