IT.
All writing

Two bathrooms for two dollars: calibrating price parsing in bazito

Ilya Trapeznikov Published

In the piece on how bazito is built, money stays with the rules: period, range and currency are parsed by deterministic code, and a model decides only what a post means. That split has a price. The rules get live text wrong, and in the week from 7 to 13 September they were fixed more than ten times.

Seven live errors

Every example comes from a real post.

Listing textWhat came outWhat it should be
”3r, 2b, 98м², 4/5 … 1000$“price of 2 dollars1,000 dollars, “2b” is two bathrooms
”Minimum income $77.800 Maximum income 154k”rent of 154,000 a montha listing with no price
”🅿️ паркинг + 80 €” (parking + 80 €)a flat for 80 euroa parking surcharge
”1000$”, an empty line, “2+1”price of 2 dollars1,000 dollars
”На год: 900€/мес” (for a year: 900€/month)900 euro a year900 euro a month
”Cijena noćenja: 50-140€“an item for sale at 50 euroa nightly rental, 50–140 a night
”Сдам 2+1 70€/сутки” (to let, 2+1, 70€ a night)an item for salea nightly rental

Each row has its own mechanism. A currency sign at the end of one line got glued to the number at the start of the next. The one-letter abbreviation for baht matched how Tbilisi posts write the number of bathrooms. Of two monthly sums the smaller one won, and the smaller one was the parking. The parser knew a nightly price only in Russian and as “per night”, so the Montenegrin “noćenja” turned a flat in Budva into goods.

The check: candidates and control

Each fix to the money rules that week ran against the corpus before merging, read-only. There are two samples. Candidates are the posts where the phenomenon can occur at all, for instance all 3,409 posts carrying the parking sign. Control is about 8,000 random others. The old code and the new code parse both samples, and the results are compared line by line.

Nothing may change in the control. A change there means the rule touches text it has no business with, and such a fix is rejected however much it repaired among the candidates. Changes among the candidates get their own review. For parking there were 62 out of 3,409, all read by hand, and all 62 were parking in place of rent: Varna, Budva, Batumi, Vienna, New York. The fix for gluing across a line break made 1,624 changes over 2,387 candidates with zero in the control.

The check catches the fix

The same setup finds errors in the new code as well.

The fix for “for a year: 900€/month” went through three runs and, at the time of writing, still sits on a separate branch and has not been deployed. The first run made 1,639 changes, nearly all correct, but about 35 car rental price lists got worse: in “1–3 days, 95 €/day” the parser dropped the left part and turned the rental into a monthly lease at 1,750 euro. The check moved out of the pattern into code and got narrower: a period before the sum yields only when the sum is followed by a period of its own, and a different one. The third run over 8,086 candidates found one more stray change, in a Serbian flea market post “400 дин. Вместе отдам за 500”: the Russian word for “together” split into a preposition and a noun that read as “per month”, and a jacket went up for monthly rent.

The “1+1” notation went the same way. The Turkish way of writing rooms became a housing signal, and nightly posts from Cyprus stopped being goods. To keep Prague lists of flats from entering housing with one random price from the list, three “N+1” entries in a row counted as a list. The second run showed this took down 839 real listings: a single listing repeats its own layout (“FOR RENT 2+1 … flat 2+1 … layout 2+1”) or appends “also available 1+0 1+1 2+1”. Only entries with a price next to them count as a list now. The digit is capped at five, because “8+1 & 6+1” in car rentals means seats in a minibus.

What is still wrong is written into the fix’s own description and left alone. Two Istanbul sales that never use the word “sale” still show as rentals, and a stand-up show announcement on the “1+1” TV channel became housing with no price.

Tenant income against a villa

An income requirement in New York affordable housing posts looks like a price: dollars and a large number. A plausibility ceiling will not catch it. Above a million baht the board holds 110 listings, and 16 of them are real villa rates. A real rent and an income requirement differ by 15%, and only the word next to the number tells them apart.

The bare word “income” is no good for this. In English it took the pay away from a vacancy reading “Income: up to €250 per day”, and its Russian equivalent sits in 947 housing listings meaning yield and in 100 vacancies meaning earnings. The rule rejects the income boundary: “minimum”, “maximum”, “combined”, plus savings and credit score. Of 1,429 live listings containing those stems not one changed, and zero changed in a control of 20,000.

One letter and two lists

The bathroom fix started with a different defect. Currency tokens were recognised by one list and the currency code was looked up in another, and the one-letter “б” was only in the first. A listing got marked “currency stated” while its currency actually came from the channel’s default.

That mark decides the channel’s own currency, which is counted from explicit mentions, so a foreign currency was voting for itself. A run over all 227,718 posts in the corpus found 81 spellings of currency and four tokens with no code, all of them one letter. 92 false votes went away, and the currency did not change for any of the 435 sources.

Then a measurement of the board showed that baht is not the only thing written as one letter. A one-letter token now counts as money only in a channel whose default currency is Thai anyway. Over 127,822 compared rows the price changed in 54, all in the right direction: two dollars became real prices from 850 to 2,700 dollars. The measurement found no legitimate baht price in a non-Thai channel.

The past is repaired separately

A corrected rule only parses new posts. Rows already on the board keep their old prices until they are parsed again, and that happens separately: from the list the measurement produced, with a dry run first. After the nightly price fix the dry run and the live run matched: 18 rows parsed again, 11 moved from goods back into housing, 4 got a new price, one came off the board. One more row was excluded by hand, because its wrong price would have been replaced by another wrong price.

A fix can cancel a listing outright when its whole price rested on an invented number. That happened with an Istanbul furniture list that produced a listing for “201 TRY”. The repricing tool did not take such rows down, because it promises to touch only money fields. Taking them down was added as a flag rather than a default, so that the tool’s promise does not widen silently.

Struck-through prices had to be fixed at intake. The database stores text without markup, so a discount written as “old new” arrived as two equal sums, and either the first or the second can be the struck one. New posts now lose struck-through text at intake. Older ones are re-read from the post’s public page and rewritten only if the earlier reading matches the database, meaning it is the same edition and not a text edited later.

The project as a whole, with its architecture, decisions and figures, is on the bazito case page.


This piece contains no parsing patterns, thresholds, channel list or source addresses. The example texts come from open channels; the measurements date from 7–13 September 2026 and were taken read-only from the working database.