trade execution: where we are, what we did, what's next
the most important thing a trading agent does is execute trades. everything else — the scoring, the filters, the risk management, the dashboard — is decoration if the buy doesn't land or the sell sits in a confirmation queue while the price collapses.
trade execution is also the part of any solana product that never finishes. solana is a moving target. the network upgrades, the validator stake redistributes, the dexes change their apis, the mempools shift, the priority-fee dynamics evolve. a trade-execution layer that was state-of-the-art in january is mid by april. anyone who tells you their execution is "done" is selling.
so this post is honest about that. trade execution at parasol is a permanent work-in-progress. the last two weeks were the most productive window we've ever had on it, and we have a clear roadmap for the next two — including an on-chain program already written and waiting to deploy. here's what we did, and what's coming.
---
the duplicate-buy story
a user clicked "buy" once. their wallet got hit twice. two transactions, same token, 46 seconds apart. they paid two sets of fees, two sets of slippage, ended up with a position twice as large as they wanted, and were — reasonably — extremely unhappy.
the bug was specific and the root cause is general enough to be useful to anyone building on solana, so it's worth explaining.
our trade execution runs as a cascade. when you click buy, we try the fastest, cheapest path first. if that path fails, we fall back to a slower, more reliable one. if that fails, we fall back further. each step has its own quote, its own simulation, its own broadcast.
what happened: the first path's transaction was broadcast successfully. it landed on-chain. but the api we used to broadcast it timed out at 8 seconds before it could tell us "yes, that landed." it returned an error. our cascade trusted the error, treated the first path as failed, and submitted a second swap via the next path. the second path also landed. now there were two transactions on-chain for the same buy.
the general lesson: a network response that says "this failed" is not the same as a network response that says "this transaction did not land." the absence of confirmation is not the presence of failure.
the fix is straightforward in description and was meaningful work in implementation. we now snapshot the user's on-chain token balance once at the start of every cascade, and after any step that returns a failure we re-read the balance. if it grew, the prior step actually landed. we short-circuit immediately and treat the trade as successful, recovering the transaction signature where we can.
this protects against an entire class of similar bugs across every venue we route through. http timeout, rpc deduplication of resubmitted blobs, an api returning early — they all collapse to the same defence: the on-chain state is the only ground truth.
---
the cascade itself
while we were rebuilding the verification layer, we slimmed the cascade itself.
the sell cascade used to have eight paths. eight ways the same trade could be retried before we gave up. that was overkill. several of the paths were duplicates with slightly different parameters — added historically when one of them was unreliable, kept around after that reliability gap closed, never pruned.
eight paths sounds like resilience. in practice it meant the worst-case sell time was around 90 seconds. solana's most-deployed serverless platform has a 60-second function timeout. a sell that entered a 25-second confirmation at second 50 would get killed at second 60, leaving the user staring at "selling..." with no idea whether the transaction had landed or not.
we cut the cascade to four paths. each one is genuinely distinct. each one has a budget. before any path runs, we check how much time is left in the cascade and skip the path if its worst-case wouldn't fit. the worst-case sell time is now around 35 seconds, comfortably under the function ceiling, with the budget skip ensuring we always exit cleanly with a meaningful message rather than a kill.
we did the same for the buy cascade. same principle: fewer paths, each with a budget, each with a deadline.
a sell that times out now gets a specific user-facing message — "sell timed out after 50s — a transaction may still be confirming. check wallet and retry if tokens are still present." — instead of a panic alert that blames the entire system for what is, most of the time, a slow confirmation that will resolve itself in another 15 seconds.
---
native fees
we used to collect our platform fee as a separate transaction after each swap. swap completes, fee transfer fires, both settle. simple in description. brittle in practice — the fee transfer could fail on a stale blockhash, a nonce collision, or a small wallet balance that didn't quite cover the additional transfer cost. when the fee transfer failed, the user saw a "failed app interaction" toast, even though their actual swap had landed perfectly. it confused everyone, including us.
we moved fee collection into the swap. major aggregator routes have native fee mechanisms designed exactly for this — a referral account or platform-fee parameter that includes the fee atomically as part of the swap transaction. one transaction, atomic settlement, no separate transfer to fail. about 340 lines of fee retry queue, blockhash caching, after-the-fact fee plumbing came out of the codebase. the replacement is around 60 lines.
the user-visible effect is "we stopped seeing failed-app-interaction toasts on successful trades." the engineer-visible effect is one fewer category of bug to ever think about again.
---
the cross-instance race
if you're running trade execution on a serverless platform, you're running multiple instances of your code at once, often without realising it. that creates a class of bug where two instances both pick up the same buy intent — say, from a user who clicked twice in three seconds — and both submit a transaction. each instance "knew" it was the only one. neither was wrong individually. together they double-spent.
we now hold a cross-instance mutex on every buy. the mutex sits in a shared cache that all instances read from. before any instance starts a buy for a given token, it has to acquire the lock. if another instance is already holding it, the second buy waits or fails closed. we hold the lock for an additional five seconds after a successful buy to bridge the gap between transaction settlement and rpc-indexed visibility, which is the window where this bug used to slip through.
it sounds simple. it took the better part of a day to get right because the failure modes are subtle. it's been holding ever since.
---
the speed work
a list of the smaller wins, each in one sentence:
each of these is a small fraction of the total trade time. cumulatively, they're material — manual buys feel obviously snappier on the live product.
---
what we're planning
three things that aren't shipped yet but are genuinely close.
our own on-chain swap program — already written
we wrote our own anchor program for direct swap execution. it handles the entire swap atomically — wsol wrap, the swap itself, fee collection, wsol close — in a single instruction. one transaction, one signature, one settlement.
it builds successfully and the integration paths are designed. it's parked, not abandoned. mainnet deployment costs around 3 sol and we wanted the hackathon-window engineering time to go into hardening the existing public-dex paths first, since most of our trade volume routes through them and improvements there benefit users today rather than after a deployment cycle.
when we deploy it, the hot path of trade execution stops depending on third-party aggregator availability. the aggregators stay in the cascade as fallbacks. the fast-path becomes ours, end to end, with atomic fee collection, no separate-transaction failure modes, and a meaningful latency improvement on the most common venues.
we'll write a separate post when it deploys. the program is in the public main repository under programs/parasol-swap if you want to read it.
entry latency as the next sprint
reliability and survivability were this sprint. entry speed is next. the gap between "agent decides to buy" and "transaction is broadcast" still includes an unnecessary batch-cycle wait for some token discovery paths. we're moving high-signal events — a fresh launch crossing a momentum threshold, a smart wallet buying — to instant signal processing, bypassing the batch entirely.
the impact this has on the average trade is hard to overstate. most memecoin runs happen in their first five minutes. a 60-second discovery delay turns a +5% entry into a +30% entry. the math compounds.
parallel risk gates
our pre-trade risk checks run sequentially today. honeypot, then holder concentration, then sector exposure. each one takes 1-3 seconds depending on upstream conditions. cumulative cost: 3-9 seconds of pure pre-flight delay.
we're moving to parallel risk checks with a shared timeout and a fallback policy: if 2 of 3 succeed within budget, we proceed with a slightly elevated rug score for the missing check rather than blocking the entry. failure-tolerant safety is the only kind of safety that actually fires when you need it most — during periods of network stress when upstream apis are slowest.
---
the standing principle
when we ship a piece of trade execution work, the bar is: does this make a real trade more likely to land in the right window with the right outcome, or does it not? if it does, it ships. if it doesn't, it doesn't. we deleted features this month — the in-product token buyback among them — that were generating failed transactions and confusing users. removal counts as shipping if the alternative is users seeing red toasts.
trade execution is never finished. it gets better, never done. the next post on it will be in two weeks when the entry-speed sprint ships.
if you want to follow what's landing as it lands: @parasolsolana is where we post the live updates. cycle-style recaps go on the blog.
— the parasol team