What the agent does
A Phoenix router says most of what an inventory needs, if it is read as code rather than as a list. In an application like plausible/analytics, lib/plausible_web/router.ex is 764 lines with 241 route macros, organised into pipelines and scopes: :external_api accepts JSON and nothing else, :api adds fetch_session and PlausibleWeb.AuthPlug, :internal_stats_api adds AuthorizeSiteAccess, NoRobots and InternalStatsApiVersion, :public_api accepts JSON and is paired on each scope with PlausibleWeb.Plugs.AuthorizePublicAPI, and :plugins_api adds OpenApiSpex.Plug.PutApiSpec with the spec module PlausibleWeb.Plugins.API.Spec. The agent runs mix phx.routes for the flat list and then reads the file to recover what the flat list drops: the pipe_through chain, the assigns such as api_scope: "stats:read:*" and api_context: :site, the private flags such as allow_consolidated_views, and the on_ee do blocks that exist only in the enterprise build.
Each scope becomes a section of the inventory. /api/v1/stats has four ExternalStatsController actions (realtime_visitors, aggregate, breakdown, timeseries) behind the public API plug with scope stats:read:*; /api/v2/query is the ExternalQueryApiController, and /api/docs/query is the same action behind :docs_stats_api with a session instead of a key; /api/v1/sites is ExternalSitesController from extra/lib/plausible_web/controllers/api/, split between sites:read:* and sites:provision:* scopes and wrapped in on_ee; /api/plugins/v1 is the Plugins API with SharedLinks, Goals, CustomProps, TrackerScriptConfiguration and, on EE, Funnels, authenticated by AuthorizePluginsAPI with a token; /api/stats/:domain/* is the internal stats API the dashboard uses; and the unauthenticated group under /api holds POST /event, the ingest endpoint, GET /error, the legacy GET /health and the /system/health/live and /system/health/ready probes from SystemController, plus the Paddle and Postmark webhooks.
For the authentication column the agent reads the plugs, not just their names. lib/plausible_web/plugs/authorize_public_api.ex documents that api_scope is matched by prefix against the key's scopes, that stats:read:* and sites:read:* are implicit for every key, that a stats:read:* route also needs a valid site_id, and that every request is rate limited per key with an hourly limit through PlausibleWeb.Api.RateLimit; authorize_plugins_api.ex extracts a token and answers through PlausibleWeb.Plugins.API.Errors.unauthorized. Those facts go into the row for each route, so a reader sees not only "API key" but which scope, which context, and what happens over the limit.
Finally it checks the source against the documentation the app publishes: GET /api/plugins/spec/openapi renders the OpenApiSpex spec, GET /api/docs/query/schema.json is the stats query schema, and the agent lists routes present in the router but absent from either and vice versa. It can start the app with mix setup against the PostgreSQL and ClickHouse the test alias expects and probe the unauthenticated routes to record status codes, and it says in the inventory which rows were probed. It changes no code and opens no pull request.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Write endpoints.md for this Phoenix application. Run mix phx.routes, then read lib/plausible_web/router.ex in full and every pipeline and plug it names under lib/plausible_web/plugs, plus the on_ee blocks and the controllers under extra/. For every route under /api, /api/v1, /api/v2, /api/docs, /api/plugins, /.well-known and /system, record method and path, controller and action with file:line, the pipelines it passes through, the authentication it requires (session, API key with which api_scope and api_context, plugins token, or none), the rate limit that applies, whether it is enterprise-only, and the private flags set on its scope. Group the inventory by scope. Compare the router against the Plugins API OpenAPI spec and the query schema endpoint and list the differences. If PostgreSQL and ClickHouse can be started in the sandbox, run mix setup, start the server and probe the unauthenticated routes, recording the status codes and marking which rows were probed. Do not change code, push or open a pull request.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is endpoints.md as an artifact on the session. It starts with the pipelines and what each plug in them does, then one table per scope: method and path, controller.action with a router.ex line, pipelines, authentication with the scope string, rate limit, an EE marker, and a probed status code where one was taken. A closing section lists routes in the router that the OpenAPI spec does not describe and spec paths with no route, and the commands the agent ran.
The transcript at GET /o/{org}/sessions/{id}/messages holds the mix phx.routes output, every file read and, if the server was started, each probe with its response, so a reader can check any row against the source. The repository is not changed and the session lists no pull request.
Before relying on the inventory, a reviewer spot-checks the enterprise rows against an EE build, confirms the implicit scopes in AuthorizePublicAPI match what the key management UI grants, and decides whether the legacy GET /api/health route, which the router comment marks for removal, should still be listed as public.
Run it on every router change on main
Create an automation in the Project with this prompt and a github trigger on pull_request.closed.merged, with branch_patterns matching your default branch. The trigger adds the pull request event after the prompt; the agent regenerates the full inventory rather than a diff, and the artifact on each run's session is the current one. A config.labels filter such as api keeps the automation to pull requests that touch the router.
From your own server, the same run starts with POST /o/{org}/sessions and this body, with a request_id, one UUID kept per merge commit, so a retry returns the first run. Read the artifact from the session or from the run.completed webhook's session_url.
The use case this job follows, with its automation and trigger: Produce a report or file, no repo.
Why run it on Rush?
An inventory of this router means reading 764 lines of scopes with nested pipe_through chains, two auth plugs with their own scope rules, enterprise-only blocks and controllers that live under extra/, and a server that needs PostgreSQL and ClickHouse before a single route can be probed. On Rush the agent does that in its own sandbox and the commands and their output stay with the session, so a row that says "probed: 401" points at the request that produced it.
The result is a cited document rather than a memory of the router, and the same request runs against several branches or several Phoenix services at once, each in its own sandbox. When the router changes, the next run replaces the document and the previous one stays on its session for comparison.
Frequently asked questions
Why read the router when mix phx.routes already lists everything?
mix phx.routes prints method, path and controller but not the assigns that set api_scope and api_context, the private flags, the pipeline order or the on_ee blocks. Those decide who can call a route, and the agent reads them from router.ex and the plugs under lib/plausible_web/plugs.
Does it include enterprise-only routes?
Yes, marked as such. Routes inside on_ee do blocks, like /api/v1/sites and the Plugins API funnels, are listed with an EE column so a community edition deployment knows they are absent.
Will it include the dashboard's internal endpoints?
The internal stats API under /api/stats/:domain and the segments and annotations routes are in the inventory as session-authenticated rows, because they are endpoints the running app serves. The report labels them internal so they are not mistaken for the public API.
Can it probe routes that need a key?
Only if you give it one through a Project secret, and the prompt here does not ask for that. Without a key it probes the unauthenticated routes and records the 401 the public API plug returns elsewhere, which is itself a useful row.