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);
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:
| # | Account | Derived From | Writable | Signer |
|---|---|---|---|---|
| 0 | bidder | Provided | Yes | Yes |
| 1 | auctionConfig | Provided | No | No |
| 2 | auctionState | findAuctionStatePda(auctionConfig) | Yes | No |
| 3 | bid | findBidPda(auctionConfig, totalBids) | Yes | No |
| 4 | latestCheckpoint | findCheckpointPda(auctionConfig, latestCheckpointAuctionBlock) | Yes | No |
| 5 | bidderQuoteAccount | Provided | Yes | No |
| 6 | quoteVault | findQuoteVaultPda(auctionConfig) | Yes | No |
| 7 | tokenProgram | TOKEN_PROGRAM_ID | No | No |
| 8 | systemProgram | System Program | No | No |
| 9+ | tickAtMaxPrice | findTickPda(auctionConfig, maxPrice) | Yes | No |
| 10+ | prevTick (if > 0) | findTickPda(auctionConfig, prevTickPrice) | Yes | No |
Common Errors
| Error | Cause | Fix |
|---|---|---|
BidAmountBelowMin (6030) | Amount less than min_bid_amount | Increase bid amount |
MaxPriceExceedsLimit (6032) | Price above max_bid_price | Lower your max price |
MaxPriceBelowClearing (6033) | Price below current clearing price | Bid above clearing |
AuctionNotStarted (6016) | Before start_slot | Wait for auction to start |
AuctionEnded (6017) | After end_slot | Auction is over |
MaxTicksReached (6035) | Too many tick price levels | Use an existing tick price |
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.