eBay meets Odoo: wiring a second-hand price tracker in one session

From Non Compliant keyset to 65 live listings: Browse API credentials, a ref() bug, and the first real dispatch of four marketplace scrapers.

I have been building a multi-marketplace search system for second-hand products. The idea is simple: describe what you want (an NVIDIA RTX 3060 12GB, under 250 EUR), and four sub-skills fan out to Wallapop, Milanuncios, eBay Spain and Cash Converters. Results land as structured records in an Odoo 16 module called ltc_second_hand, scored, tagged, and ready for human triage. This post covers the session where the system ran end-to-end for the first time.

Kanban view of second-hand searchesThe search kanban: two active searches grouped by state, with listing counts and hot-lead badges.

The eBay keyset puzzle

The Browse API is the only public eBay search endpoint that still works. It uses OAuth 2.0 client credentials, no user tokens needed. Getting the keys should be a two-minute task. It was not.

Creating the app on developer.ebay.com went fine. The portal gave me an App ID (Client ID) immediately. But when I tried to copy the Cert ID (Client Secret), the page showed:

Your Keyset is currently disabled. Comply with marketplace deletion/account closure notification process or apply for an exemption.

eBay now requires every app to handle or opt out of Marketplace Account Deletion notifications. Since our system only reads public listings via client_credentials and stores zero eBay user data, the exemption toggle applies. One click on "Exempted from Marketplace Account Deletion", save, and the keyset unlocked.

First token, first search

With EBAY_APP_ID and EBAY_CERT_ID configured in ~/.config/ltc-second-hand/ebay.env, a quick smoke test confirmed the OAuth flow works: a 7200-second Application Access Token, and a Browse API query for "RTX 3060 12GB" on EBAY_ES returned 1,518 results. The scraper was ready.

Search form view with criteria and statsSearch form for RTX 3060 12GB: price target 250 EUR, max 300 EUR, keywords filter, 59 listings and 7 hot leads.

The ref() bug in the Anuncios view

When I tried to open the Anuncios menu item in Odoo, the page crashed. The error log was unhelpful at first, full of favicon 404s from a missing filestore entry. The real problem was in the ir.actions.act_window context:

<field name="context">{
    'search_default_hide_reserved': 1,
    'reserved_tag_id': ref('ltc_second_hand.tag_reserved'),
}</field>

The ref() helper only evaluates during XML data loading when used inside an eval attribute. Written as plain field text, it gets stored literally as the string ref('ltc_second_hand.tag_reserved'). When the web client tries to safe_eval that context, ref is not in scope and the action fails silently.

The fix is a one-line change: use eval on the field so the XML ID resolves to an integer at install time:

<field name="context" eval="{
    'search_default_hide_reserved': 1,
    'reserved_tag_id': ref('ltc_second_hand.tag_reserved'),
}"/>

The upgrade that did not upgrade

After editing the XML, I ran odoo-bin -u ltc_second_hand directly. Zero output, zero errors, exit code 0. But the view in the database still had the old context. Why?

The production config (odoo.conf) has no addons_path directive. The path is constructed by the /bin/odoo wrapper script, which loops over /opt/odoo/addons/* and prepends /opt/odoo/custom/apps. Calling odoo-bin directly skipped the wrapper, so the module directory was invisible. The log even said it: "module ltc_second_hand: no manifest file found". Lesson: always use /bin/odoo, never odoo-bin directly.

First real dispatch: 65 listings, 7 hot leads

With the view fixed and eBay credentials live, I launched the coordinator skill:

/ltc-second-hand
Busco una tarjeta grafica NVidia RTX 3060 12GB, presupuesto < 250 EUR

The dispatcher created a second.hand.search record in Odoo and fanned out to all four marketplaces in parallel. Results after 15 seconds:

MarketplaceStatusListings
Wallapopok40
eBayok19
Milanunciosblocked (Cloudflare)0
Cash Convertersok (no stock)0

Listings kanban grouped by marketplaceListings kanban grouped by marketplace: Wallapop, eBay and Cash Converters cards with thumbnails, prices and scores.

Seven listings scored above 0.85 and were flagged as hot leads. The best hit: an ASUS single-fan RTX 3060 12GB on eBay at 240 EUR, score 0.875. On Wallapop, prices started at 225 EUR for a bare Nvidia card and 230 EUR for a Zotac model. One listing at 70 EUR for an Aorus card screamed scam or defect.

Hot lead listing detailDetail of the top hot lead: ASUS RTX 3060 12GB at 240 EUR on eBay, score 0.875, tagged as shop, with one-click actions to open, mark relevant or noise.

What is not working yet

  • Milanuncios is blocked by Cloudflare on every run. The Playwright profile needs a manual session to establish clearance cookies.
  • Cash Converters returns zero results because the site uses Salesforce Commerce Cloud (Demandware) and the URL pattern needs adjustment.
  • Duplicate detection across searches is not implemented yet. Running the same query twice creates duplicate listing records.

Scraping sessions listScraping sessions: OK (green), Blocked by Cloudflare (orange), Error (red). Each row shows marketplace, duration, and listing count.

What I learned

  1. ref() in Odoo XML only resolves inside eval="...", never in plain field text content.
  2. eBay's Marketplace Account Deletion compliance gate blocks API access until you either set up a webhook or claim the exemption. For read-only Browse API apps, the exemption is the right path.
  3. The /bin/odoo wrapper is not optional on this server. Without it, the addons path is incomplete and module upgrades silently do nothing.
  4. A 70 EUR RTX 3060 on Wallapop is not a bargain. It is a trap.
How I customize my Odoo with Claude Code
From 108 attachments in a single task to a searchable chatter box: 200 lines of code, two hours, and the screw-ups I learned from.