The sanctioned, default method for any eBay listing fetch, price comparison, or search task at BOSSTORQUE. Server-to-server OAuth2 + modern Browse API. No browser scraping. No deprecated Finding API. Credentials never leave the Cloudflare Worker.
The old approach scraped eBay search HTML or hit the deprecated Finding API (svcs.ebay.com). Both are dead ends. The Browse API is what eBay's own partner integrations use — richer data in one call, with proper authentication. This doc locks in the architecture so every future task and code build starts from the same place.
Worker hits api.ebay.com/identity/v1/oauth2/token with Basic auth (App ID + Cert ID), caches the bearer token for ~2 hours, then GETs api.ebay.com/buy/browse/v1/item_summary/search with that token and X-EBAY-C-MARKETPLACE-ID: EBAY_US. Credentials live as Cloudflare Pages secrets on always-pay-less, never in source or frontend.
PRD prefix on credentials means real listings. Sandbox returns fake data — never useful.env.EBAY_CLIENT_ID / env.EBAY_CLIENT_SECRET. Never inline in source, never log, never expose via frontend.| Field | Value |
|---|---|
| App ID (Client ID) | BOSSTORQ-alwayspa-PRD-56c1d3885-30e85485 |
| Cert ID (Client Secret) | Stored as EBAY_CLIENT_SECRET Pages secret. Local reference at ~/Documents/.claude/memory/context/api-keys.md. |
| Environment | Production (PRD) |
| Marketplace | EBAY_US |
| Scope | https://api.ebay.com/oauth/api_scope |
| OAuth endpoint | https://api.ebay.com/identity/v1/oauth2/token |
| Browse search endpoint | https://api.ebay.com/buy/browse/v1/item_summary/search |
| CF Account ID | 8cef3a20d2c22491d2bbbc594cf4865d |
| CF Pages project | always-pay-less |
| Worker file | ~/Documents/GitHub/always-pay-less/public/_worker.js |
If keys ever roll or you spin up a new Pages project that needs eBay access, set the secrets via wrangler. Never paste them into source.
npx wrangler pages secret put EBAY_CLIENT_ID --project-name always-pay-less
npx wrangler pages secret put EBAY_CLIENT_SECRET --project-name always-pay-less
Confirm they're set in the right scope:
npx wrangler pages secret list --project-name always-pay-less
let _ebayTokenCache = null;
async function getEbayToken(env) {
const now = Date.now();
if (_ebayTokenCache && _ebayTokenCache.expiresAt > now + 60_000) {
return _ebayTokenCache.token;
}
const basic = btoa(`${env.EBAY_CLIENT_ID}:${env.EBAY_CLIENT_SECRET}`);
const r = await fetch("https://api.ebay.com/identity/v1/oauth2/token", {
method: "POST",
headers: {
"Authorization": `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded"
},
body: "grant_type=client_credentials&scope=" +
encodeURIComponent("https://api.ebay.com/oauth/api_scope")
});
if (!r.ok) throw new Error(`eBay OAuth ${r.status}`);
const data = await r.json();
_ebayTokenCache = {
token: data.access_token,
expiresAt: now + (data.expires_in - 300) * 1000
};
return data.access_token;
}
async function searchEbayBrowse(env, query, opts = {}) {
const { limit = 30, conditions = ["NEW"], priceMin = 10 } = opts;
const token = await getEbayToken(env);
const filter = [
`price:[${priceMin}..]`,
`priceCurrency:USD`,
`conditions:{${conditions.join("|")}}`
].join(",");
const url = `https://api.ebay.com/buy/browse/v1/item_summary/search`
+ `?q=${encodeURIComponent(query)}&limit=${limit}`
+ `&filter=${encodeURIComponent(filter)}`;
let r = await fetch(url, {
headers: {
"Authorization": `Bearer ${token}`,
"X-EBAY-C-MARKETPLACE-ID": "EBAY_US"
}
});
// Token expired mid-flight — refresh once and retry
if (r.status === 401) {
_ebayTokenCache = null;
const fresh = await getEbayToken(env);
r = await fetch(url, {
headers: {
"Authorization": `Bearer ${fresh}`,
"X-EBAY-C-MARKETPLACE-ID": "EBAY_US"
}
});
}
if (!r.ok) return []; // Quota exhausted or unrecoverable
const data = await r.json();
return (data.itemSummaries || []).map(it => ({
title: it.title,
price: parseFloat(it.price?.value || "0"),
shipping: parseFloat(it.shippingOptions?.[0]?.shippingCost?.value || "0"),
total: parseFloat(it.price?.value || "0") +
parseFloat(it.shippingOptions?.[0]?.shippingCost?.value || "0"),
condition: normalizeEbayCondition(it.condition, it.conditionId),
seller: it.seller?.username,
feedbackScore: it.seller?.feedbackScore,
feedbackPct: it.seller?.feedbackPercentage,
url: it.itemWebUrl,
location: it.itemLocation?.country,
image: it.image?.imageUrl
}));
}
| ID | Label | Normalized |
|---|---|---|
1000 | New | NEW |
1500 | New other (open box) | NEW_OTHER |
2000 | Certified pre-owned | CPO |
2500 | Seller refurbished | SELLER_REFURB |
2750 | Like new | VERY_GOOD |
3000 | Used | USED_GOOD |
4000 | Very good | VERY_GOOD |
5000 | Good | USED_GOOD |
6000 | Acceptable | USED_ACCEPTABLE |
7000 | For parts | FOR_PARTS |
If you need to verify the keys are alive without touching the worker, run this from a terminal where the credentials are exported as env vars:
curl -s -X POST -u "$EBAY_CLIENT_ID:$EBAY_CLIENT_SECRET" \
"https://api.ebay.com/identity/v1/oauth2/token" \
-d "grant_type=client_credentials&scope=https://api.ebay.com/oauth/api_scope" | jq .access_token
If that returns a long bearer token string, auth is healthy. If it returns an error, check that the keys haven't rolled and that the keyset is enabled in the eBay Developer dashboard.
eBay OAuth 401 or HTTP 429 (quota).npx wrangler pages secret list --project-name always-pay-less.Server-to-server means the credentials never leave the Cloudflare Worker. The browser sees only the JSON results — never the API key, never the bearer token. The user never authenticates anything; we're hitting eBay's public listing data as a registered developer. Same architecture eBay uses for partner integrations.
The Browse API also returns far more useful fields than the old Finding API in a single call: seller usernames, feedback scores, shipping breakdown, condition enum, listing URLs, location, watchers. That's why falling back to HTML scraping should only happen when the API is genuinely down — the API is just better.
The current always_pay_less worker doesn't yet have the 401-retry block from the snippet above. If the cached token times out mid-search, the request 401s and the user sees zero eBay results until the next Worker isolate cold-start. Patch this next time the worker is touched.
~/Documents/.claude/skills/ebay-browse-api/SKILL.md~/Documents/.claude/memory/context/api-keys.md~/Documents/.claude/CLAUDE.md — "eBay Browse API — Headless Access (Global Default)"~/Documents/GitHub/always-pay-less/public/_worker.jsalways-pay-less · Account 8cef3a20d2c22491d2bbbc594cf4865d