Program Notes
Program Notes

Anatomy of a Curve Buy Instruction on a Launchpad

A curve buy looks like one action and arrives as several. There is a compute budget preamble, sometimes a token account creation, and then the trade itself: eight bytes naming the call, a short argument block, and an ordered account list the program validates before it moves anything. This note reads that structure the way a decoder does.

IX-02 The Program Notes Desk 2200 words 10 min read Updated 13 August 2026

Summary of this note

Question
What is actually inside a bonding curve purchase, in the order a decoder reads it
Inputs
A confirmed transaction: instruction data, account keys, inner instructions, logs, pre and post balances
Rule
Data names the call and sets the bounds; accounts decide whether the call is allowed at all
Failure mode
Reading the logs and ignoring the balance deltas, which is how a partial fill becomes a confident wrong number

A bonding curve buy is a single program instruction carrying an eight-byte discriminator that names the call, a short little-endian argument block that sets the size and the acceptable price bound, and an ordered list of accounts the program checks before it moves anything. It almost never travels alone: a compute budget preamble and, on a first purchase, an associated token account creation usually share the same atomic transaction.

Reading that structure is the single most useful skill for anyone trying to understand launchpad activity, because it converts arguments into observations. You stop asking whether a wallet "sniped" a token and start reading what it asked the program to do, how much it was willing to pay in compute priority, and what the ledger says it received.

The shape of a transaction that buys

Open a confirmed launchpad purchase in any explorer and the outer instruction list is usually three to four entries long. The first two set compute limits. The third, when present, creates the buyer's token account for the mint. The last is the curve trade. They are atomic: if the token account creation fails, the trade is rolled back with it, and the fee is still charged.

That atomicity is not a convenience feature, it is the Solana execution model. A transaction either applies in full or not at all, which is why a launchpad bot can safely bundle account creation with the trade rather than sequencing them as two separate submissions. It is also why a single failed constraint in the trade instruction wipes out the account creation that preceded it in the same transaction.

outer 1 ComputeBudget SetComputeUnitLimit outer 2 ComputeBudget SetComputeUnitPrice outer 3 AssociatedToken Create only if the buyer has no token account yet outer 4 Launchpad buy the instruction this note is about inner System Transfer lamports to the curve side inner Token Transfer tokens to the buyer inner Launchpad self-invocation event written to the log

The inner instructions matter more than they look. Token movement on Solana is performed by the token program, not by the launchpad program directly, so the actual transfers appear as cross-program invocations nested under the trade. A reader who lists only outer instructions will see a buy and no token movement, and will conclude something strange happened.

The eight bytes that name the call

Programs built with the Anchor framework prefix every instruction's data with eight bytes derived from the instruction's name: the first eight bytes of a SHA-256 hash of the string global: followed by the snake-case name. Account data gets the same treatment with a different prefix string. The effect is that a decoder can identify both the call and the struct it is looking at without any schema, provided it knows the candidate names.

For a launchpad this is why explorers can label an instruction buy or sell without special support: they hold a table of known discriminators and match on the first eight bytes. It is also why an unknown instruction shows as raw base58 data. The bytes have not changed meaning; nobody has told the explorer what name produces them.

The practical reading habit is to treat the discriminator as the only reliable label. A transaction can be presented in any interface with any description attached to it. The eight bytes are what the program itself dispatched on, and they either match a known instruction or they do not.

The arguments a buy carries

After the discriminator comes the argument block, and on a curve trade it is short. A purchase typically needs to express two things: how much, and what price movement is tolerable. Both are unsigned 64-bit integers in base units, written little-endian, because that is the standard Borsh encoding used across the ecosystem.

OffsetWidthFieldWhat it means when you decode it
08 bytesdiscriminatorNames the instruction. Match it against known names rather than guessing from context.
88 bytesamountBase units, not display units. Divide by ten to the power of the mint's decimals to read it as a token figure.
168 bytesboundThe other side of the trade the sender will accept: a maximum lamport spend on a buy, a minimum receipt on a sell.

The base-unit detail catches people constantly. Launchpad mints commonly use six decimals, so an amount field reading 35,000,000,000 is thirty-five thousand tokens rather than thirty-five billion. SOL amounts are in lamports, one billion to the SOL. A decoder that ignores decimals produces figures that are wrong by six or nine orders of magnitude and look plausible enough to publish.

Some deployed versions carry extra arguments beyond those two, and the ordering of amount and bound has differed between programs. This is exactly the kind of detail that should be re-derived from a recent confirmed transaction rather than trusted from a table. The stable part is the pattern: a size and a bound, both integers, both in base units.

The compute budget preamble

Every Solana transaction gets a compute budget, and the two instructions that adjust it are among the most informative things in a launchpad trace. SetComputeUnitLimit declares how much execution the sender expects to need; SetComputeUnitPrice attaches a price in micro-lamports per compute unit, which is what actually funds priority.

Those two numbers are a window into intent. A sender who sets a high unit price is paying for inclusion urgency, which is a reasonable thing to do around a launch and an unreasonable thing to do on a routine trade. A sender who sets a limit far above what the instruction consumes is either being cautious or using a default. Comparing declared limit against consumed units in the confirmed transaction is a two-second check that tells you whether the sender's tooling is tuned or generic.

The consumed compute units appear in the confirmed transaction's log output, usually on a line naming the program and the units it used. Comparing that figure to the declared limit across many transactions from one wallet is one of the more reliable ways to recognise a single piece of software behind different addresses, because defaults travel with tools rather than with people.

The token account that may not exist

On Solana a wallet does not hold a token directly. It holds a token account owned by the token program, associated with the wallet and the mint. The associated token account program derives that address deterministically from the wallet and the mint, which means anyone can compute where a buyer's tokens will live before the account exists. The derivation rules and the account layout are described in the Solana program library documentation.

For a brand-new mint the buyer's associated account has never been created, so the transaction has to create it, which costs a rent-exempt deposit in lamports funded by the payer. This is a small, fixed, protocol-level cost, and it is one of the reasons a wallet's first purchase of a token is slightly more expensive than its second.

It also has a side effect worth knowing when reading other people's activity. The creation instruction names a funder, and the funder does not have to be the wallet that ends up owning the account. When a single address funds token accounts for a dozen different wallets in the same slot, that is a structural fact about how the wallets are operated, visible without any inference about who owns them.

What the program does once it accepts

Assuming the account list validates, the program executes the trade in a predictable order. It reads its own state account for the mint, applies its pricing formula to determine what the sender receives, checks the result against the bound in the arguments, moves lamports into the curve side, invokes the token program to move tokens to the buyer, applies whatever fee split the current deployment defines, updates the reserve figures in state, and writes an event.

The order the program works in

  1. Load the curve state account and confirm the mint matches the accounts supplied.
  2. Reject immediately if the curve is already marked complete, because completed curves no longer accept trades.
  3. Compute the output from the current reserve figures using the program's pricing formula.
  4. Compare against the sender's bound and abort the whole transaction if the bound is violated.
  5. Move lamports in, invoke the token program to move tokens out, and route the fee portion to whichever accounts the deployment names.
  6. Write the new reserve figures back to state and emit an event describing the trade.

Step two is the one people forget. Once a curve has completed, the same instruction that worked five minutes earlier fails, and it fails for a reason that has nothing to do with the sender's configuration. That transition is the subject of the note on what a migration writes to the chain.

The balance deltas left behind

The most under-used part of a confirmed transaction is the pre and post balance arrays. Every account in the message has a lamport balance before and after, and every token account has a token balance before and after. These are not descriptions of intent. They are the ledger's own statement of what changed, and they cannot be styled, summarised or spun.

A worked balance read

A worked read looks like this. Suppose a buyer sends a trade with a maximum spend of 1.02 SOL and the confirmed transaction shows the buyer's lamport balance falling by 1.0043 SOL, of which 0.000005 is the base fee and roughly 0.002 is the rent deposit for the new token account. The remainder is what went into the trade. Meanwhile the buyer's token account shows a post balance where the pre balance did not exist. Those two lines together tell you the effective price paid, and they are the only numbers in the transaction that were not chosen by the sender.

The lamport figures in that example are illustrative, not measured. Base fees and rent minimums are protocol parameters that can be changed by governance, and fee structures on launchpad programs have been adjusted across deployments. The method is the point: subtract the fee and the rent, and what remains is the trade.

Reading a slippage failure

A failed launchpad transaction is more informative than a successful one, because the error names the constraint. Anchor programs return custom error codes, and explorers surface both the number and, when they can resolve it, the name. A bound violation says the curve moved between the moment the sender computed its bound and the moment the instruction executed.

The useful diagnosis is comparative. If a wallet's failures cluster on tokens with heavy simultaneous buying, the bound was simply too tight for the conditions and the tooling worked correctly. If failures appear on quiet mints with no competing flow, the sender is computing bounds from stale state, which is a decoder problem rather than a market problem. Same error code, completely different remedy.

This is also where a public explorer earns its place in a workflow. Opening a handful of failed signatures on the Solana explorer and reading the error names is faster than any amount of speculation about why fills are not landing, and it costs nothing.

How a sell differs from a buy

Structurally, very little. The discriminator names a different instruction, the argument block expresses an amount of tokens to sell and a minimum lamport receipt rather than a maximum spend, and the token transfer runs in the opposite direction. The account list is nearly identical, because the same accounts are involved regardless of direction.

AspectBuySell
Amount argumentTokens requested, in base unitsTokens offered, in base units
Bound argumentMaximum lamports the sender will spendMinimum lamports the sender will accept
Token accountMay need creating in the same transactionAlready exists, or the sender has nothing to sell
Typical failureBound exceeded by competing buysBound missed after the price fell
Effect on reservesLamport side rises, token side fallsToken side rises, lamport side falls

The symmetry is why a repeated buy-then-sell pattern from the same wallet on the same mint is so easy to spot in a trace: two nearly identical instruction shapes alternating within a small number of slots, with the token balance returning to roughly where it started. That pattern is the operating signature of a volume bot for Pump.fun launches, and it looks nothing like an entry bot's single directional purchase followed by silence.

Why none of this is permanent

Launchpad programs are upgradeable. An upgrade authority can deploy new bytecode to the same program id, and everything described here that is specific to a launchpad rather than to Solana itself can move with it: instruction names and therefore discriminators, argument order and width, fee destinations, the set of accounts required, and the constants inside the pricing formula.

What does not move is the framework. Instruction data will still begin with a discriminator if the program is Anchor-built. Arguments will still be little-endian integers in base units. Token transfers will still happen through the token program as inner instructions. Balances will still be recorded before and after. A reader who has internalised the framework can re-derive the specifics from a recent transaction in a few minutes, which is the durable skill.

The next note goes through the account list slot by slot, because the argument block is the easy half of an instruction and the account list is where transactions actually fail.

Filed under Instructions. Account lists, discriminators and program constants described here belong to the program version that was live when the note was written, and a launchpad deployment can change any of them without warning. If a signature you are looking at disagrees with this page, the signature is right: send it to the desk and the page gets corrected in the open.

Read next