Cost of Living Scraper - Baskets, Indices & Official Floor
Pricing
$4.00 / 1,000 city priceds
Cost of Living Scraper - Baskets, Indices & Official Floor
One row per city: cost of living baskets for one person and for a family of four, cost-of-living indices, household estimates, nomad scores and the official country floor. Every figure is labelled city or country grain. For relocation teams, HR pay bands and market sizing.
Cost of Living Scraper: one row per city with baskets, indices and an official floor
This cost of living scraper returns one row per city with the cost of living baskets for one person and for a family of four (total with rent, without rent, rent and utilities, food, transport, salary after tax), twelve basket lines from lunch to a one-bedroom flat, six cost-of-living indices with the household estimates excluding rent, the versus-city percentages, the nomad score block where that door still answers, and the official country-grain price floor next to all of it. It is built for relocation platforms pricing a move, HR compensation teams testing an offer, and market sizing across a list of cities: give it a list of city names and get one structured row each, with every figure labelled city or country grain.
What this cost of living scraper returns
- One dataset row per city, so a 40-city market sizing is billable only for the rows you actually received.
- Cost baskets for one person and for a family of four:
total_with_rent,without_rent,rent_utilities,food,transportandsalary_after_tax, plus the world rank and the rank inside the country. - Twelve basket lines covering the things a relocation quote has to price: lunch menu, dinner for two, a fast-food meal, beer, cappuccino, a soft drink, one-bedroom flat in the centre, a cheaper one-bedroom flat, a three-bedroom flat, utilities for one person and for a family, and an internet plan.
- Six cost-of-living indices: cost of living, rent, cost of living plus rent, groceries, restaurant prices and local purchasing power.
- Household estimates excluding rent for a family of four and for a single person, and the versus-reference-city percentages for cost of living and for rent, so "how much cheaper is Lisbon than Amsterdam" is a field rather than a separate request.
- The nomad score block: nomad score and rank, the monthly cost for a nomad, an expat, a family and a local, the lifestyle band with its monthly figure, average internet speed, the quality of life, family and community bands, and the safety ratings.
- The official country-grain price floor: the harmonised consumer price index (annual average, 2015 = 100), the purchasing-power parity conversion factor in local currency units per international dollar, and for the Netherlands the national consumer price index with its annual change.
- A
field_grainmap on every row saying which blocks are city grain and which are country grain, and adata_coveragecount block saying which blocks returned figures. - Money in the currency you ask for, with the conversion rate and its date recorded on the row so any number can be traced back to what the source page showed.
Why do a cost of living comparison
Nomad and relocation platforms, HR compensation teams and private equity market sizing all have the same question: what does life in this city cost, and does the salary on the table survive it. The crowd-sourced price tables answer the first half well and the official statistics answer the second half differently, and until now a buyer had to pick one or the other and hope the grain matched.
That gap matters because the numbers disagree in ways that change a decision. A crowd-sourced basket for one city is a real monthly figure. A national consumer price index is a percentage change against a base year. A purchasing-power parity factor is local currency units per international dollar. Placing them in the same row without labels is how a relocation model ends up comparing a Lisbon flat to a Portuguese inflation rate.
The salary arbitrage angle is the reason the Dutch market cares. A Bay Area
package tested against Lisbon costs, or a Dutch offer tested against a
cheaper market, is one subtraction once salary_after_tax and the basket sit
in the same currency in the same row. Doing that by hand means opening three
different sites, reconciling three page shapes, guessing at the grain of an
index, and re-doing it every time the numbers move.
Input
| Field | Required | What it does |
|---|---|---|
cities | Yes | Cities to price. Write Lisbon, Lisbon, Portugal to pin the country, or paste a full city page address. Up to 50 per run. |
currency | No | Display currency for every money figure. One of USD, EUR, GBP, CHF, SEK, NOK, DKK, PLN, CZK, JPY, CAD, AUD, BRL, INR. Default USD. |
include_baskets | No | Cost baskets and basket lines. On by default. |
include_indices | No | Indices, household estimates and versus-city percentages. On by default. |
include_scores | No | Nomad score block. On by default. |
include_floor | No | Official country floor. On by default. |
max_cities | No | Upper bound on cities returned per run, 1 to 50. Default 20. |
cursor | No | Continuation token from an earlier run's next_cursor. Send it with the same input and the run picks up where the last one stopped. API field, not on the run form. |
Output
{"city": "Lisbon","country": "Portugal","currency": "EUR","as_of": "2026-10-07","baskets": {"grain": "city","one_person": {"total_with_rent": 1758.96,"without_rent": 632.55,"rent_utilities": 1126.41,"food": 399.03,"transport": 94.84,"salary_after_tax": 1435.98},"family_of_four": {"total_with_rent": 4007.32},"basket_lines": {"lunch_menu": 13.15,"apartment_1bd_centre": 1257.93,"utilities_one": 105.57,"internet": 30.42},"world_rank": 2123,"country_rank": 1,"status": "ok"},"indices": {"grain": "city","cost_of_living_index": 54.1,"rent_index": 34.5,"cost_of_living_plus_rent_index": 45,"groceries_index": 49.1,"restaurant_price_index": 56.8,"local_purchasing_power_index": 66.8,"family_of_four_excl_rent": 2743.8,"single_person_excl_rent": 760.4,"pct_cheaper_than": {"vs_city": "Amsterdam","cost_of_living_pct": 30.8,"direction": "cheaper","rent_pct": 34.7},"status": "ok"},"scores": {"grain": "city","nomad_score": 3.79,"nomad_rank": 70,"cost_of_living_nomad": 4279.3,"cost_of_living_expat": 2968.58,"cost_of_living_family": 5712.6,"cost_of_living_local": 1631.91,"lifestyle_band": "Very rich","lifestyle_band_monthly": 2867.48,"internet_mbps": 101,"safety": "Good","status": "ok"},"floor": {"grain": "country","country": "Portugal","reference_year": "2024","hicp_annual_average_index": 122.15,"hicp_label": "harmonised consumer price index, annual average, 2015 = 100","hicp_period": "2024","ppp_conversion_factor": 0.515993,"ppp_label": "purchasing power parity conversion factor, local currency units per international dollar","error": null},"field_grain": {"baskets": "city","indices": "city","scores": "city","floor": "country"},"data_coverage": {"baskets": 1,"indices": 1,"scores": 1,"floor": 1},"missing_blocks": [],"conversion": {"USD": {"to": "EUR","rate": 0.89469,"rate_date": "2026-10-07"}}}
This is one item out of a live run on 7 October 2026 (examples/live-lisbon.json,
input cities: ["Lisbon, Portugal"], currency: "EUR"), trimmed to the blocks that
matter; the row also carries currency_note per block, quality_of_life and
population on the basket, headline_total_with_rent for the same figure the source
prints at the top, and fetched_at (the ISO 8601 moment the row was fetched) beside
the date-only as_of. Note that world_rank and country_rank sit on baskets,
not inside one_person. Figures move as the sources update.
The floor block keeps its full key set on every run: hicp_label and
hicp_period sit next to the harmonised index, ppp_label next to the
conversion factor, national_cpi_index, national_cpi_annual_change_pct,
national_cpi_period and national_cpi_label are filled for the Netherlands
and stay null elsewhere, and a figure the source does not publish for that
country comes back as null instead of a missing key.
Every block also carries an error field: null on the rows above, and a
short reason such as city not found, upstream unavailable,
no official figure returned or country not in the official floor when that
block came back without figures, so a block that missed is labelled rather than
missing. Each row carries coverage_count (how many of the four blocks
answered, 0 to 4 — the count the pricing section charges on) and
country_code. The run as a whole reports partial and next_cursor: a run
that collected everything in one go has partial: false and
next_cursor: null. partial: true with a next_cursor means more cities
are still to collect; partial: true with next_cursor: null means the list
is complete but some rows carry a soft error (no_data,
conversion_unavailable) — see errors and the per-row error fields.
Use cases
Relocation platform pricing a move. A user in Amsterdam asks what Lisbon costs. One call returns both cities in one currency: the basket for one person, the basket for a family of four, the rent and utilities line, and the versus-city percentage that the platform can put straight into its copy. The floor block lets the platform show a national price index next to the city basket without mixing the two.
HR compensation team testing a package. A Bay Area offer is on the table
and the candidate is looking at Lisbon. salary_after_tax in the row is the
local benchmark; the basket is what the package has to cover. The
purchasing-power parity factor converts a local figure into an international
dollar for a multi-country band, which is what a global comp spreadsheet
actually needs.
Private equity market sizing. Forty cities in one run, one row each, with
the food and transport basket as the cheapest way to size a consumer market
and the local purchasing power index as the correction. max_cities bounds
the run and the per-row charge means a pilot of five cities costs a pilot's
money.
Salary arbitrage research. The Dutch angle is net salary minus the
basket, computed per city pair. salary_after_tax and total_with_rent are
the two fields that matter, and putting them in the same currency in the same
row is what makes the subtraction possible without a second tool.
How it compares
| This actor | logiover/numbeo-cost-of-living-scrape | automation-lab/numbeo-scraper | lulzasaur/nomadlist-scraper | |
|---|---|---|---|---|
| Sources in one row | Baskets, indices, nomad scores, official floor | One price table | One price table | One score page |
| Per 1k rows, free plan | $4.00 | $3.50 | $5.00 | $10.00 |
| Per 1k rows, Store "from" price | $4.00 | $1.99 | $3.25 | $10.00 |
| Run-start event | None | $0.00005 per GB of run memory | $0.01 | $0.00005 per GB of run memory |
| Official statistics floor | Yes, with grain labels | No | No | No |
| Grain labels per block | Yes | No | No | No |
| Versus-city percentages | Yes | No | No | No |
| Household estimates excluding rent | Yes | No | No | No |
| Nomad score block | Yes | No | No | Yes |
| Currency conversion recorded | Yes | No | No | No |
The two price rows are what a buyer actually pays: the per-event rate each
competitor lists for a free Apify plan, and the "from" price the Store shows
for that actor. logiover charges $0.0035 per result on a free plan, $0.003
on BRONZE and $0.00199 from GOLD up; automation-lab $0.005 per city plus a
$0.01 run start ($0.00325 from); lulzasaur $0.01 per result at every tier.
All three also bill a run-start event on top of that: logiover and
lulzasaur $0.00005 per GB of run memory, automation-lab a flat $0.01.
This actor bills no run-start event — the row price is the only charge it
adds. Compute usage of the run itself is billed by Apify on every plan, for
their runs as for this one.
The closest competitor by audience is logiover/numbeo-cost-of-living-scrape
at 59 users (10 of them in the last 30 days), and its row costs $0.0035 against
this actor's $0.004 on a free plan, then $0.003 (BRONZE) down to $0.00199 (GOLD
and above) against this actor's flat $0.004 once the plan is paid. What those actors return is one source's page as JSON. This actor folds
four blocks into one schema, labels the grain of each, and puts the official
country floor next to the crowd numbers so a model can tell a city basket from
a national index. What they have that we do not: the cheaper row on paid plans,
and a single source that never disagrees with itself.
Pricing
All pricing is pay-per-event at $0.004 per city, one event per returned
city row. $4.00 per 1,000 rows. Rows that carry no figures at all are
delivered but never charged, so you only pay for results you receive. There
is no actor-start fee and no other event on this actor: the row price is the
whole price this actor adds. Compute usage of the run itself is billed by
Apify at its usual rate, exactly as it is for any other actor. The run record
reports billing.cities, the number of rows charged for that run, so an
invoice line can be checked against the run that produced it.
Free-plan Apify accounts receive up to 10 city rows per run, and those same 10 rows are the most a free run can be charged. Larger city lists run from a paid plan, or split across runs.
Limits and gotchas
- The scoring door answers a limited number of cities per run and then stops.
The actor paces itself and stops as soon as that happens, and every
remaining city comes back with
scores.statusset toblockedrather than a zero score.include_scores=falseskips that block entirely and makes a long city list cheaper. - The price tables and the index pages also answer a limited number of
requests before they refuse. The actor pauses between cities.
max_citiesis the real lever on a big list: raise it only when the run is worth the extra minutes. - Money figures arrive in whatever unit each source publishes in. The actor
reads the unit off the page, converts to your
currency, and records the rate and its date underconversion. If no rate can be published for a unit, those figures stay in their original unit and the row says so. - The official floor is country grain.
field_grainmarks it as such. Do not read a national consumer price index as a city number; it is there as a floor, not as a basket. - The purchasing-power parity factor is local currency units per international dollar. It is not a price level and not a cost.
- City resolution uses the source's own city index, so a bare
Lisbonresolves to the right country. An ambiguous name with no country resolves to the largest match and the row reports the country it found. - A long list can spend its collection budget partway through. The run follows
the continuation token by itself while there is time left, so most lists come
back complete in one run. When it does run out, it stops in the open instead
of quietly handing you half a city:
partialgoes totrue,next_cursorcarries a continuation token and the run warns. Send that token asRequest budget reached; continue with next_cursorcursorwith the same input and the rest is collected. The rows you already hold are delivered and charged for; the continuation is charged only for the city rows it adds.partialwith a cursor only means that deferral; on a finished list (next_cursor: null) it flags rows that carry a soft error, not work that is still owed. - A run of 20 cities takes a few minutes because of the pacing. That is the cost of staying inside the sources' budgets.
FAQ
How do I run a cost of living comparison of cities?
Put both cities in cities (Lisbon, Amsterdam) and read the two rows.
pct_cheaper_than on each row gives the versus-city percentage the index
source publishes, and both rows are converted to the same currency so the
baskets subtract cleanly.
What does a cost of living comparison between cities actually return? One row per city with four blocks: the cost baskets, the indices and household estimates, the nomad score block, and the official country floor. Each block carries a grain label and the row carries a coverage count, so you can see which blocks returned figures.
Can I get cost of living for Lisbon, Portugal specifically?
Yes. Write Lisbon, Portugal to pin the country, or Lisbon alone and let
the city index resolve it. The row comes back with country filled in
either way.
How do I compare minimum wage against cost of living?
The row carries salary_after_tax alongside the baskets, which is the
monthly net figure the comparison needs. Bring your own minimum wage figure
for the market you are testing and subtract total_with_rent for the
standard of living you have in mind.
Does the nomad score block always come back?
No. That door answers a limited number of cities per run. When it closes,
the remaining rows carry scores.status set to blocked. Turn
include_scores off if you only want baskets and indices.
What if a run stops partway through my city list?
It says so rather than hiding it: partial is true, next_cursor holds a
continuation token and a warning tells you to continue. Pass that token as
cursor with the input you already used and the remaining cities come back as
normal rows. Cities you already received are not collected or charged twice.
Which figures are city grain and which are country grain?
field_grain says so on every row. Baskets, indices and scores are city
grain. The floor block is country grain and carries the country name with it.
What if a city is not in the sources at all?
The run still succeeds. The row comes back with error: "city not found" in
the blocks that could not answer, the run record carries
no figures for <city> in errors, and the row is delivered without being
charged. Add the country (Lisbon, Portugal) if the name resolved to the
wrong place.
Related Actors
- Google Flights Scraper for the travel cost side of a move alongside the monthly basket.
- Zillow Scraper for US rental listings next to the rent and utilities line.
- LinkedIn Jobs Scraper for the salary side of the arbitrage question.