<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: uzie</title>
    <description>The latest articles on DEV Community by uzie (@fauzimatakupan).</description>
    <link>https://dev.to/fauzimatakupan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196150%2Fb9140b46-9fd8-434b-9351-ee0299054ff2.png</url>
      <title>DEV Community: uzie</title>
      <link>https://dev.to/fauzimatakupan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fauzimatakupan"/>
    <language>en</language>
    <item>
      <title>An IDX/BEI financial panel: 4,408 company-years, 22 columns, and the caveats that matter</title>
      <dc:creator>uzie</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:17:27 +0000</pubDate>
      <link>https://dev.to/fauzimatakupan/an-idxbei-financial-panel-4408-company-years-22-columns-and-the-caveats-that-matter-2hck</link>
      <guid>https://dev.to/fauzimatakupan/an-idxbei-financial-panel-4408-company-years-22-columns-and-the-caveats-that-matter-2hck</guid>
      <description>&lt;p&gt;Indonesian listed-company data is public. Analysis-ready it is not.&lt;/p&gt;

&lt;p&gt;The Indonesia Stock Exchange (IDX/BEI) publishes financial statistics for every listed&lt;br&gt;
issuer, and anyone can open the pages one by one. The friction starts when you need a&lt;br&gt;
panel: the same company across five years, with consistent ratios, in one table you can&lt;br&gt;
feed to R or Stata. That is a data-engineering problem disguised as a download.&lt;/p&gt;

&lt;p&gt;I built that panel: &lt;strong&gt;4,408 company-year observations, 975 tickers, 11 IDX-IC sectors,&lt;br&gt;
22 columns, 2021–2025&lt;/strong&gt;. This post documents what broke, what I normalized, and what the&lt;br&gt;
numbers actually mean — including the parts that make the dataset weaker than a sales page&lt;br&gt;
would suggest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things that make IDX data awkward
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Each yearly bucket is a snapshot, not a fiscal year.&lt;/strong&gt; When you pull a year, you get&lt;br&gt;
whatever statement IDX had at collection time — predominantly the Q3 (30 September) report.&lt;br&gt;
The column &lt;code&gt;fs_date&lt;/code&gt; is in the dataset for exactly this reason. If your research requires&lt;br&gt;
audited full-year figures, this panel is not a substitute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ratio scales were not stable.&lt;/strong&gt; For 2021–2022 the source stores ROA, ROE, and NPM as&lt;br&gt;
decimals; from 2023 onward they are percentages. Mixing them silently produces averages&lt;br&gt;
that are wrong by two orders of magnitude. I normalized 2021–2022 ×100 and kept the flag in&lt;br&gt;
the codebook. Those two years remain &lt;em&gt;preliminary&lt;/em&gt;: they have not been reconciled line by&lt;br&gt;
line against audited reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The panel is unbalanced.&lt;/strong&gt; Coverage grows as more companies list:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Observations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;769&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;828&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;906&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;947&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;958&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That growth is real, not a bug. It also means naive year-over-year comparisons mix&lt;br&gt;
composition changes with performance changes. Fixed-effects models handle it; a simple&lt;br&gt;
mean by year does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Missing is not zero.&lt;/strong&gt; An empty cell means IDX did not publish that figure. Filling it&lt;br&gt;
with &lt;code&gt;0&lt;/code&gt; destroys every ratio built on it. The panel keeps nulls, and the codebook says so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Valuation multiples move with the snapshot.&lt;/strong&gt; &lt;code&gt;per&lt;/code&gt; and &lt;code&gt;pbv&lt;/code&gt; are trailing values that&lt;br&gt;
depend on the share price on collection day. They are useful as context, not as a clean&lt;br&gt;
time series.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in the panel
&lt;/h2&gt;

&lt;p&gt;Each row is one company-year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; &lt;code&gt;kode_saham&lt;/code&gt;, &lt;code&gt;nama_emiten&lt;/code&gt;, &lt;code&gt;sektor&lt;/code&gt;, &lt;code&gt;subsektor&lt;/code&gt;, &lt;code&gt;tahun&lt;/code&gt;, &lt;code&gt;fs_date&lt;/code&gt;,
&lt;code&gt;audit (U/A)&lt;/code&gt;, &lt;code&gt;sharia&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profitability and leverage:&lt;/strong&gt; &lt;code&gt;roa_pct&lt;/code&gt;, &lt;code&gt;roe_pct&lt;/code&gt;, &lt;code&gt;der&lt;/code&gt;, &lt;code&gt;npm_pct&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-share and valuation:&lt;/strong&gt; &lt;code&gt;eps_idr&lt;/code&gt;, &lt;code&gt;book_value&lt;/code&gt;, &lt;code&gt;pbv&lt;/code&gt;, &lt;code&gt;per&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size:&lt;/strong&gt; &lt;code&gt;assets_miliar_idr&lt;/code&gt;, &lt;code&gt;liabilities_miliar_idr&lt;/code&gt;, &lt;code&gt;equity_miliar_idr&lt;/code&gt;,
&lt;code&gt;sales_miliar_idr&lt;/code&gt;, &lt;code&gt;profit_attr_owner_miliar_idr&lt;/code&gt;, &lt;code&gt;firm_size_lnassets&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 11 sectors follow IDX-IC: Financials, Energy, Basic Materials, Industrials, Consumer&lt;br&gt;
Non-Cyclicals, Consumer Cyclicals, Healthcare, Properties &amp;amp; Real Estate, Technology,&lt;br&gt;
Infrastructures, and Transportation &amp;amp; Logistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design choices I would defend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep the source date and the audit flag.&lt;/strong&gt; A panel without &lt;code&gt;fs_date&lt;/code&gt; and &lt;code&gt;audit&lt;/code&gt; invites&lt;br&gt;
the reader to assume audited December figures. Most rows are neither.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalize loudly.&lt;/strong&gt; The ×100 correction is documented in the codebook and repeated in the&lt;br&gt;
listing. A silent fix is worse than a visible caveat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not coerce missing values.&lt;/strong&gt; &lt;code&gt;null&lt;/code&gt; propagates honestly; &lt;code&gt;0&lt;/code&gt; invents a fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate portal-computed multiples from your own.&lt;/strong&gt; &lt;code&gt;pbv&lt;/code&gt; and &lt;code&gt;per&lt;/code&gt; come from the source&lt;br&gt;
and will disagree at the edges with &lt;code&gt;price / book value&lt;/code&gt; computed later. Label which is&lt;br&gt;
which.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is good for — and what it is not
&lt;/h2&gt;

&lt;p&gt;Good for: panel regressions with firm and year effects, sector comparisons, teaching&lt;br&gt;
econometrics with real data, and quick descriptive work on Indonesian issuers.&lt;/p&gt;

&lt;p&gt;Not good for: audited full-year analysis, investment advice, event studies that need&lt;br&gt;
announcement dates, or market-wide estimates from a 25-row sample. A sample validates a&lt;br&gt;
schema; it cannot estimate a market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect it first
&lt;/h2&gt;

&lt;p&gt;A free 25-row sample with all 22 columns is here — no signup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sample CSV:
&lt;a href="https://fauzimatakupan.github.io/indonesian-financial-data/downloads/bei-panel-sample-2026-11-07.csv" rel="noopener noreferrer"&gt;https://fauzimatakupan.github.io/indonesian-financial-data/downloads/bei-panel-sample-2026-11-07.csv&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository and column notes: &lt;a href="https://github.com/FauziMatakupan/indonesian-financial-data" rel="noopener noreferrer"&gt;https://github.com/FauziMatakupan/indonesian-financial-data&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample uses 25 well-known tickers and the latest year, with values copied verbatim.&lt;br&gt;
If the schema fits your work, the full panel adds all 4,408 rows, a quarterly version&lt;br&gt;
(16,138 observations across 20 periods), per-sector files, a codebook, and runnable&lt;br&gt;
R/Stata/SPSS syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I assembled this dataset and sell the full version. The sample and the&lt;br&gt;
documentation above are free, and the limitations are the same in both.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>finance</category>
      <category>dataset</category>
      <category>indonesia</category>
    </item>
    <item>
      <title>Dubai property listings: a schema for price-per-sqm analysis</title>
      <dc:creator>uzie</dc:creator>
      <pubDate>Sat, 07 Nov 2026 08:57:01 +0000</pubDate>
      <link>https://dev.to/fauzimatakupan/dubai-property-listings-a-schema-for-price-per-sqm-analysis-3634</link>
      <guid>https://dev.to/fauzimatakupan/dubai-property-listings-a-schema-for-price-per-sqm-analysis-3634</guid>
      <description>&lt;p&gt;Dubai property data looks easy until you try to compare two listings. One portal shows&lt;br&gt;
"2BR, Dubai Marina, AED 2,100,000"; the next shows "2 Beds, 1,180 sqft, Marina, 2.1M".&lt;br&gt;
Same building, different fields, no shared key. Multiply that by a few thousand listings&lt;br&gt;
and any price-per-sqm analysis becomes a manual spreadsheet exercise.&lt;/p&gt;

&lt;p&gt;This post documents the schema I settled on for Dubai listings, the parts that actually&lt;br&gt;
matter for analysis, and a free 40-row sample you can inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why listing data is harder than it looks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The listing id is not the property id.&lt;/strong&gt; The same unit can be re-listed by two
brokers with different ids. Dedupe on the portal id, then cluster on
coordinates + size + bedrooms if you need property-level counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price is not comparable across transaction types.&lt;/strong&gt; A sale price and a yearly rent
both look like integers. Without &lt;code&gt;transactionType&lt;/code&gt; and &lt;code&gt;pricePeriod&lt;/code&gt;, averages are
meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size is published in both units, inconsistently.&lt;/strong&gt; Some records carry sqft, some
sqm, some both rounded differently. Keep both and store the conversion you used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community metrics are snapshots.&lt;/strong&gt; "Average community price" is computed by the
portal at collection time. It is useful context, not a time series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broker contact data is personal data.&lt;/strong&gt; If you publish a sample, strip names,
phones, emails, and full descriptions. The listing facts are enough for analysis.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The schema
&lt;/h2&gt;

&lt;p&gt;Each record is one listing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;listingId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stable dedupe key across runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Canonical source for verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;price&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;pricePeriod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Comparable only when read together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transactionType&lt;/code&gt;, &lt;code&gt;propertyType&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sale vs rent; apartment vs villa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bedrooms&lt;/code&gt;, &lt;code&gt;bathrooms&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Studio = &lt;code&gt;bedrooms: 0&lt;/code&gt;, not null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sizeSqft&lt;/code&gt;, &lt;code&gt;sizeSqm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Both units, no silent conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pricePerArea&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Portal value when present; &lt;code&gt;null&lt;/code&gt; otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;city&lt;/code&gt;, &lt;code&gt;community&lt;/code&gt;, &lt;code&gt;subCommunity&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Location hierarchy for grouping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;completionStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ready vs off-plan changes the price logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;isVerified&lt;/code&gt;, &lt;code&gt;isExclusive&lt;/code&gt;, &lt;code&gt;isNewConstruction&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Source flags, not guarantees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Approximate; useful for cluster checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;listedDate&lt;/code&gt;, &lt;code&gt;scrapedAt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Snapshot integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;communityAvgPrice&lt;/code&gt;, &lt;code&gt;communityPriceTrend12mPct&lt;/code&gt;, &lt;code&gt;communityActiveListings&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Context at collection time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rera&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permit string when the portal publishes it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two design choices I would defend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never coerce a missing price to zero.&lt;/strong&gt; A zero in a price column silently destroys
every average. Use &lt;code&gt;null&lt;/code&gt; and filter explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep portal-computed fields separate from your own.&lt;/strong&gt; &lt;code&gt;pricePerArea&lt;/code&gt; from the portal
and &lt;code&gt;price / sizeSqft&lt;/code&gt; from your pipeline will disagree at the edges; label which is
which.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;p&gt;A real 40-row snapshot collected on 6 November 2026 from public PropertyFinder search&lt;br&gt;
results (sale and rent). Broker names, emails, phones, and descriptions are removed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON: &lt;a href="https://fauzimatakupan.github.io/indonesian-marketplace-data/downloads/dubai-sample-2026-11-06.json" rel="noopener noreferrer"&gt;https://fauzimatakupan.github.io/indonesian-marketplace-data/downloads/dubai-sample-2026-11-06.json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CSV: &lt;a href="https://fauzimatakupan.github.io/indonesian-marketplace-data/downloads/dubai-sample-2026-11-06.csv" rel="noopener noreferrer"&gt;https://fauzimatakupan.github.io/indonesian-marketplace-data/downloads/dubai-sample-2026-11-06.csv&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schema: &lt;a href="https://fauzimatakupan.github.io/indonesian-marketplace-data/downloads/dubai-property-listing.schema.json" rel="noopener noreferrer"&gt;https://fauzimatakupan.github.io/indonesian-marketplace-data/downloads/dubai-property-listing.schema.json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/FauziMatakupan/indonesian-marketplace-data" rel="noopener noreferrer"&gt;https://github.com/FauziMatakupan/indonesian-marketplace-data&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample is small on purpose. Forty rows validate a schema; they cannot estimate the&lt;br&gt;
Dubai market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and ethics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Public listings only. No login, no private data, no broker contact details.&lt;/li&gt;
&lt;li&gt;Asking prices, not transaction prices.&lt;/li&gt;
&lt;li&gt;Listings change daily; a snapshot is not a feed.&lt;/li&gt;
&lt;li&gt;Respect the portal's terms and applicable data-protection rules. Do not republish
personal data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I packaged
&lt;/h2&gt;

&lt;p&gt;I turned the collection into an Apify Actor so pagination, normalization, and dedupe are&lt;br&gt;
reusable:&lt;br&gt;
&lt;a href="https://apify.com/matakupanlabs/dubai-property-listings-scraper" rel="noopener noreferrer"&gt;https://apify.com/matakupanlabs/dubai-property-listings-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: this is my Actor and it is pay-per-listing. The sample and schema are free.&lt;br&gt;
If you are building something similar, I am happy to compare notes on deduplication or&lt;br&gt;
price-per-sqm normalization.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>dataset</category>
      <category>realestate</category>
      <category>api</category>
    </item>
    <item>
      <title>Scraping OLX Indonesia past Akamai: architecture notes and a 25-row sample dataset</title>
      <dc:creator>uzie</dc:creator>
      <pubDate>Tue, 03 Nov 2026 11:59:45 +0000</pubDate>
      <link>https://dev.to/fauzimatakupan/scraping-olx-indonesia-past-akamai-architecture-notes-and-a-25-row-sample-dataset-11jd</link>
      <guid>https://dev.to/fauzimatakupan/scraping-olx-indonesia-past-akamai-architecture-notes-and-a-25-row-sample-dataset-11jd</guid>
      <description>&lt;p&gt;OLX Indonesia (&lt;code&gt;olx.co.id&lt;/code&gt;) is one of the largest classified marketplaces in Southeast Asia, but it is also one of the harder Indonesian sources to collect reliably. Search and listing pages sit behind an Akamai interstitial, and the data you want is not in the initial HTML — it is fetched by the page's own JavaScript after the challenge is cleared.&lt;/p&gt;

&lt;p&gt;This article documents the architecture that worked for me, the normalized schema I settled on, and a small real sample dataset you can inspect. It is written for data engineers who need Indonesian marketplace data and for anyone evaluating whether a scraper is worth building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why plain HTTP and headless browsers fail
&lt;/h2&gt;

&lt;p&gt;The first attempts were the obvious ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plain &lt;code&gt;fetch&lt;/code&gt; / &lt;code&gt;requests&lt;/code&gt; — returned the interstitial or an empty shell;&lt;/li&gt;
&lt;li&gt;server-side rendered parsing — the listing JSON was not in the document;&lt;/li&gt;
&lt;li&gt;headless Chrome without a real browsing context — the challenge sometimes passed, but the listing request was never captured correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is familiar: the challenge is not the final boss. The final boss is the JSON request the page makes &lt;strong&gt;after&lt;/strong&gt; the challenge, and that request only exists if the page is allowed to run like a normal browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked: run a real browser, intercept the response
&lt;/h2&gt;

&lt;p&gt;The working approach was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Launch a real browser context with a normal desktop fingerprint.&lt;/li&gt;
&lt;li&gt;Navigate to the public search URL.&lt;/li&gt;
&lt;li&gt;Let the page solve the interstitial naturally — no captcha-solving service, no login.&lt;/li&gt;
&lt;li&gt;Intercept the JSON response the page's own frontend fetches.&lt;/li&gt;
&lt;li&gt;Normalize the response into a flat record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified sketch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// map payload -&amp;gt; normalized listing&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not a secret endpoint. It is the order of operations: &lt;strong&gt;real browser → challenge → page JavaScript → response interception&lt;/strong&gt;. A proxy alone does not fix this; a different user agent does not fix this; parsing harder does not fix this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema
&lt;/h2&gt;

&lt;p&gt;I normalized each listing into a flat record. The exact fields depend on the marketplace, but this shape has been stable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Listing ID (stable key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Listing title&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adDescription&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seller-written description&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asking price in IDR (integer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;priceDisplay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Price as displayed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;condition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bekas&lt;/code&gt; / &lt;code&gt;Baru&lt;/code&gt; when provided&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;itemType&lt;/code&gt;, &lt;code&gt;categoryId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Marketplace taxonomy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;country&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;district&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Location hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sellerType&lt;/code&gt;, &lt;code&gt;sellerKyc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Seller account type / platform flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;favorites&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Saves at collection time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createdAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Listing creation timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;isNew&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Listing flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Canonical public listing URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search query used&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two design choices worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the marketplace ID as the stable key.&lt;/strong&gt; Titles change, prices change, and listings get republished. &lt;code&gt;adId&lt;/code&gt; is what lets you deduplicate across runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate the display price from the numeric price.&lt;/strong&gt; &lt;code&gt;priceDisplay&lt;/code&gt; is useful for QA; &lt;code&gt;price&lt;/code&gt; is what your pipeline should aggregate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The sample dataset
&lt;/h2&gt;

&lt;p&gt;I published a real 25-row snapshot (not synthetic) collected on 3 November 2026 from a public search for &lt;code&gt;mechanical keyboard&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON: &lt;a href="https://gist.githubusercontent.com/FauziMatakupan/0b420c5dea9a55677db65cff79dac196/raw/olx_indonesia_sample_2026-11-03.json" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/FauziMatakupan/0b420c5dea9a55677db65cff79dac196/raw/olx_indonesia_sample_2026-11-03.json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CSV: &lt;a href="https://gist.githubusercontent.com/FauziMatakupan/0b420c5dea9a55677db65cff79dac196/raw/olx_indonesia_sample_2026-11-03.csv" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/FauziMatakupan/0b420c5dea9a55677db65cff79dac196/raw/olx_indonesia_sample_2026-11-03.csv&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schema notes: &lt;a href="https://gist.github.com/FauziMatakupan/0b420c5dea9a55677db65cff79dac196" rel="noopener noreferrer"&gt;https://gist.github.com/FauziMatakupan/0b420c5dea9a55677db65cff79dac196&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample omits seller names and exact coordinates. Descriptions are seller-written and may contain typos. Prices are asking prices, not transaction prices. It is a schema sample, not a representative market sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and ethics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public data only.&lt;/strong&gt; No login, no private messages, no seller contact details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect the platform.&lt;/strong&gt; Read the terms of service and applicable data-protection rules (UU PDP in Indonesia). Do not republish personal data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect drift.&lt;/strong&gt; Classified sites change their frontend often. A schema that survives a redesign is worth more than a clever selector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small samples lie.&lt;/strong&gt; Twenty-five rows can validate a schema; they cannot estimate a market.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I packaged
&lt;/h2&gt;

&lt;p&gt;I turned this architecture into an Apify Actor so the browser lifecycle, challenge handling, pagination, and normalization are reusable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/matakupanlabs/olx-indonesia-listings-scraper" rel="noopener noreferrer"&gt;https://apify.com/matakupanlabs/olx-indonesia-listings-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: this is my Actor and it is pay-per-listing. The sample dataset and schema notes are free. If you are building something similar, I am happy to compare notes on fields, deduplication, or handling large runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would you add?
&lt;/h2&gt;

&lt;p&gt;For marketplace price monitoring, the interesting question is time: is &lt;code&gt;createdAt&lt;/code&gt; enough, or do you need first-seen / last-seen fields to build a real price time series? I would like to hear how other people model republished listings and seller churn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;The schemas and sample datasets also live in a public GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/FauziMatakupan/indonesian-marketplace-data" rel="noopener noreferrer"&gt;https://github.com/FauziMatakupan/indonesian-marketplace-data&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It includes JSON Schema files, ethics notes, and a landing page:&lt;br&gt;
&lt;a href="https://fauzimatakupan.github.io/indonesian-marketplace-data/" rel="noopener noreferrer"&gt;https://fauzimatakupan.github.io/indonesian-marketplace-data/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
  </channel>
</rss>
