Sending funds to the wrong address can result in permanent loss. Address verification ensures you only send to addresses that can properly receive and access the funds.
Validation depends on what you send:
- SPL tokens are partly self-protecting. The Token Program rejects a transfer whose accounts don't match the expected mint, so a misdirected token transfer fails without losing funds. Most of this page covers SPL token sends.
- Native SOL has no such guard. A System Program transfer succeeds into any account, so an incorrect recipient permanently locks the SOL. See Sending native SOL.
See How Payments Work on Solana for core payment concepts.
Understanding Solana Addresses
Solana accounts have two types of addresses, on-curve and off-curve.
On-Curve Addresses
Standard addresses are the public keys from Ed25519 keypairs. These addresses:
- Have a corresponding private key that can sign transactions
- Are used as wallet addresses
Off-Curve Addresses (PDAs)
Program Derived Addresses are deterministically derived from a program ID and seeds. These addresses:
- Do not have a corresponding private key
- Can only be signed for by the program the address was derived from
Account Types in Payments
Use the address to fetch an account from the network, check its program owner and account type to determine how to handle the address.
Knowing whether an address is on-curve or off-curve does not tell you what type of account it is, what program owns it, or whether an account exists at that address. You must fetch the account from the network to determine these details.
System Program Accounts (Wallets)
Accounts owned by the System Program are standard wallets. To send SPL tokens to a wallet, you derive and use its Associated Token Account (ATA).
After deriving the ATA address, check whether the token account exists onchain. If the ATA does not exist, you can include an instruction to create the recipient's token account in the same transaction as the transfer. However, this requires paying rent for the new token account. Since the recipient owns the ATA, the SOL paid for the rent cannot be recovered by the sender.
Without safeguards, subsidizing ATA creation can be exploited. A malicious user could request a transfer, have their ATA created at your expense, close the ATA to reclaim the rent SOL, and repeat.
Token Accounts
Token accounts are owned by the Token Program or Token-2022 Program and hold token balances. If the address you receive is owned by a token program, you should verify the account is a token account (not a mint account) and matches the expected token mint account before sending.
The Token Programs automatically validate that both token accounts in a transfer hold tokens of the same mint. If validation fails, the transaction is rejected and no funds are lost.
Mint Accounts
Mint accounts track the token supply and metadata of a specific token. Mint accounts are also owned by Token Programs but are not valid recipients for token transfers. Attempting to send tokens to a mint address results in a failed transaction, but no funds are lost.
Other Accounts
Accounts owned by other programs require a policy decision. Some accounts (e.g. multisig wallets) may be valid token account owners, while others should be rejected.
Sending native SOL
The classification above determines where SPL tokens can go. Native SOL is stricter: the only safe recipient is a System Program wallet (or an unfunded on-curve address that becomes one).
A System Program transfer adds lamports to any account, including mints, token accounts, programs, and PDAs. Lamports can only be moved out by the account's owning program, so sending SOL to an incorrect recipient may result in funds being permanently lost.
Unlike an SPL token transfer, the transaction does not fail when the recipient is an unexpected address.
When sending native SOL, only an IS_WALLET result is acceptable.
IS_TOKEN_ACCOUNT is not: a token account holds SPL tokens, and SOL sent
there is outside the sender's control.
This is a common way SOL is lost: a user pastes a token's mint address (or a program address) into a SOL withdrawal. The transfer succeeds and the SOL is unrecoverable. Always classify the recipient before signing a SOL transfer.
Verification Flow
The following diagram shows a reference decision tree for validating an address:
Fetch account
Use the address to fetch the account details from the network.
Account does not exist
If no account exists at this address, check whether the address is on-curve or off-curve:
-
Off-curve (PDA): Conservatively reject the address to avoid sending to an ATA that may be inaccessible. Without an existing account, you cannot determine from the address alone which program derived this PDA or whether the address is for an ATA. Deriving an ATA for this address to send tokens could result in funds being locked in an inaccessible token account.
-
On-curve: This is a valid wallet address (public key) that hasn't been funded yet. Derive the ATA, check if it exists, and send tokens to it. You must make a policy decision whether to fund the creation of the ATA if it doesn't exist.
Account exists
If an account exists, check which program owns it:
-
System Program: This is a standard wallet. Derive the ATA, check if it exists, and send tokens to it. You must make a policy decision whether to fund the creation of the ATA if it doesn't exist.
-
Token Program / Token-2022: Verify the account is a token account (not a mint account) and that it holds the token (mint) you intend to send. If valid, send tokens directly to this address. If it's a mint account or a token account for a different mint, reject the address.
-
Other Program: This requires a policy decision. Some programs like multisig wallets may be acceptable owners of token accounts. If your policy allows it, derive the ATA and send. Otherwise, reject the address.
Demo
The following example shows only the address validation logic. This is reference code for illustration purposes.
The demo does not show how to derive an ATA or build a transaction to send tokens. Refer to the token account and token transfer documentation for example code.
The demo below uses three possible outcomes:
| Result | Meaning | Action |
|---|---|---|
IS_WALLET | Valid wallet address | Derive and send to associated token account |
IS_TOKEN_ACCOUNT | Valid token account | Send tokens directly to this address |
REJECT | Invalid address | Do not send |
It then maps each result to per-asset acceptability with canReceiveNativeSol
(wallets only) and canReceiveSplToken (wallets or token accounts). A token
account returns IS_TOKEN_ACCOUNT, so it can receive SPL tokens but not
native SOL — the distinction that prevents SOL from being locked.
/*** Validates an input address and classifies it as a wallet, token account, or invalid.** @param inputAddress - The address to validate* @param rpc - Optional RPC client (defaults to mainnet)* @returns Classification result:* - IS_WALLET: Valid wallet address* - IS_TOKEN_ACCOUNT: Valid token account* - REJECT: Invalid address for transfers*/export async function validateAddress(inputAddress: Address,rpc: Rpc<GetAccountInfoApi> = defaultRpc): Promise<ValidationResult> {const account = await fetchJsonParsedAccount(rpc, inputAddress);// Log the account data for democonsole.log("\nAccount:", account);// Account doesn't exist onchainif (!account.exists) {// Off-curve = PDA that doesn't exist as an account// Reject conservatively to avoid sending to an address that may be inaccessible.if (isOffCurveAddress(inputAddress)) {return { type: "REJECT", reason: "PDA doesn't exist as an account" };}// On-curve = valid keypair address, treat as unfunded walletreturn { type: "IS_WALLET" };}// Account exists, check program ownerconst owner = account.programAddress;// System Program = walletif (owner === SYSTEM_PROGRAM) {return { type: "IS_WALLET" };}// Token Program or Token-2022, check if token accountif (owner === TOKEN_PROGRAM || owner === TOKEN_2022_PROGRAM) {const accountType = (account.data as { parsedAccountMeta?: { type?: string } }).parsedAccountMeta?.type;if (accountType === "account") {return { type: "IS_TOKEN_ACCOUNT" };}// Reject if not a token account (mint account)return {type: "REJECT",reason: "Not a token account"};}// Unknown program ownerreturn { type: "REJECT", reason: "Unknown program owner" };}/*** Native SOL is only safe to send to a wallet. Any other account locks it.*/function canReceiveNativeSol(result: ValidationResult): boolean {return result.type === "IS_WALLET";}/*** SPL tokens can go to a wallet (via its ATA) or directly to a token account.*/function canReceiveSplToken(result: ValidationResult): boolean {return result.type === "IS_WALLET" || result.type === "IS_TOKEN_ACCOUNT";}// =============================================================================// Examples// =============================================================================
Is this page helpful?