Sander Korf
  • Portfolio
  • Resume

Sander Korf

Dutch Full Stack AI Engineer helping businesses cut costs through intelligent automation. 13+ years turning expensive manual processes into systems that work 24/7. Based in Amsterdam, available for freelance projects.

Navigation

  • Portfolio
  • Resume

Legal

  • Cookies
  • Privacy Policy
  • Terms and Conditions

Blog

Algolia

  • Algolia InstantSearch INP and object identity
  • Algolia merch by Sanity _type, not title
  • Algolia InstantSearch search key, not admin

Vercel

  • Next.js empty Suspense fallbacks wreck CLS P95
  • Vercel Workflows: skip closed eToro legs

eToro

  • eToro deploy vs rotate: idle cash not rotate
  • eToro search 404: query fields, not ticker
  • eToro v2 opens at-most-once, settlement
  • eToro LLM overlay ±10pp: critic cannot veto

Next.js

  • next-intl useTranslate skip links above fold
  • next-intl owns Klaviyo nl-NL email copy

Firebase & Expo

  • Staging deep links must not hijack production
  • Turn off Worklets Bundle Mode for EAS SHA-1
  • Reclaim Firebase orphan without password wipe

Centra & Klaviyo

  • Centra to Klaviyo tags need a plugin contract

About

  • Full Stack AI Engineer in Amsterdam

© 2026 Sander Korf. All rights reserved

94719489

  • Applied AI
  1. Home
  2. Blog
  3. eToro v2 opens at-most-once, settlement

eToro v2 opens at-most-once, settlement

Open and close may use different API versions. I never retry an open after a timeout or I double-buy; settlement must match the instrument.

Sander KorfPublished August 12, 20265 min read
etoroapi

This is eToro execution, not a paper notebook

v2 open. v1 close. Live eToro tickets. settlementType has to match the leverage config. If you retry an open timeout, you can buy twice. If you default settlement to real, a CFD name can reject or open the wrong thing.

What they would notice if it failed

Two fills for one intent. Or a close that still works while the new open path is a mess. Or a leverage-1 name that suddenly wants a settlement you did not mean. The account is the demo.

Ship the eToro open. Leave the close.

I moved eToro opens to the documented v2 execution path. Closes stayed on v1. That mix is not sloppy leftover work. It is the cut I wanted.

v2 open is the path eToro now documents: POST /api/v2/trading/execution/orders with action: open, transaction of buy or sellShort, orderType: mkt, plus instrumentId, leverage, and amount. settlementType is optional. I did not invent a hostname. The path is enough.

Closes still hit POST /api/v1/trading/execution/market-close-orders/positions/{positionId} with InstrumentId and UnitsToDeduct. Same body. Same route. Same eToro risk I already understand.

Changing both in one pull request doubles the blast radius: two request shapes, two failure modes, two places an eToro timeout can lie. I staged the open. The close waits until the new open has lived in production.

// open (v2), at-most-once: do not retry timeouts/5xx
POST /api/v2/trading/execution/orders
{ action: 'open', transaction: 'buy' | 'sellShort', instrumentId, orderType: 'mkt', leverage, amount }
 
// close (v1, unchanged)
POST /api/v1/trading/execution/market-close-orders/positions/{positionId}
{ InstrumentId, UnitsToDeduct }

Timeouts on eToro open are not retries

A timeout or a 5xx on an eToro open is not a "try again." eToro may have filled and dropped the response. You get no id. You send the same open again, you maybe get a second fill.

Missing positions are recoverable. You reconcile later and place a fresh order on purpose. Duplicates are messy. I treat eToro open as at-most-once: a 2xx with an id is done. A 4xx, except a rate-limit, is a hard fail. Timeout or 5xx is ambiguous. Mark it. Do not send that open again.

Close is a different animal. You already have a position id. A retry there does not invent a second position the way a retried open can. I am still not changing that eToro path in this cut. One side. One risk.

Paper mode short-circuits both

Paper mode never hits the live eToro routes. Both open and close return a receipt that looks like paper-${uuid}. Same client code. Fake fill at the edge. I can exercise the mixed-version eToro client without paying for a real fill, and without teaching tests to retry a timeout they should never retry.

Preferring real settlementType is a trap

settlementType on that v2 open is optional. Treating real as the honest default is still a trap.

I used to send settlementType: real whenever eToro eligibility listed a real row. Real stock. Real custody. An eToro real config often exists only at 1x. The same instrument at 5x is a different product. Eligibility lists that ticket as CFD. If I send settlementType: real on a levered eToro order that only matches CFD, I get a silent reject, or I open something I did not model. Same symbol. Wrong settlementType. Wrong risk.

Crypto and commodities made the rule obvious. Those eToro books are typically CFD only. There is no real row to fall back on. Equities hid the same constraint behind a 1x real config that looks like a default and is not.

I do not pick settlementType from the first real-looking eToro row. I filter leverageConfigs by side and leverage, then choose CFD vs real from that slice.

export function preferredSettlementType(
	eligibility: Eligibility,
	side: Side,
	leverage: number
): 'cfd' | 'real' | undefined {
	const matching = eligibility.leverageConfigs.filter(
		(c) => c.direction === side && (c.leverageValues.length === 0 || c.leverageValues.includes(leverage))
	);
	return (
		matching.find((c) => c.settlementType === 'real' && !c.isPotential)?.settlementType ??
		matching.find((c) => c.settlementType === 'cfd' && !c.isPotential)?.settlementType
	);
}

Non-potential real wins when it matches this eToro ticket. Else CFD. Else I omit settlementType. Potential rows stay out. They are hints, not live eligibility.

Buy and sell are not interchangeable. A long real row does not authorize a short CFD at the same leverage. If leverageValues is empty, that config accepts any leverage on that side. If it lists numbers, my ticket has to be one of them.

minPositionAmount has the same trap. An eToro real row at 1x can carry a different minimum than the CFD row at 5x. Pull the minimum from a random real config and pair it with a levered CFD settlementType, and I either undersize the ticket or invent a product that does not exist. Same matching config. Same side. Same leverage. CFD vs real and the minimum come out together.

Ship the documented eToro v2 open. Leave the working v1 close. Never retry a timeout on open. Ask eligibility what this ticket actually is before you stamp settlementType.


Happy coding!
Sander