ActorStack.dev

A quarter of freelance budgets have no minimum, and the separator moves with the language

"USD 1,000" and "USD 1.000" are both one thousand. "Less than USD 50" has no lower bound. Both facts break naive budget parsing in ways that survive review.

By Oswaldo Carabano8 min read

Short answer

Freelance budgets are not numbers, they are strings with a shape. Measured on 272 Workana budgets, 70.6% are ranges, 25.0% are `max_only` ("Less than USD 50", which has no minimum to report), 4.4% are `min_only`, and the rest are fixed or unspecified. On top of that the thousands separator changes with the content language, so a parser that ignores the locale turns "USD 1.000" into 1 — a three-orders-of-magnitude error in the field people filter on most.

Key points

  • `budget_type` distinguishes five shapes: `range` 70.6%, `max_only` 25.0%, `min_only` 4.4%, plus rare `fixed` and `unspecified`.
  • A null `budget_min` on a `max_only` budget is not a coverage gap: there is no minimum to report.
  • Averaging `budget_min` across a mixed set silently drops a quarter of projects and biases the average upward.
  • The thousands separator follows the content language: "USD 1,000" in English and "USD 1.000" in Spanish are both one thousand.
  • Every row keeps `budget_raw` and `locale`, so the parse is checkable rather than trusted.
  • `budget_unit` matters too: some budgets are per hour, which changes what any comparison means.
On this page7 sections

The field everybody filters on is the field hardest to parse. A freelance budget is a string written by a marketplace in a language you chose, describing a shape that may have no lower bound, no upper bound, or no number at all — and every one of those cases breaks a naive parser differently.

A budget is not a number

"USD 100 - 250" is a range. "Less than USD 50" has no minimum. "Over USD 3,000" has no maximum. "Open" has no figure. Flattening all of those into budget_min and budget_max without recording which case it was throws away the information that makes the numbers comparable.

The five shapes, measured

budget_typeShareMeaning
range70.6%"USD 100 - 250" — both bounds exist
max_only25.0%"Less than USD 50" — no lower bound exists
min_only4.4%"Over USD 3,000" — no upper bound exists
fixedrareA single figure; min and max are the same
unspecifiedrare"Open" — Workana shows no figure at all

Measured on 272 budgets. The distinction is decided by structure rather than vocabulary: a budget string with no digits is unspecified in any language, which is what makes the classification robust across locales.

Missing versus non-existent

This is the whole point of budget_type. budget_min is null on 25% of rows, and reporting that as a coverage gap would be wrong — the client posted “less than USD 50”, so there is no minimum to report. Nothing failed.

The consequence for analysis is direct: averaging budget_min across a mixed set silently drops a quarter of the projects, and the quarter it drops is the cheap end. The average comes out high and looks plausible.

the wrong way and the right way
-- Wrong: silently excludes 25% of projects, biasing upward
SELECT AVG(budget_min) FROM projects;

-- Right: say which population you are describing
SELECT budget_type, COUNT(*), AVG(budget_min), AVG(budget_max)
FROM projects
WHERE budget_unit = 'project'
GROUP BY budget_type;

The separator that moves with the language

Workana renders its text in the language you request, and the thousands separator follows:

StringLocaleActual value
"USD 1,000"en1000
"USD 1.000"es1000

A parser that assumes the English convention reads the Spanish string as 1 — a three-orders-of-magnitude error in the field people filter on most, and one that produces a suspiciously large population of one-dollar projects rather than an exception.

So the Actor parses per locale and records locale on every row. If you ever see a cluster of implausibly small budgets, that field is the first thing to check.

Why every parsed field keeps its original

budget_min: 100 travels with budget_raw: "USD 100 - 250". total_bids: 30 travels with total_bids_raw: "Propuestas: 30".

The parsed number is an interpretation; the raw string is the marketplace's data. Keeping both means a disagreement is discoverable rather than invisible — the same principle as `fuel_type_raw` on coches.net and the Korean category original on Naver.

Per project or per hour

Some budgets are quoted "USD 15 - 45 / hour", so budget_unit is hour rather than project. An average that mixes the two is meaningless in a way that is hard to spot, because both populations produce plausible-looking numbers.

is_hourly is true on 11.7% of projects, so this is not a rounding error — it is one project in nine.

Analysis that survives all of this

  1. Group by budget_type and budget_unit before computing anything. Two group-bys, and most of the traps are gone.
  2. Report distributions, not averages. A median and quartiles within a type say more than a mean across types.
  3. Use budget_max when you need one number. It is present on 95.6% of rows against 75.0% for the minimum, because most budgets have a ceiling and a quarter have no floor.
  4. Sanity-check against budget_raw on a sample. Ten rows read by eye catches a locale bug that no aggregate will reveal.

Frequently asked questions

Why is `budget_min` null on a quarter of projects?
Because those budgets have no lower bound. "Less than USD 50" is a `max_only` budget: reporting a null minimum as a coverage gap would be wrong, since there is no minimum to report. Read `budget_type` before treating a null as missing data.
How does the language break budget parsing?
The thousands separator changes with it. "USD 1,000" in English and "USD 1.000" in Spanish are both one thousand, so a parser that assumes one convention turns the other into 1. Every row records its `locale` and keeps `budget_raw` so the parse can be verified.
Can I just average the budgets?
Not across a mixed set. Averaging `budget_min` drops the 25% that never had one, which biases the result upward, and mixing per-project with per-hour budgets compounds it. Filter on `budget_type` and `budget_unit` first.

Sources

Every URL below was requested and returned a page on the date shown.

  1. Operator claimchecked 19 Aug 2026
    Workana Projects & Client Demand Scraper — Actor README and input schemaActorStack / Apify Store
A white measuring tape curving across a dark background, showing the numbers 15 to 45.
WorkanaMeasured

The rating that is not a rating

Workana returns a rating for every project, so "100% coverage" would be true. It would also throw away four fifths of the market if you believed it.

6 min
A laptop showing lines of code on a wooden desk in a dimly lit room.
WorkanaMeasured

Filters that do nothing

`budget_min`, `is_hourly`, `duration`, `payment_verified` and nine others return HTTP 200 and change nothing. One of them makes the result set bigger.

7 min
A white measuring tape curving across a dark background, showing the numbers 15 to 45.
WorkanaMeasured

Proposal velocity

Measured across 401 projects: competition on a freelance listing has a half-life measured in hours, which makes a stale row a wrong row.

6 min