Skip to main content

August 12, 2026

eToro search 404: query fields, not ticker

A live eToro rebalance returned 200 with zero orders. Search 404ed on a bad fields list. I keep the ticker map and treat an empty 200 as the miss.

Sander Korf4 min read
etoroapiredis

The live eToro rebalance job resolves tickers through GET /market-data/search before it places orders. Redis holds a ticker-to-instrumentId map so repeat symbols do not hammer search. The operator watching the account sees HTTP 200 at the job, zero orders, and logs full of 404. A bad fields list made every name look missing. Then I discarded the Redis map on a miss, so a Tuesday outage could empty the book.

The job didn't crash and didn't time out. It returned a polite success that did nothing. Every ticker hit GET /market-data/search and came back HTTP 404. resolvedCount=0. I briefly assumed the catalog had vanished overnight. The instruments were still there. My query string was not.

A 404 means the query string is illegal

On eToro search, a real miss is 200 plus an empty items array. A 404 means I asked search a question it refuses to parse, same class of error as a bad sort. The whole request dies. One unknown field name, and every ticker looks missing.

GET /market-data/search requires fields. I sent a projection with mixed casings: instrumentID in one place, instrumentId in another, plus internalSymbolFull when I remembered the docs. eToro doesn't skip the unknown one. It 404s the call. I treated that as "ticker not found" and kept walking the list. Every row failed the same way, so the book stayed empty and the 200 at the top of the job looked fine.

Then I stacked a second bug. URLSearchParams encodes commas as %2C. eToro wants a literal comma in fields=instrumentId,internalSymbolFull. Encoded commas look like another malformed projection. Same 404. Same empty book.

The first 404 also threw. Later search strategies never ran. I had written a fallback for unknown tickers. It never got a chance to try, because I aborted on the first "not found" that was actually a bad request.

Documented fields, then the next strategy

I send documented field names only. If the docs say instrumentId and internalSymbolFull, those are the only spellings that exist. Alternate casings aren't aliases. They're unknown fields.

I rewrite %2C back to , after URLSearchParams. The browser helper is fine until eToro is picky about list separators:

function encodeSearchQuery(params: Record<string, string>): string {
	const query = new URLSearchParams();
	for (const [k, v] of Object.entries(params)) query.set(k, v);
	return query.toString().replaceAll('%2C', ',');
}

On 404, I retry once with a minimal field set. If that still 404s, I return an empty match and try the next strategy. One ugly projection shouldn't abort the job.

In the first demo, flip between a legal fields list and a typo that 404s. Watch how a bad projection zeroes every ticker even though the catalog still exists. The job still returns 200 at the top; the miss is in the empty book.

eToro search query encoder

URLSearchParams turns list commas into %2C. eToro GET /market-data/search wants a literal comma in fields. A 404 is a bad projection, not a missing ticker.

URLSearchParams

fields=instrumentId%2CinternalSymbolFull

After rewrite

fields=instrumentId,internalSymbolFull

HTTP 404Whole request rejected. Same class as a bad sort.

An eToro search 404 is often my fields list. I treat it as a bad request I can recover from, not as a ticker that vanished.

Redis stale cache of eToro instrument IDs is a feature

The same morning those 404s landed, I had already thrown away the fallback I needed.

I kept a Redis map of ticker to eToro instrument ID. After about 73 days I marked it stale and discarded it, which is what I'd do for a session token or a price quote. An eToro instrument ID doesn't rotate. eToro assigned 22 to AAPL years ago and it is still 22. Age is not a validity signal.

When I discarded the Redis map, I forced every resolve through live eToro search. Search was 404ing. Every ticker missed. Coverage went to zero. The old map was still valid. I discarded truth because a clock said it was old.

New policy: Redis reads return the stale map. Live eToro search only fills missing tickers and refreshes updatedAt. An eToro search outage can't zero the book. Tests flipped from stale → null to stale → { AAPL: 22 }.

if (ageMs > INSTRUMENT_LOOKUP_CACHE_MAX_AGE_MS) {
	log.info('instrument_lookup_cache_stale', { ageMs, symbolCount, updatedAt });
	return payload.symbolToInstrument; // still trade
}

I still log the age and still try to refresh. I don't evict a map of identifiers that don't change. Stale here means I haven't confirmed lately, not that the ids are wrong. For a lookup table of stable eToro IDs, the Redis TTL is a reminder to refresh. It doesn't license a delete.

In the second demo, toggle an eToro search outage against discard-stale vs serve-stale Redis policy. Same outage, different coverage: evict-on-stale leaves the book at zero; serve-stale keeps trading on cached instrument IDs.

Stale lookup map, 73 days old. Search outage plus discard-stale zeros the book. Serve-stale keeps the IDs that never rotate.
Search API
Cache policy

Coverage 0 / 6

Stale map discarded. Live search 404s. Book is empty.

Resolved eToro instrument IDs for the current policy
TickerInstrument IDSource
AAPL—unresolved
MSFT—unresolved
GOOG—unresolved
AMZN—unresolved
TSLA—unresolved
NVDA—unresolved

Happy coding! Sander