Verification Tools

Tokens land in your wallet the moment a transaction confirms. There is no action required by the recipient. Solana atomically increments the receiver's token account balance and decrements the sender's balance. In this guide, we cover some helpful tools for understanding your token account balance and watching for incoming payments.

Query Token Balance

Check your stablecoin balance using the getTokenAccountBalance RPC method:

import { createSolanaRpc, address, type Address } from "@solana/kit";
import {
findAssociatedTokenPda,
TOKEN_PROGRAM_ADDRESS
} from "@solana-program/token";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const USDC_MINT = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
async function getBalance(walletAddress: Address) {
// Derive the token account address
const [ata] = await findAssociatedTokenPda({
mint: USDC_MINT,
owner: walletAddress,
tokenProgram: TOKEN_PROGRAM_ADDRESS
});
// Query balance via RPC
const { value } = await rpc.getTokenAccountBalance(ata).send();
return {
raw: BigInt(value.amount), // "1000000" -> 1000000n
formatted: value.uiAmountString, // "1.00"
decimals: value.decimals // 6
};
}

Watch for Incoming Transfers

Subscribe to your token account for real-time payment notifications using the accountNotifications RPC method:

const rpcSubscriptions = createSolanaRpcSubscriptions(
"wss://api.mainnet-beta.solana.com"
);
async function watchPayments(
tokenAccountAddress: Address,
onPayment: (amount: bigint) => void
) {
const abortController = new AbortController();
const subscription = await rpcSubscriptions
.accountNotifications(tokenAccountAddress, {
commitment: "confirmed",
encoding: "base64"
})
.subscribe({ abortSignal: abortController.signal });
let previousBalance = 0n;
for await (const notification of subscription) {
const [data] = notification.value.data;
const dataBytes = getBase64Codec().encode(data);
const token = getTokenCodec().decode(dataBytes);
if (token.amount > previousBalance) {
const received = token.amount - previousBalance;
onPayment(received);
}
previousBalance = token.amount;
}
abortController.abort();
}

Note here that we are using RPC Subscriptions and a websocket connection to the Solana network.

Each notification contains a base64 encoded string of the token account data. Because we know the account we are looking at is a token account, we can decode the data using the getTokenCodec method from the @solana-program/token package.

Note that for production applications, you should consider a more robust streaming solution. Some options include:

Parsing Transaction History

Solana has RPC methods that allow you to fetch an account's transaction history (getSignaturesForAddress) and get the details of a transaction (getTransaction). To parse transaction history, we fetch recent signatures for our token account, then retrieve each transaction's pre/post token balances. By comparing our ATA's balance before and after each transaction, we can determine the payment amount and direction (incoming vs outgoing).

async function getRecentPayments(
tokenAccountAddress: Address,
limit = 100
): Promise<Payment[]> {
const signatures = await rpc
.getSignaturesForAddress(tokenAccountAddress, { limit })
.send();
const payments: ParsedPayment[] = [];
for (const sig of signatures) {
const tx = await rpc
.getTransaction(sig.signature, { maxSupportedTransactionVersion: 0 })
.send();
if (!tx?.meta?.preTokenBalances || !tx?.meta?.postTokenBalances) continue;
// Find our ATA's index in the transaction
const accountKeys = tx.transaction.message.accountKeys;
const ataIndex = accountKeys.findIndex((key) => key === ata);
if (ataIndex === -1) continue;
// Compare pre/post balances for our ATA
const pre = tx.meta.preTokenBalances.find(
(b) => b.accountIndex === ataIndex && b.mint === USDC_MINT
);
const post = tx.meta.postTokenBalances.find(
(b) => b.accountIndex === ataIndex && b.mint === USDC_MINT
);
const preAmount = BigInt(pre?.uiTokenAmount.amount ?? "0");
const postAmount = BigInt(post?.uiTokenAmount.amount ?? "0");
const diff = postAmount - preAmount;
if (diff !== 0n) {
payments.push({
signature: sig.signature,
timestamp: tx.blockTime,
amount: diff > 0n ? diff : -diff,
type: diff > 0n ? "incoming" : "outgoing"
});
}
}
return payments;
}

To identify the counterparty, you can scan the transaction's token balances for another account whose balance changed in the opposite direction—if you received funds, look for an account whose balance decreased by the same amount.

Because SPL token transfers can exist beyond just payments between users, this approach might yield some transactions that are not payments. A good alternative here is to use Memos.

Limitations of Pre/Post Balance Parsing

The approach above works well for simple payment flows. However, companies processing payments at scale often need more granular and real-time data:

  • Per-instruction breakdown: A single transaction can contain multiple transfers. Pre/post balances only show the net change, not individual transfers.
  • Multi-party transactions: Complex transactions (swaps, batch payments) involve multiple accounts. Balance diffs don't reveal the complete flow of funds.
  • Audit requirements: Financial compliance often requires reconstructing exact transfer sequences, not just final balances.

For production systems handling high volumes, consider dedicated indexing solutions that parse individual transfer instructions and provide transaction-level detail.

Reconcile Payments with Memos

When senders include memos (invoice IDs, order numbers), you can extract them from the transaction's message using the getTransaction RPC method and the jsonParsed encoding:

function extractMemos(transaction): string | null {
const { instructions } = transaction.transaction.message;
let memos = "";
for (const instruction of instructions) {
if (instruction.program !== "spl-memo") continue;
memos += instruction.parsed + "; ";
}
return memos;
}
async function getTransactionMemo(
signature: Signature
): Promise<string | null> {
const tx = await rpc
.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
encoding: "jsonParsed"
})
.send();
if (!tx) return null;
return extractMemos(tx);
}

Protections

A few failure modes to avoid:

  • Trusting the frontend. A checkout page says "payment complete"—but did the transaction actually land? Always verify server-side by querying the RPC. Frontend confirmations can be spoofed.

  • Acting on "processed" status. Solana transactions go through three stages: processed → confirmed → finalized. A "processed" transaction can still be dropped during forks. Wait for "confirmed" (1-2 seconds) before shipping orders, or "finalized" (~13 seconds) for high-value transactions.

  • Ignoring the mint. Anyone can create a token called "USDC". Validate that the token account's mint matches the real stablecoin mint address and token program, not just the token name.

  • Double-fulfillment. Your webhook fires, you ship the order. Network hiccup, webhook fires again. Now you've shipped twice. Store processed transaction signatures and check before fulfilling.

Is this page helpful?

सामग्री तालिका

पृष्ठ संपादित करें

द्वारा प्रबंधित

© 2026 सोलाना फाउंडेशन। सर्वाधिकार सुरक्षित।
जुड़े रहें