Troostwijk Auction Scraper - Industrial Lots, Bids, Close Times avatar

Troostwijk Auction Scraper - Industrial Lots, Bids, Close Times

Pricing

$1.70 / 1,000 per lot row returneds

Go to Apify Store
Troostwijk Auction Scraper - Industrial Lots, Bids, Close Times

Troostwijk Auction Scraper - Industrial Lots, Bids, Close Times

From $1.70 per 1,000 lots, no start fee. Every live lot on Troostwijk Auctions (TBAuctions: Troostwijk, Surplex, Vavato) with its real per-lot closing time, current bid, bid count, city and country. Optional lot pages add specs, brand, year, photos and the inspection/collection address.

Pricing

$1.70 / 1,000 per lot row returneds

Rating

0.0

(0)

Developer

Scrapers Delight

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Troostwijk Auction Scraper — industrial lots, bids and real closing times

Every live lot on troostwijkauctions.com, the TBAuctions storefront that also carries Surplex and Vavato inventory: bankruptcy estates, plant clearances, CNC machinery, forklifts, trucks, farm equipment, lab and IT hardware across 21 European countries.

One row = one live lot. $0.0017 per row — $1.70 per 1,000 lots. No start fee. Nothing you filter out is charged.

Measured on the live board on 2026-09-18, by walking all of it: 506 open auctions carrying 77,868 lots, across 21 countries (Belgium 879 collection locations, Netherlands 807, Germany 148, France 103, Austria 28, Poland 21, and 15 more).


The thing that makes this different

Every lot in this dataset carries a real per-lot closing time. That sounds unremarkable until you look at where the site actually publishes it:

SurfaceendDate present
The auction's lot list (/en/a/<slug>)568 of 568 = 100.0%
The lot's own detail page (/en/l/<slug>)0 of 48 = 0.0%

Those are literal counts over real captured bytes, both in fixtures/. A scraper that starts from lot pages — which is the obvious way to build this, and what the store's most detailed Troostwijk rival does with its 37-field dump — cannot produce a closing time at all. Substituting the auction's closing time does not fix it either, because closes are staggered within a single auction: in one sampled 48-lot page, all 48 lots had 48 different close times.

So this actor builds every row from the auction lot list and treats the lot page purely as optional enrichment layered on top. mergeDetail() is explicitly forbidden from touching the close time. That is the whole architectural decision, and it is why there is deliberately no "paste a lot URL" mode — a lot URL alone cannot tell you when the lot closes.

The same asymmetry hits the price: the lot detail payload publishes currentBidAmount on only 15 of 48 lots (31.3%), while the lot list carries it on 100%.


What you get

Always (one request per 48 lots)

FieldTypeWhat it isFill
lotUrlstringDirect link to the lot page.100.0%
displayIdstringThe lot number the site shows, e.g. A7-50158-6. Unique, and the auction id is its prefix.100.0%
titlestringLot title as listed.100.0%
closesAtISO 8601 UTCWhen this lot closes. Per lot, not per auction.100.0%
minutesToCloseintegerMinutes from scrapedAt to closesAt. Negative if it closed while the run was in flight.100.0%
opensAtISO 8601 UTCWhen bidding opened on this lot.100.0%
currentBidnumberCurrent bid in currency units, not cents. Before the first bid this is the opening price.100.0%
currencystringEUR on all 568 captured rows.100.0%
bidsCountintegerNumber of bids. 0 is a real answer, not a missing value.100.0%
hasBidsbooleanbidsCount > 0, for filtering without a null check.100.0%
followersCountintegerHow many users are watching the lot. The demand signal that moves before the bids do.100.0%
biddingStatusstringBIDDING_OPEN, BIDDING_NOT_YET_OPENED, BIDDING_CLOSED.100.0%
saleTermstringGUARANTEED_SALE (250 of 568), OPEN_RESERVE_PRICE_NOT_ACHIEVED (235), OPEN_ALLOCATION_SET (83).100.0%
citystringCity the lot is physically in.100.0%
countryCodestringISO-2, lower case. 21 countries on the live board.100.0%
auctionDisplayIdstringParent auction number, e.g. A7-50158.100.0%
auctionNamestringParent auction title.100.0%
auctionUrlstringLink to the parent auction.100.0%
auctionLotCountintegerLots the board reports for that auction. See the note below on why this is not the lot-list total.100.0%
auctionFirstLotClosesAtISO 8601 UTCThe auction's minEndDate - the earliest close among its lots, and a true lower bound.100.0%
imageUrlstringPrimary thumbnail.100.0%
platformstringTWK Troostwijk (448 of 568), SPX Surplex (103), VAVATO Vavato (17).100.0%
Plus lotId, itemId, urlSlug, auctionId, auctionOpensAt, closesAtUnix and scrapedAt as
identifiers.

With Fetch each lot page on (one extra request per lot, ~65 KB)

Same rows, more columns — at no extra charge. There is one billing event and it is per row, so enrichment costs you nothing beyond run time.

FieldTypeWhat it isFill
descriptionstringSeller description, HTML as published.77.1%
remarksstringAuctioneer notes on condition, wear and what is included.45.8%
specsarrayStructured specifications as {name, value, unit}. 97 distinct spec names across 48 lots; median 6 per lot, max 29.89.6%
brandstringPulled out of specs - the Brand attribute.72.9%
modelTypestringThe Type or Model attribute.54.2%
yearOfBuildstringThe Year of build / Construction date attribute.56.3%
serialNumberstringThe Serial number / Chassis number attribute.14.6%
categorystringTop-level category, e.g. Metalworking, Transport & Logistics.100.0%
subCategorystringSecond level, e.g. Lathes.100.0%
categoryPathstringThe breadcrumb joined with >.100.0%
startingBidnumberThe lot's opening amount (initialAmount), in currency units.100.0%
reserveStatusstringNO_MINIMUM_BID_AMOUNT (25 of 48) or MINIMUM_BID_AMOUNT_NOT_MET (23) - whether a reserve exists and whether it is met.100.0%
quantityintegerUnits in the lot.100.0%
conditionstringReturned verbatim. Not a condition grade - see honest limits.100.0%
marginSchemebooleantrue = sold under the VAT margin scheme, so no reclaimable VAT on the invoice. true on 1 of 48.100.0%
hasWarrantybooleanfalse on all 36 lots that publish it.75.0%
imageUrlsarrayEvery photo, in the site's own order. Median 10 per lot, max 119.100.0%
imageCountintegerLength of imageUrls.100.0%
documentUrlsarrayAttached PDFs - spec sheets, inspection reports.8.3%
carbonFootprintKgnumberThe site's own carbon figure for the lot (its "LCA value"), kg CO2e.35.4%
companyTypestringCOMPANY (40 of 48), LIQUIDATOR (4), COMPANY_LIGHT (4).100.0%
companyIdstringNumeric consignor-account id, stable across that consignor's lots. Not a company name.100.0%
auctioneerIdstringThe selling TBAuctions entity: TBA, SPX_DE, KDA, DECHOW, TWK_FR, TWK_VAVATO, TWK_REST_OF_WORLD.100.0%
regionRestrictionstringNONE (44 of 48) or EU (4) - where the lot may be bid from.100.0%
visiblePlatformsarrayWhich sibling storefronts also show this lot.75.0%
otherLanguageUrlsarrayThe same lot page in up to 11 languages, as {locale, url}.100.0%
collectionStreetstringStreet of the collection facility.91.7%
collectionPostalCodestringPostcode of the collection facility.91.7%
collectionCitystringCity of the collection facility.100.0%
collectionCountryCodestringCountry of the collection facility.100.0%
collectionFacilityNamestringFacility name, e.g. Amann Werkzeugmaschinen GmbH.91.7%
collectionWindowsarrayCollection slots as {from, to, byAppointment, isDeliverySchedule, forkliftAvailable, forkliftCapacityKg, contactNote, emails}.100.0%
viewingStreetstringStreet of the viewing/inspection location, when it differs from collection.50.0%
viewingPostalCodestringPostcode of the viewing location.50.0%
viewingCitystringCity of the viewing location.100.0%
viewingFacilityNamestringName of the viewing location.50.0%
viewingWindowsarrayInspection slots, same shape as collectionWindows.100.0%
forkliftAvailablebooleanWhether any collection slot lists a forklift on site.100.0%
forkliftCapacityKgintegerLargest forklift capacity offered, in kg. Null when the site records none.25.0%
collectionContactEmailsarrayEmail addresses extracted from the collection/viewing block. Logistics contacts, not the consignor.50.0%
collectionNotesstringThe prose left in that block once the addresses are extracted - collection deadlines, a contact name, a phone number.33.3%
Every percentage in both tables above is a literal count over real captured rows — 568 lot-list
rows across 14 auctions for the core fields, 48 lot pages across those same 14 auctions for the
detail fields. null, "", "-", "N/A" and an empty array are all counted as not filled.
offline_validate.mjs re-derives every one of them from fixtures/ and fails if a number here
drifts from the code. Run node offline_validate.mjs yourself.

A real row

This is not an illustrative example. It is an actual row this code produced from the captured bytes, reproduced field-for-field by the validator. The long arrays (specs, imageUrls, otherLanguageUrls, collectionWindows, description) are shown separately below only so the block stays readable.

{
"lotUrl": "https://www.troostwijkauctions.com/en/l/mori-seiki-ntx1000-cnc-lathe-A7-50158-6",
"displayId": "A7-50158-6",
"title": "Mori Seiki - NTX1000 - CNC lathe",
"lotId": "717bab5d-74f9-410e-a0bf-3fda05bf0023",
"itemId": "72b38e6d-0fa2-4a4a-aa37-2b66e7d04393",
"urlSlug": "mori-seiki-ntx1000-cnc-lathe-A7-50158-6",
"closesAt": "2026-10-13T08:06:00.000Z",
"closesAtUnix": 1791878760,
"minutesToClose": 35406,
"opensAt": "2026-09-17T08:00:00.000Z",
"currentBid": 60000,
"currency": "EUR",
"bidsCount": 0,
"hasBids": false,
"followersCount": 3,
"biddingStatus": "BIDDING_OPEN",
"saleTerm": "OPEN_RESERVE_PRICE_NOT_ACHIEVED",
"city": "Nürnberg",
"countryCode": "de",
"auctionDisplayId": "A7-50158",
"auctionName": "CNC machinery from Deckel Maho, Hüller Hille, Emag, Mazak and other manufacturers",
"auctionUrl": "https://www.troostwijkauctions.com/en/a/cnc-machinery-from-deckel-maho-huller-hille-emag-mazak-and-other-manufacturers-A7-50158",
"auctionId": "eb232a3b-51b6-4165-99da-1584c11c609f",
"auctionLotCount": 16,
"auctionOpensAt": "2026-09-17T08:00:00.000Z",
"auctionFirstLotClosesAt": "2026-10-13T08:00:00.000Z",
"imageUrl": "https://media.tbauctions.com/image-media/43f2f32b-229a-48f3-bd90-19af9a4e51bf/file",
"platform": "SPX",
"scrapedAt": "2026-09-18T18:00:00.000Z",
"detailFetched": true,
"remarks": "Used machine - functional, without warranty",
"brand": "Mori Seiki",
"modelType": "NTX1000",
"yearOfBuild": "2012",
"serialNumber": "NTX10120608",
"category": "Metalworking",
"subCategory": "Lathes",
"categoryPath": "Metalworking > Lathes",
"startingBid": 60000,
"reserveStatus": "MINIMUM_BID_AMOUNT_NOT_MET",
"quantity": 1,
"condition": "NOT_CHECKED",
"marginScheme": false,
"hasWarranty": null,
"imageCount": 11,
"carbonFootprintKg": 39413.88,
"companyType": "COMPANY",
"companyId": "16239",
"auctioneerId": "SPX_DE",
"regionRestriction": "NONE",
"collectionStreet": "Elisenstraße 4",
"collectionPostalCode": "90441",
"collectionCity": "Nürnberg",
"collectionCountryCode": "de",
"collectionFacilityName": "Amann Werkzeugmaschinen GmbH",
"viewingStreet": null,
"viewingPostalCode": null,
"viewingCity": "Nürnberg",
"viewingFacilityName": null,
"forkliftAvailable": false,
"forkliftCapacityKg": null,
"collectionContactEmails": [
"j.berchner@amann-wzm.de",
"project.de@surplex.com"
],
"collectionNotes": "Julian Berchner, +49 171 5303 918,"
}

Its specs array, verbatim:

AttributeValueUnit
BrandMori Seiki
TypeNTX1000
Year of build2012
Serial numberNTX10120608
Turning Ø370mm
Turning length424mm
Spindle boring Ø65mm
Speed from n=0rpm
Speed till n=12000rpm
Number of tool changer stations10
Coolant pumptrue
Chip conveyortrue
CNC control, brand, typeMSX-711-IV
Engine power (kW)45kW
Machine weight10500kg
Dimension l2710mm
Dimension w2430mm
Dimension h2400mm
Terms of deliveryLoaded on truck

And its collectionWindows, which is where the collection contact comes from:

[
{
"from": null, "to": null, "byAppointment": true,
"isDeliverySchedule": false, "forkliftAvailable": false, "forkliftCapacityKg": null,
"contactNote": "Julian Berchner, +49 171 5303 918,",
"emails": ["j.berchner@amann-wzm.de"]
},
{
"from": "2026-10-23T06:00:00.000Z", "to": "2026-10-23T14:00:00.000Z", "byAppointment": false,
"isDeliverySchedule": false, "forkliftAvailable": false, "forkliftCapacityKg": null,
"contactNote": null, "emails": []
}
]

A CNC lathe at EUR 60,000, closing on a known date, sitting at a named facility with a street address, an inspection slot, and a contact for arranging collection. That is the row.


Inputs

What to scrape

InputWhat it does
auctionUrlsAuction URLs, slugs or display ids (A1-49582). Empty = the whole live board. An auction that has already closed is reported, not billed.

Filter the live board

InputWhat it does
countryCodesLot's own country, ISO-2 lower case. 21 available.
platformsTWK / SPX / VAVATO — the storefront serves all three.
keywordsCase-insensitive match on the lot title. Title is the only text the list surface publishes.
closingWithinHoursOnly lots closing inside the window. The cheapest filter here — see below.
onlyWithBidsOnly lots that already have a bid (37.5% of live lots do).
minCurrentBid / maxCurrentBidCurrent bid band, in EUR. Measured range across 568 rows: EUR 1 to EUR 60,000, median EUR 700.
includeNotYetOpenInclude the 14 pre-announced auctions. Off by default — they usually publish no lots yet.

Order and depth

InputWhat it does
auctionOrderclosingSoonest (default), newest, mostLots, siteOrder. A capped run should hand you the lots about to close, not an arbitrary slice of 77,868.
maxLotsHard cap on rows delivered — and therefore charged. Default 500, 0 = no cap.
includeLotDetailsFetch each lot page for the detail columns. Off by default.

Advanced

InputWhat it does
concurrency1–10, default 5. Measured: 5 gave 90/90 clean requests; 10 and 16 each dropped one or two to connection errors.
useFastJsonRouteUse the site's cheaper JSON route where possible, with automatic HTML fallback.
proxyConfigurationApify proxy on by default. The site is clean through it — see below.

closingWithinHours is nearly free, and that is not an accident

You are charged per row delivered but we pay per row examined, so a filter applied after fetching would decouple your cost from ours. Two things keep that honest here:

  1. The lot list returns 48 rows per request. One 84 KB fetch yields 48 candidates, so a filter that rejects 47 of them still cost one request. Cost per examined row is ~1.75 KB.

  2. The site publishes a pre-filter index and we use it. Every auction carries minEndDate, the earliest close among its lots — and it is a genuine lower bound (verified on 14 auctions, 14/14: minEndDate <= min(lot endDate)). So an auction whose first lot closes after your window is skipped whole, all 5,784 of its lots if need be, without a single request. In the validator's own run against the captured board, a 24-hour window pruned 499 of 506 auctions and ~69,794 lots that were never fetched.

The expensive surface — lot detail, one request and ~500 KB per row — is fetched only for rows that already passed every filter and fit inside your cap. A run never pays detail cost for a row it will not deliver, and the validator asserts that ordering in the source.


How it reads the site, and the one thing that rots

The site is a Next.js app; every page server-renders its data into a __NEXT_DATA__ script block. There is no DOM scraping and no browser — this actor runs on plain Node at 512 MB.

StepRequestMeasured
1. The boardGET /en/auctions?page=N11 pages, 506 auctions, closes exactly on the site's own totalSize
2. Lots in an auctionGET /en/a/<slug>?page=N48 rows per page, terminates on hasNext:false, asserted against lots.totalSize
3. Optional detailGET /en/l/<slug>~65 KB on the wire, ~530 KB decoded

The site serves the same rows a second way, as JSON at /_next/data/<buildId>/…, for 56 KB instead of 88 KB. That URL embeds a build id that changes on every site deploy. The validator confirms the two routes return page 2 element-for-element identically, so this actor:

  • uses the HTML route as primary — it needs no build id and cannot rot;
  • scrapes the build id fresh each run and never hardcodes it (the validator greps src/ to prove the literal is absent);
  • treats the JSON route as a bandwidth optimisation that falls back to HTML automatically on any failure. A build-id rotation mid-run is a slowdown, not an outage.

lotCount is not the lot-list total. The board's lotCount and the lot list's own totalSize disagree slightly — measured across 14 auctions, 1,934 against 1,932, with one auction reporting lotCount 116 against totalSize 114. The walk asserts against totalSize, because that is the number the lot list will actually hand you.


Reliability, measured rather than asserted

All on 2026-09-18, through the Apify proxy unless noted:

TestResult
60 lot-detail pages, concurrency 560/60 HTTP 200 in 33 s (1.82 req/s), 65,030 B avg
30 auction lot-list pages, concurrency 530/30 HTTP 200 in 20 s (1.50 req/s), 84,003 B avg
Same 60 at concurrency 1058/60 — two connection failures
Same 60 at concurrency 1659/60 — one connection failure
Apify proxy, auto pool3/3 HTTP 200
Apify proxy, RESIDENTIAL + country NL3/3 HTTP 200, full __NEXT_DATA__
Full board walk, 11 pages11/11 HTTP 200
48 lot pages from a bare home IP47 first-try, 1 transient 403 that returned 200 on one retry

That single 403 is the only anti-bot signal on this target, and its body was ordinary lot HTML with the lot's own <title> — a rate-limit blip, not a challenge page. The actor therefore retries 403 on a fresh proxy session, which most builds do not. No browser, no cookie mint, no challenge solve is needed anywhere.

What happens when something does go wrong

  • A non-200, or a 200 carrying a block page, is a transport failure, never end-of-data. Retried on a fresh proxy session, up to 4 attempts.
  • A body that arrives short is rejected before parsing. The __NEXT_DATA__ block sits near the end of a 890 KB document, so a body truncated mid-stream parses to "no data" rather than to an error — the validator feeds it a real 120,003-byte truncation to prove the rejection fires.
  • One unreadable auction does not abort the run; it is recorded in RUN_SUMMARY and the walk continues. The run only fails if more than half the auctions are unreadable — that is a block, not bad luck.
  • Deliberate aborts use Actor.fail(), never a bare throw (which the runtime's uncaught-exception net swallows, leaving a broken run reading SUCCEEDED).
  • Both walks are floor-asserted against the site's own published totals. A short read is refused rather than reported as a small site.

Two real runs, start to finish

Both on 2026-09-18 against the live site, through the Apify proxy at concurrency 5.

Run A — the default shape (maxLots: 40, detail off, closing-soonest):

board: 504 auctions (site reported 504), 11 pages, 11/11 HTTP 200
482 of 504 auctions passed the auction-level filters
40 lots delivered and charged, from 48 lot rows examined (1.2 examined per delivered)
40/40 carry a real close time; every core field 100% filled
24 seconds wall clock, container boot included

Run B — a deliberately selective filter (countryCodes: ["de"], closingWithinHours: 72, detail on, maxLots: 12):

the closing-time pre-filter skipped 426 auctions outright, plus 47 on country and 8 empty
~75,279 lots were never fetched at all
6 auctions opened, 1,071 lot rows examined, 12 delivered and charged (89.3 examined per delivered)
12/12 carry a real close time; 12 lot pages enriched, 0 unreadable
one transient NGHTTP2_REFUSED_STREAM, retried on a fresh session and succeeded

Run B is the expensive case on purpose — a filter that matches about 1% of what it reads. Even there the ratio is 89 examined rows per delivered row, against a break-even near 2,200: one lot-list request costs roughly $0.000037 of compute and returns 48 rows, so $0.0017 of revenue buys about 46 requests. The ratio is reported in RUN_SUMMARY as lotRowsExaminedPerDelivered on every run, so you can see it rather than take this on trust.


Billing

One event, lot-scraped, $0.0017 per row delivered. No start fee. No separate detail event.

Not charged, and kept apart from each other in RUN_SUMMARY so you can tell them apart:

  • lots rejected by your filters (never delivered),
  • whole auctions skipped by the closing-time pre-filter (never even fetched),
  • requested auctions that are no longer on the live board (reported by id),
  • lot pages that could not be read after retries (reported with the URL).

Actor.pushData(items, event) bills per item and respects your charge cap, so delivered equals charged. The cap is checked against rows produced, not rows flushed — a cap checked against flushed batches lags by a whole chunk.


RUN_SUMMARY

Written to the key-value store on every run. It reconciles what the site said was there against what was actually read:

  • live auctions the site reported vs. auctions collected vs. board pages read;
  • auctions opened, auctions skipped and why, and how many lots sat behind the skipped ones;
  • lot rows examined vs. delivered; delivered == charged;
  • everyOpenedAuctionWalkedToItsOwnTotal — a per-auction completeness flag;
  • closeTimeIntegrity — the share of delivered rows carrying a real close time, which is the claim this actor is built around;
  • fieldFillOnDeliveredRows — per-field fill counted on the rows this run actually produced, not quoted from this README;
  • the four kinds of nothing, listed above.

Honest limits

There is no consignor email, phone or company name, and there never will be. Consignors here are bankruptcy estates and liquidators, and the platform keeps them anonymous on purpose. Over 48 real lot pages: zero email or phone fields anywhere on the lot object. companyId is a numeric account id (16239), not a name — it is shipped as an id and labelled as one.

What is published, and what this actor ships instead, is the physical facility: street, postcode, city, country and facility name on 91.7% of lots, with inspection and collection windows, forklift availability and — on 50.0% — an email for arranging collection, plus 33.3% carrying a contact name, phone or collection deadline in collectionNotes. Those are logistics contacts at the handling or consigning company, not the beneficial seller, and they are labelled that way. On 8.3% of lots an auctioneer has typed an address into the free-text remarks; it is left there in context rather than promoted into a contact column.

condition is not a condition grade. The site publishes the field on 100% of lots, but across 48 real lot pages every value was either NOT_CHECKED (46) or NOT_APPLICABLE (2). It is returned verbatim because both store rivals advertise it and you will look for it — but do not build a condition filter on it. packaging and appearance carry exactly the same two placeholders and are cut rather than shipped as constant columns.

No all-in price estimate. The buyer's premium and VAT are revealed only inside the authenticated bid-confirmation flow — the site's own words are "before you confirm, you'll see a full overview of all amounts related to your bid, including buyer's premium and VAT". They are in no public byte this actor reads, so no premium, VAT or all-in figure is offered. currentBid is the hammer figure only. Budget roughly 25–45% on top; confirm against the auction's own terms.

No coordinates. lot.location publishes {city, countryCode} and nothing else — 0 of 48 lot pages carried a latitude or longitude. The collection street and postcode this actor ships geocode precisely; a fabricated point would not.

Closed and sold lots are out of scope. This actor reads the live board. The site's sitemaps carry ~205,000 mostly-closed lot URLs, and status query parameters do not reopen them on the auction page — a realized-price archive is a genuinely different product with a different enumeration, and pretending otherwise would ship a dataset whose close-time column is meaningless.

Fields cut for being empty, each with its count: videos (0 of 48), packaging and appearance (placeholder-only), onboarding (false on 48 of 48), bsc (an undocumented float with no published meaning), assignedExplicitly (internal auction-ops flag). documentUrls is only 8.3% full but is kept, because when a spec sheet or inspection report is attached it is worth having.

Bids arrive late. The site soft-closes: a bid in the final 2 minutes extends the lot by 2 minutes. A snapshot taken hours before close is inventory data, not a price. Schedule the run inside closingWithinHours if you want prices that mean something.

The board moves under you. totalSize read 507, then 506, then 505 over about an hour of probing. The walks tolerate 1% of drift and refuse anything worse.


Public listing data from www.troostwijkauctions.com. No login, no paywall, no personal data beyond the business collection contacts described above. This is an independent tool, not affiliated with or endorsed by TBAuctions, Troostwijk Auctions, Surplex or Vavato.

robots.txt at the time of building, quoted verbatim (also captured in fixtures/robots.txt):

User-agent: *
Allow: /
Disallow: */search*
Disallow: *categoryLevel=*
Disallow: *totalSize=*
Disallow: *countries=*
Disallow: *brands=*
Disallow: *auctions=*
Disallow: *amounts=*
Disallow: */login?callbackUrl=*
Disallow: */signup?callbackUrl=*

This actor reads three paths — /en/auctions?page=, /en/a/<slug> and /en/l/<slug> — and none of them matches a Disallow rule. Every filter in this actor is applied on our side, against rows the plain default listing already returned, precisely so that no request ever carries countries=, brands=, amounts=, categoryLevel=, totalSize= or the search path. The validator generates seven representative URLs from the real URL builders and tests each against all nine rules.


Verify it yourself

node offline_validate.mjs

No network, no Apify account, no npm install. It re-derives every number in this README from the real bytes in fixtures/ — the close-time asymmetry, every fill percentage, the route equivalence, the truncation rejection, the billing ordering, the schema/code agreement, and the sample row above.