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.
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.
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 }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 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.
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