Skip to main content

Placing Bids Programmatically

This guide shows the complete end-to-end flow for placing a bid on an active CCA auction using the @runner-protocol/sdk.

Prerequisites

npm install @runner-protocol/sdk @solana/web3.js

Step 1: Fetch Auction State

Query the indexer to get the current auction state. You need total_bids, latest_checkpoint_auction_block, and quote_mint to build the transaction.

import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import {
buildPlaceBidInstruction,
encodeQ96,
getAssociatedTokenAddress,
createAssociatedTokenAccountIdempotentInstruction,
TOKEN_PROGRAM_ID,
} from "@runner-protocol/sdk";

const INDEXER_URL = "https://cca-indexer-production.up.railway.app";
const connection = new Connection("https://api.devnet.solana.com");

// Fetch the auction detail
const auctionPubkey = "YOUR_AUCTION_CONFIG_PUBKEY";
const response = await fetch(`${INDEXER_URL}/api/auctions/${auctionPubkey}`);
const auction = await response.json();

// Key fields:
// auction.clearing_price - Current Q96 clearing price
// auction.total_bids - Current bid count (string)
// auction.latest_checkpoint_auction_block - Latest checkpoint block (string)
// auction.quote_mint - Quote currency mint
// auction.base_mint - Token being auctioned

Step 2: Determine prevTickPrice

The prevTickPrice is a linked-list insertion hint. Fetch the tick list and find the correct position for your bid price.

// Fetch active ticks, sorted by price descending
const ticksResponse = await fetch(
`${INDEXER_URL}/api/auctions/${auctionPubkey}/ticks`
);
const { data: ticks } = await ticksResponse.json();

// Your bid price (human-readable)
const maxPriceHuman = 0.50;
const maxPriceQ96 = encodeQ96(maxPriceHuman);

// Find prevTickPrice: the highest existing tick price below your maxPrice
// If no tick below your price exists, use 0n (insert at floor)
let prevTickPrice = 0n;
for (const tick of ticks) {
const tickPrice = BigInt(tick.price);
if (tickPrice < maxPriceQ96) {
prevTickPrice = tickPrice;
break; // ticks are sorted DESC
}
}

Step 3: Build the Transaction

const bidder = new PublicKey("YOUR_WALLET_PUBKEY");
const auctionConfig = new PublicKey(auctionPubkey);
const quoteMint = new PublicKey(auction.quote_mint);

// Derive bidder's quote token ATA
const bidderQuoteAccount = getAssociatedTokenAddress(
quoteMint,
bidder,
TOKEN_PROGRAM_ID
);

// Ensure ATA exists (idempotent -- no-ops if already created)
const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
bidder, // payer
bidderQuoteAccount,
bidder, // owner
quoteMint,
TOKEN_PROGRAM_ID
);

// Build the place_bid instruction
const placeBidIx = buildPlaceBidInstruction({
bidder,
auctionConfig,
maxPrice: maxPriceQ96,
amount: 1_000_000n, // 1 USDC (6 decimals) in atomic units
prevTickPrice,
totalBids: BigInt(auction.total_bids),
latestCheckpointAuctionBlock: BigInt(
auction.latest_checkpoint_auction_block
),
bidderQuoteAccount,
tokenProgram: TOKEN_PROGRAM_ID,
});

Step 4: Sign and Send

const transaction = new Transaction();
transaction.add(createAtaIx);
transaction.add(placeBidIx);

const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = bidder;

// Sign with your wallet (varies by context)
// Keypair: transaction.sign(keypair);
// Wallet adapter: await sendTransaction(transaction, connection);

const signature = await connection.sendRawTransaction(
transaction.serialize()
);

// Confirm with the same blockhash used above
const confirmation = await connection.confirmTransaction({
signature,
blockhash,
lastValidBlockHeight,
});

if (confirmation.value.err) {
throw new Error(
`Transaction failed: ${JSON.stringify(confirmation.value.err)}`
);
}

console.log("Bid placed:", signature);
caution

Always fetch a fresh blockhash immediately before sending. If you build the transaction and then wait, the blockhash may expire and the transaction will be rejected.

Accounts Layout

The buildPlaceBidInstruction function automatically derives these accounts:

#AccountDerived FromWritableSigner
0bidderProvidedYesYes
1auctionConfigProvidedNoNo
2auctionStatefindAuctionStatePda(auctionConfig)YesNo
3bidfindBidPda(auctionConfig, totalBids)YesNo
4latestCheckpointfindCheckpointPda(auctionConfig, latestCheckpointAuctionBlock)YesNo
5bidderQuoteAccountProvidedYesNo
6quoteVaultfindQuoteVaultPda(auctionConfig)YesNo
7tokenProgramTOKEN_PROGRAM_IDNoNo
8systemProgramSystem ProgramNoNo
9+tickAtMaxPricefindTickPda(auctionConfig, maxPrice)YesNo
10+prevTick (if > 0)findTickPda(auctionConfig, prevTickPrice)YesNo

Common Errors

ErrorCauseFix
BidAmountBelowMin (6030)Amount less than min_bid_amountIncrease bid amount
MaxPriceExceedsLimit (6032)Price above max_bid_priceLower your max price
MaxPriceBelowClearing (6033)Price below current clearing priceBid above clearing
AuctionNotStarted (6016)Before start_slotWait for auction to start
AuctionEnded (6017)After end_slotAuction is over
MaxTicksReached (6035)Too many tick price levelsUse an existing tick price
tip

The totalBids value must match the current on-chain value exactly. Each bid increments this counter, so fetch it from the indexer immediately before building the transaction.