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. Vercel Workflows: skip closed eToro legs

Vercel Workflows: skip closed eToro legs

When a market is closed, mark the trade leg as skipped, not failed. Retrying a partially filled rebalance can buy the same position twice and leave cash stuck.

Sander KorfPublished August 12, 20265 min read
verceletoroworkflows

This is a Vercel Workflows step that opens eToro legs

One step. Several tickets. eToro fills what it can. Vercel Workflows retries what you mark retryable. If a closed commodity is a RetryableError, the step starts over and the filled legs buy twice.

What they would notice if it failed

Duplicate eToro positions on names that already filled. A parent FatalError after four retries. Residual cash that tries GOLD again instead of walking past it. The account sees extra tickets. The workflow log looks busy and proud.

The retry that bought the same names twice

I shipped a production trading agent that opens a basket of tickets through eToro. One Vercel Workflows step. Four legs. The first two filled. The third was a commodity closed for the session. Local validation said, correctly, not currently tradable.

I wrapped that as Vercel Workflows RetryableError. Vercel Workflows did what it does: it retried the whole step. Four times. Legs one and two opened again. The remaining names never ran. The parent then threw Vercel Workflows FatalError.

That is not a flaky socket. That is a class of error that cannot succeed on retry in the same session: not tradable, side disabled, leverage out of range. Treating those as retryable is how you invent duplicate eToro fills.

Checkpoints are steps, not legs

Vercel Workflows checkpoints steps, not the legs inside a step. If you treat a closed eToro market as retryable, the runtime does not resume at the failed name. It re-enters the step from the top. Every earlier openPosition runs again. eToro has no memory of your mental model. It just opens.

Prefetch the constraints and skip the untradable leg. If the eToro client still throws those messages, treat them as skip, not retry. Put the matchers in one shared helper so the throw text and the skip patterns cannot drift. A wording change in the eToro client should not silently turn a skip back into a four-retry replay.

HTTP 4xx stays fatal. Other errors stay retryable. A timeout can succeed on the next attempt. A session-closed commodity cannot.

This does not unwind duplicates already live. Those fills are on the eToro book. Skipping the next time you see the same validation is the fix going forward.

Skip the leg, keep the Vercel Workflows step

type OpenAttempt = { status: 'placed'; orderId?: string } | { status: 'skipped' };
 
async function attemptOpen(input: OpenInput): Promise<OpenAttempt> {
	try {
		const receipt = await etoro.openPosition(input);
		return { status: 'placed', orderId: receipt?.orderId };
	} catch (error) {
		if (isSkippableOpenValidationError(error)) {
			return { status: 'skipped' }; // not Vercel Workflows RetryableError
		}
		throw error;
	}
}

isSkippableOpenValidationError is the only place that knows which eToro messages mean this name will not trade this session. attemptOpen returns { status: 'skipped' } instead of throwing Vercel Workflows RetryableError. The step finishes. Remaining legs run.

Four-leg open step

Local fake state only. Retry wraps a closed market as RetryableError. Skip treats it as a skip.
  1. Leg 1: EQ-A

    Equity A

    pending
  2. Leg 2: EQ-B

    Equity B

    pending
  3. Leg 3: CMD-X

    Commodity X (session closed)

    pending
  4. Leg 4: EQ-C

    Equity C

    pending

Idle. Open the basket, then compare Retry against Skip-untradable.

Hit Retry and watch filled legs clone themselves while the rest never start. Hit Skip-untradable and the remaining names complete.

Residual eToro cash sweep is the same rule

Primary eToro fills land. Cash remains. I sweep that residual into the highest-weight target that can still take an eToro open. Weight picks the winner. Tradability decides who is even in the race. I used to let weight win alone.

The old helper sorted remaining targets by weight and grabbed index zero. A closed eToro commodity with the fattest remainder still won. Think GOLD when isTradable is false or canOpenPositionNow is false. The sweep then tried to open it.

I wrapped that miss as Vercel Workflows RetryableError. Now a closed session could abort the leftover or no-op the whole step. Twice. First on the primary path. Again on the sweep. One untradable eToro name became a second way to stall leftover cash.

A closed eToro market is not leftover capacity. It is a hard no. The new picker still sorts by weight descending. It filters first.

  • Missing instrument ID: skip
  • Missing constraints: skip
  • canOpenPositionNow is false: skip
  • skipReasonForOpenConstraints is not null: skip
function pickResidualSweepTarget(targets, symbolToInstrument, constraintById, eligibilityById) {
	const sorted = Object.entries(targets)
		.filter(([symbol, target]) => {
			const id = symbolToInstrument[symbol];
			if (id === undefined) return false;
			const constraints = constraintById[id];
			if (!constraints) return false;
			if (!canOpenPositionNow(eligibilityById[id])) return false;
			return skipReasonForOpenConstraints(constraints, target.side, target.leverage) === null;
		})
		.sort((a, b) => b[1].weight - a[1].weight);
	return sorted[0];
}

GOLD sits at the top of the remainder and is untradable. The picker walks past it. The next eligible eToro target still opens. If a later throw happens, that throw does not retry the Vercel Workflows step. Residual sweep is not a retry loop. It is a one-shot allocation onto a name that can actually take the eToro order.

Two tests keep this honest. An untradable GOLD-like name at the top of leftover weight does not eat the cash. A later throw does not retry the sweep step. Closed eToro markets stay closed. Leftover cash still lands. Same skip helper. Same Vercel Workflows step. No RetryableError for a session that will not open.


Happy coding!
Sander