Hex.pm Package Rankings & Download Change Monitor
Pricing
from $0.60 / 1,000 package results
Hex.pm Package Rankings & Download Change Monitor
Monitor Hex.pm package rankings. Track 90-day downloads, rank and version via the official Hex API.
Pricing
from $0.60 / 1,000 package results
Rating
0.0
(0)
Developer
Vadim Bezrukov
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
See which Elixir/Erlang/Gleam packages sit in the official Hex.pm top-N for
90-day downloads, whether rank, downloads or version moved since your last
successful check, and whether a watched package entered or left that window.
The Actor reads the published Hex HTTP API. It does not scrape HTML package
pages, call /diff/, walk the whole catalog, or emit owner emails.
Use it to refresh an Elixir, Erlang or Gleam package dashboard or alert on
changes in packages you follow. The default snapshot returns the current
top-20 packages, one query_status and one run_summary. Export JSON or CSV,
or pass the Dataset to your automation.
To start collecting history now, choose monitor + changesOnly on your
first run and keep the same stateNamespace on later runs. The first monitor
returns BASELINE; the second compares against that saved observation.
Snapshots do not establish a monitor baseline. Hex provides no native ranking
history through these endpoints, so observations begin when you start running
the Actor. Default rank is 90-day recent_downloads; all-time
total_downloads is a separate ranking.
By using this Actor you agree to the Hex Terms of Service. This Actor uses the published Hex HTTP API as a third-party product under those Terms and collects public package metadata only.
Choose your workflow
Start with a prepared example:
Each example opens with its saved input and a preview of the output fields.
| Goal | Settings | What you get |
|---|---|---|
| Export today's official top-N | snapshot + all | One package_observation per package, all BASELINE |
| Re-check the same window next week | monitor + changesOnly | NEW, UPDATED, RANK_UP, RANK_DOWN, EXITED, REAPPEARED |
| Watch named packages | watchlist | Observations for those names; rank only if they sit in this topN window |
- Export top packages from official
sort=recent_downloadsorder, default top-20. First run writesBASELINErows and onequery_status. - Monitor rank changes on the same query later. Keep
stateNamespacestable. Later runs returnNEW,UPDATED,RANK_UP,RANK_DOWN,EXITEDorREAPPEARED. - Watch named packages such as
jason,phoenixandtelemetry. A name missing from this topN window is unranked, notNOT_FOUND, whileGET /api/packages/{name}returns 200. A JSON HTTP 404 is item-levelNOT_FOUND.
BASELINE identifies an initial observation. In monitor mode it is saved for
the next comparison; in snapshot mode it is an export only. It does not mean
the package was published today.
Quick start
{"mode": "snapshot","rankingMetric": "recent_downloads","topN": 20}
On the 2026-09-13 probe, rank 1 was jason and rank 2 was telemetry. Then
schedule the same window:
{"mode": "monitor","outputMode": "changesOnly","stateNamespace": "top20","rankingMetric": "recent_downloads","topN": 20}
Keep stateNamespace stable for a given watch. Snapshot mode does not read or
write history. snapshot + changesOnly is rejected before any Hex request.
A watchlist accepts phoenix or https://hex.pm/packages/phoenix. http://
URLs and other hosts are INVALID_INPUT without a fetch. Rank is the 1-based
index in official list page 1, including holes: a non-object at position 2 does
not make the next package rank 2. A watchlist name outside that window is
unranked (rank=null). First-seen unranked names stay rank=null. EXITED is
used when a prior successful observation of that id had an integer rank and then
left this window for two complete successful runs. NOT_FOUND is only used when
the package endpoint returns JSON HTTP 404. A complete, valid ranking plus
verified package responses produces a SUCCESS query. A verified [] is NO_RESULTS on a first
snapshot or first monitor, including with a watchlist; a later monitor with
last-good active or pending_exits treats [] as PARTIAL /
EMPTY_RANKING_LIST and does not emit EXITED.
Hex API vs this Actor API
- Hex API:
GET https://hex.pm/api/packages?sort=recent_downloads&page=1(default) and, for watchlist ids absent from the downloaded first page,GET https://hex.pm/api/packages/{name}. Public JSON, no token.total_downloadsmaps to officialsort=downloads. - Actor API: Apify input, Dataset rows (
package_observation,query_status,run_summary), and optional last-good state in this Actor's key-value store.
The Actor never requests HTML /packages/{name} (Hex ToS §6 scrape ban), never
requests /diff/ (robots Disallow), and never paginates page=2+ to recover a
global rank. It never emits owners or email.
What changed, and what did not
Monitor diffs last successful state against current successful state.
Keep stateNamespace, rankingMetric, topN and the set of watchlist names
unchanged to continue the same comparison. Changing any of these starts a
separate baseline. Reordering the same watchlist does not reset history.
- First successful monitor, including
changesOnly, emitsBASELINE. - A new id after that is
NEW. An integer rank move isRANK_UP/RANK_DOWNeven when 90-day or all-time downloads also moved;changed_fieldsstill lists every fingerprint field that changed.UPDATEDis used when rank did not move but downloads orlatest_versiondid. Day and week download counters are observation fields only; they are not in the fingerprint, so day/week changes alone do not emitUPDATED. Changes to the 90-day or all-time counters do: a daily check can charge for updated packages even when every rank stays the same. - An id missing from two different sequential successful complete windows
becomes
EXITEDonly if a prior successful observation of that id had an integer rank. The first miss ispending_exitsonly. A transport error,OUTPUT_LIMIT, or mass-removal guard does not advance that counter. First-seen unranked watchlist names stayrank=nulland do not EXIT. A package JSON HTTP 404 isNOT_FOUND, notEXITED. EXITEDmeans "not in this query's successful top-N / watchlist response". It does not mean the package was unpublished from Hex.- If a successful top-N window keeps under 50% of the previous ids at the
same
topN, the query isPARTIAL(mass-removal guard). Named watchlists skip that guard. Pending exits are not applied on mass-removal. - Broken stored state is
STATE_INCOMPATIBLEfor that query, not a silent new baseline. The run fails; changestateNamespaceto start a new baseline.
Failure semantics
NO_RESULTS is a verified list [] with no last-good snapshot to protect. It
is never a transport failure, and it is not "this watchlist id is unranked in a
populated list". A later monitor that already has last-good active or
pending_exits and then sees [] is PARTIAL / EMPTY_RANKING_LIST,
including for named watchlists, so a blank page cannot manufacture EXITED.
HTML when JSON was expected, truncated JSON, a list body over 2 MiB
(SOURCE_LIMIT), HTTP 5xx, or 429 after retry are FAILED. An unparseable
object inside topN is PARTIAL / INVALID_LIST and does not move state.
Unparseable list slots keep distinct source_id values (hexpm:invalid-rank:{n})
and matches_query=false. A successful list GET followed by failed watchlist
package GETs is PARTIAL / PARTIAL_WATCHLIST (not a run-level source failure)
and does not move state. Missing packages are JSON HTTP 404; that is item-level
NOT_FOUND, not an empty ranking. Invalid input writes one query_status
(INVALID_INPUT) and a run_summary whose status is INVALID_INPUT (not a
default snapshot), then fails the run before any Hex request. A resumed run
keeps that failed outcome. If Dataset rows were already written but billing
could not finish, the query is PARTIAL / SPEND_LIMIT; last-good state is
still saved so the next monitor does not replay those charges.
A non-empty list shorter than topN is PARTIAL / INCOMPLETE_LIST:
there is no verified complete window to compare. Repeated package identities
anywhere on list page 1 are PARTIAL / DUPLICATE_PACKAGE; the Actor does not
deduplicate or renumber an ambiguous ranking. Both cases emit no package rows,
charge no package or window events, and keep last-good state and pending exits
unchanged. A malformed package detail or a detail identifying a different
package is an item-level FAILED under the requested name and makes the query
PARTIAL; it cannot update state or confirm an exit.
maxPackageRows defaults to 150 so a topN of 100 plus EXITED extras can fit.
It never truncates a window: if the package rows do not fit, you get PARTIAL /
OUTPUT_LIMIT and the previous state is kept.
Pricing
The configured rates are listed below. The Pricing tab is authoritative.
At the default 512 MB and FREE/BRONZE event rates:
| Example | Bill per run |
|---|---|
| Export top 20 | $0.02005 |
| Compare 3 existing named packages | $0.00305 |
| First monitor baseline or an unchanged monitor | $0.01005 |
| Repeat monitor with 20 changed packages | $0.03005 |
| Export top 100 | $0.10005 |
A 30-run month of top-20 monitoring is $0.30150 if every run is unchanged,
or $0.90150 if each run has 20 billable changes. These are calculated examples,
not forecasts; confirmed exits can add change rows. The monitor formula is
start + $0.01 per completed window + package rate × delivered changes.
UPDATED includes changed 90-day/all-time download counts, not only rank or
version changes. All first-monitor BASELINE rows are free package results
in either output mode; the window fee still applies.
On private build 0.1.1 (2026-09-13, MEASURED): snapshot top-20 used one Hex
list GET, ~685 KiB, no proxy, no browser, 2.2 s Actor time, platform cost
about $0.00056. Buyer bill is 20 × $0.001 + start $0.00005 = $0.02005.
A quiet second monitor of the same window billed start + window-check =
$0.01005 and charged no package-result. These are historical private
validation measurements, not a runtime guarantee.
| Event | When it is charged | FREE / BRONZE |
|---|---|---|
apify-actor-start | Platform start; one event at the default 512 MB. The Actor never emits it. | $0.00005 |
package-result | Each successful package row in a complete snapshot. In monitor, only NEW / UPDATED / RANK_* / EXITED / REAPPEARED from a complete check. | $0.001 |
window-check | Monitor mode, fully delivered window, including NO_RESULTS and zero-change. Not charged for PARTIAL or FAILED. | $0.01 |
Verified missing packages (NOT_FOUND) and diagnostic rows never incur a
package-result fee. Invalid input and source failures incur no custom events;
the platform start fee still applies once a run starts. If all data was already
delivered and the SDK accepts only part of a charge, the final query becomes
PARTIAL / SPEND_LIMIT; accepted charges remain visible in RUN_SUMMARY.
package-result is $0.0008 on SILVER and $0.0006 on GOLD+. Platform usage is
included, not passed through. apify-default-dataset-item is off: enabling it
would double-bill every Dataset row.
A run cap must cover start plus the events: $0.02 is not enough for the sample
top-20, and $0.01 is not enough for a quiet monitor. The configured minimum
cap is $0.021 so the Store prefill can finish. This is a minimum budget,
not a minimum bill: a three-package snapshot still costs $0.00305. The Actor
default budget is $0.50. Snapshot of 20 packages →
1 × $0.00005 start + 20 × $0.001 = $0.02005. Quiet or first monitor of the
same top-20 → start + $0.01 window-check = $0.01005 and $0 change rows.
Retries, FAILED and PARTIAL are not charged as package-result or
window-check (the platform still charges start). If Dataset rows were already
written and billing then hits the cap, window-check may already have been
charged; that query is PARTIAL / SPEND_LIMIT and last-good state is saved.
Limitations
- There is no native Hex history comparable to storing yesterday's JSON
yourself.
curlof the same public endpoints remains free. Per-version daily download series are outside this Actor's scope. - The Actor does not cover the whole Hex catalog. It never paginates the catalog.
- EXITED tombstones are kept 180 days, at most 20 000 per query. After prune, a
very old return may classify as
NEW. - MVP assumes one writer per
stateNamespaceand sequential runs. There is no distributed lock. - Watchlist package GETs use concurrency 2 and are not exposed in Input.
Legal review on 2026-09-13: https://hex.pm/robots.txt disallows only /diff/.
/api/ is allowed. Hex ToS §4 permits API use through a third-party product;
§6 prohibits scraping outside published interfaces. Re-read robots and the
Hex Terms of Service before Store
publication; a later Disallow: /api/ is a stop, not something to bypass.
Use from AI agents with MCP
After Store publication, connect using your own Apify account through
https://mcp.apify.com?tools=automa-flow/hex-pm-package-rankings-monitor.
Ask the agent to keep mode: monitor and outputMode: changesOnly on repeat
runs, and to treat NO_RESULTS as a verified empty window, not a source
failure. Execution requires your own authentication and run budget. Public
search discovery becomes verifiable after publication; a private Actor is not
expected in anonymous search. Autonomous agentic payments are not a verified
channel for this Actor.
Automation example (n8n / Make)
This is a copy-paste illustration, not a hosted integration promise.
- Save the monitor JSON above as a Task. In Apify Schedules, use
0 8 * * *in UTC for a daily 08:00 check and attach that Task. - Add an
ACTOR.RUN.SUCCEEDEDwebhook pointing to your n8n/Make webhook URL. Readresource.defaultDatasetIdfrom the notification and fetch its items with your own Apify connection. Inspectquery_statusfirst: a run can finish with aPARTIALbusiness outcome. - Keep rows where
record_type=package_observationandchange_typeis one ofNEW,UPDATED,RANK_UP,RANK_DOWN,EXITED,REAPPEARED. - Open
source_urlfor the human check. IgnoreEXITEDas "unpublished from Hex".
Protocol notes
User-Agent: automa-flow-hex-pm-package-rankings-monitor/0.1. Origin is fixed
to https://hex.pm. Redirects off documented /api/ paths fail the window.
Timeout 20 s. A list body over 2 MiB is FAILED / SOURCE_LIMIT. List GET concurrency is 1.
Watchlist GETs use concurrency 2 and are not exposed in Input.