Build a swap transaction
Buys and sells use the same swap instruction and account graph. Only the direction, quote amounts, and trader token-account preparation differ.
This page builds a reusable tribe-swap.ts module. It expects the TribeMarket returned by Read market state onchain and a quote returned by the TypeScript swap flows guide.
The module:
- Validates the referral recipient and prevents self-referral.
- Uses the trader's Token-2022 associated token account.
- Creates that account idempotently for a buy, or verifies its mint, owner, and balance for a sell.
- Sets
referrer_tier_accounttoprotocol_vaultfor the default referral tier. - Adds a compute-unit limit and a current nonzero priority fee.
- Simulates before the wallet is asked to sign.
Wallet boundary
Your wallet connector must provide Anchor with publicKey, signTransaction, and signAllTransactions. Wallet Standard, wallet-adapter, mobile-wallet, injected-wallet, bot-keypair, and custody integrations can adapt to that interface.
The referral address is a public payout address and never signs the swap. Only the trader's connected wallet signs.
Configure TRIBE_REFERRER with the Solana address that should receive your integration's fees. Fund that wallet to the rent-exempt minimum before the first attributed swap.
Build an exact-input swap
import { AnchorProvider, BN, Program, type Idl } from "@anchor-lang/core";
import {
ComputeBudgetProgram,
Connection,
PublicKey,
SystemProgram,
Transaction,
type TransactionInstruction,
} from "@solana/web3.js";
import {
TOKEN_2022_PROGRAM_ID,
createAssociatedTokenAccountIdempotentInstruction,
getAccount,
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import swapIdl from "./tribe-swap-idl.json";
import { TRIBE_PROGRAM_ID, type TribeMarket } from "./tribe-market";
export type ExactInSwapQuote = {
amountIn: bigint;
amountOut: bigint;
limit: bigint;
creatorFee: bigint;
grossProtocolFee: bigint;
lpFee: bigint;
totalFee: bigint;
newSqrtPriceX64: bigint;
};
type Wallet = ConstructorParameters<typeof AnchorProvider>[1];
async function validateReferralRecipient(connection: Connection, referral: PublicKey): Promise<void> {
const info = await connection.getAccountInfo(referral, "confirmed");
const rentMinimum = await connection.getMinimumBalanceForRentExemption(0, "confirmed");
if (!info || !info.owner.equals(SystemProgram.programId) || info.data.length !== 0) {
throw new Error("Referral must be an existing zero-data System Program wallet");
}
if (info.lamports < rentMinimum) {
throw new Error(`Referral wallet must retain at least ${rentMinimum} lamports`);
}
}
async function currentPriorityFee(connection: Connection, writableAccounts: PublicKey[]): Promise<number> {
const samples = await connection.getRecentPrioritizationFees({ lockedWritableAccounts: writableAccounts });
const values = samples.map((sample) => sample.prioritizationFee).sort((a, b) => a - b);
if (values.length === 0) throw new Error("RPC returned no current priority-fee samples");
return Math.max(1, values[Math.ceil((values.length - 1) * 0.75)] ?? 1);
}
export async function buildExactInSwap(input: {
connection: Connection;
wallet: Wallet;
market: TribeMarket;
trader: PublicKey;
referral: PublicKey;
quote: ExactInSwapQuote;
isBuy: boolean;
}): Promise<{ transaction: Transaction; blockhash: string; lastValidBlockHeight: number }> {
const { market } = input;
const hasReferral = !input.referral.equals(market.accounts.protocolVault);
if (hasReferral) {
if (input.referral.equals(input.trader)) throw new Error("The referral wallet must differ from the trader");
await validateReferralRecipient(input.connection, input.referral);
}
const traderTokenAccount = getAssociatedTokenAddressSync(market.mint, input.trader, false, TOKEN_2022_PROGRAM_ID);
const setupInstructions: TransactionInstruction[] = [];
if (input.isBuy) {
setupInstructions.push(
createAssociatedTokenAccountIdempotentInstruction(
input.trader,
traderTokenAccount,
input.trader,
market.mint,
TOKEN_2022_PROGRAM_ID,
),
);
} else {
const traderTokens = await getAccount(input.connection, traderTokenAccount, "confirmed", TOKEN_2022_PROGRAM_ID);
if (!traderTokens.mint.equals(market.mint) || !traderTokens.owner.equals(input.trader)) {
throw new Error("Trader Token-2022 account has the wrong mint or owner");
}
if (traderTokens.amount < input.quote.amountIn) throw new Error("Insufficient token balance for sell");
}
const provider = new AnchorProvider(input.connection, input.wallet, { commitment: "confirmed" });
const program = new Program(swapIdl as Idl, provider);
const swapMethod = program.methods["swap"];
if (!swapMethod) throw new Error("Swap IDL did not expose the swap method");
const swapInstruction = await swapMethod({
amount: new BN(input.quote.amountIn.toString()),
limit: new BN(input.quote.limit.toString()),
isBuy: input.isBuy,
swapMode: 0,
})
.accountsPartial({
trader: input.trader,
protocol: market.accounts.protocol,
token: market.accounts.token,
mint: market.mint,
solVault: market.accounts.solVault,
tokenVault: market.accounts.tokenVault,
poolAuthority: market.accounts.poolAuthority,
traderTokenAccount,
creator: market.creator,
protocolVault: market.accounts.protocolVault,
feeTierConfig: market.accounts.feeTierConfig,
token2022Program: TOKEN_2022_PROGRAM_ID,
systemProgram: SystemProgram.programId,
eventAuthority: market.accounts.eventAuthority,
program: TRIBE_PROGRAM_ID,
referral: input.referral,
referrerTierAccount: market.accounts.protocolVault,
})
.instruction();
const priorityFee = await currentPriorityFee(input.connection, [
market.accounts.token,
market.accounts.solVault,
market.accounts.tokenVault,
]);
const transaction = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee }),
...setupInstructions,
swapInstruction,
);
const latest = await input.connection.getLatestBlockhash("confirmed");
transaction.feePayer = input.trader;
transaction.recentBlockhash = latest.blockhash;
return { transaction, blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight };
}
For a real referral, pass your funded payout wallet as referral. To build without attribution, pass market.accounts.protocolVault; the builder then uses the sentinel for both referral account slots.
Every integration currently uses the default 15% referral tier. The instruction requires referrer_tier_account, so the builder always passes protocol_vault. No separate tier account is needed.
Validate without signing
An AI agent can verify transaction construction without controlling a private key:
- Use an existing funded account as the read-only
traderand fee payer. A buy needs enough SOL; a sell also needs the market's Token-2022 associated account and sufficient tokens. - Give Anchor a read-only wallet object with that
publicKeyand signing methods that always throw. - Build the transaction normally, then call
connection.simulateTransaction(transaction). Do not enable signature verification. - Require
simulation.value.errto benull, record logs and compute units, and stop. Do not call a signing method or submit the transaction.
This proves that discovery, quote math, account metas, priority fees, balance checks, and the swap instruction are valid against current mainnet state. It does not authorize a trade.
Simulate, sign, and submit
export async function submitSwap(input: {
connection: Connection;
wallet: Wallet;
transaction: Transaction;
blockhash: string;
lastValidBlockHeight: number;
}): Promise<string> {
const simulation = await input.connection.simulateTransaction(input.transaction);
if (simulation.value.err) {
throw new Error(`Swap simulation failed: ${JSON.stringify(simulation.value.err)}`);
}
const signed = await input.wallet.signTransaction(input.transaction);
const signature = await input.connection.sendRawTransaction(signed.serialize(), {
maxRetries: 3,
skipPreflight: false,
});
const confirmation = await input.connection.confirmTransaction(
{
signature,
blockhash: input.blockhash,
lastValidBlockHeight: input.lastValidBlockHeight,
},
"confirmed",
);
if (confirmation.value.err) {
throw new Error(`Swap confirmation failed: ${JSON.stringify(confirmation.value.err)}`);
}
return signature;
}
If simulation reports stale price, fee, gate, vault, or balance state, reload the onchain market and rebuild. Never silently lower the user's slippage protection.
Verify the referral
The referral wallet is writable but does not sign. Its reward is transferred directly in SOL and requires no claim.
- Record its SOL balance before submission.
- Confirm the transaction.
- Read its balance again, or decode
SwapCompleted. - At the default tier, the increase is
floor(gross_protocol_fee × 150_000 / 1_000_000).
Referral rewards do not increase the trader's fee; they split the existing protocol portion.