Token Transfers
address.transfer({ to, amount, asset? }).whenConfirmed() sends value in one call — the common path; drop to buildTransferPayload → buildTransaction → send for finer control. Native coin, ERC-20/SPL by address or mint, or by symbol.
The common path is one call: address.transfer(req) lowers a chain-agnostic transfer intent, builds the transaction, signs it, and broadcasts — returning a live TransactionTracker.
const evm = acc.wallets[0].evm;
// Native coin, a token by contract/mint, or a token by symbol — one call.
const receipt = await evm.transfer({
to: recipient,
amount: 1_000_000n,
asset: { symbol: "USDC" }, // omit `asset` for the native coin
}).whenConfirmed();transfer(req, opts?) is a thin composition of buildTransferPayload(req) → buildTransaction(...) → send(): chainId and rpcUrl default from the Address's own network, so you pass only the intent, and it returns the tracker (never a re-sendable transaction). opts carries the send options plus a guard — override the destination-safety gate with transfer(req, { guard: { forceUnsafeTransfer: true } }).
For finer control — setting gas, fees, nonce, or compute budget, or inspecting the transaction before it is sent — lower the intent yourself with buildTransferPayload and build + send it explicitly, as the rest of this page describes.
Lowering an intent with buildTransferPayload
address.buildTransferPayload(req) takes one chain-agnostic transfer intent — send this much of this asset to that recipient — and lowers it into the chain-specific fields a transaction needs: a native value, ERC-20 calldata, or SPL instructions. It does the routing so you do not have to know, per chain, whether "send USDC" means calldata or a pair of instructions.
buildTransferPayload does not build or send a transaction. It returns a TransferPayload — a plain, non-sendable object. Spread it into buildTransaction(...) (which owns network / chainId / gas) and call .send() on that. This keeps buildTransaction(...).send() the single send path.
The intent: TransferRequest and AssetRef
interface TransferRequest {
to: string; // recipient wallet address
asset?: AssetRef; // omitted / { native: true } = the chain's native coin
amount: bigint; // raw base units (wei, lamports, or token smallest unit)
}
type AssetRef =
| { native: true }
| { address: string } // ERC-20 contract / SPL mint
| { symbol: string }; // resolved against tracked assets on this networkamount is always in raw base units — wei for ETH, lamports for SOL, and the token's smallest unit (e.g. 1_000_000n = 1 USDC at 6 decimals) for tokens. An out-of-range amount is a PARAMETER_ERROR.
The three transfer cases
Native coin
Omit asset (or pass { native: true }). On EVM this becomes { to, value }; on Solana { recipient, amount }.
const evmPayload = w0.evm.buildTransferPayload({
to: String(w1.evm.publicKey),
amount: 1_000_000_000_000_000n,
});
// { vm: "evm", to: <recipient>, value: 1_000_000_000_000_000n, data: "0x" }
const svmPayload = w0.svm.buildTransferPayload({
to: String(w1.svm.publicKey),
amount: 5000n,
});
// { vm: "svm", recipient: <recipient>, amount: 5000n }Token by contract address / mint
Name the token by its on-chain address. On EVM this lowers to ERC-20 transfer(address,uint256) calldata; on Solana to SPL instructions.
// EVM ERC-20 — the payload targets the token contract with transfer() calldata
const payload = w0.evm.buildTransferPayload({
to: String(w1.evm.publicKey),
asset: { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }, // USDC
amount: 1_000_000n,
});
// payload.to === the token contract
// payload.value === 0n
// payload.data begins with 0xa9059cbb (the transfer(address,uint256) selector)
// Solana SPL — two instructions
const spl = w0.svm.buildTransferPayload({
to: String(w1.svm.publicKey),
asset: { address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }, // USDC mint
amount: 1_000_000n,
});
// spl.instructions.length === 2Token by symbol
Name the token by symbol. It is resolved against assets tracked on this signer's network (with a curated set of built-ins as a fallback), then re-lowered — so a symbol that maps to the native coin becomes a plain value transfer, and a token symbol becomes calldata / instructions.
const usdc = w0.evm.buildTransferPayload({
to: String(w1.evm.publicKey),
asset: { symbol: "USDC" },
amount: 1_000_000n,
});
// usdc.data begins with 0xa9059cbb (resolved to the ERC-20 token)
const eth = w0.evm.buildTransferPayload({
to: String(w1.evm.publicKey),
asset: { symbol: "ETH" },
amount: 777n,
});
// eth.value === 777n (resolved to the native coin)An unknown symbol — one not tracked on the signer's network — throws PARAMETER_ERROR.
Destination safety
The full signature is buildTransferPayload(req: TransferRequest, opts?: DestinationGuardOptions). Before it lowers the intent, it runs an offline destination-safety gate on the true recipient — the check has to run on the intent, because once lowered an ERC-20's to becomes the token contract and the real recipient is buried in calldata.
If the recipient would irreversibly lose the funds, the call throws IRREGULAR_DESTINATION:
- EVM — the zero address, a known burn address, a precompile, or (for a token send) the ERC-20's own contract.
- SVM — an off-curve address for a native-SOL send, or the incinerator.
The thrown error's details carry destinationCode, severity, chain, address, and asset, so you can branch on the exact class. Override a deliberate case — an intentional burn, funding a program-owned account — with opts.forceUnsafeTransfer: true. (The offline gate only ever produces refuse-tier verdicts; other classes need an RPC lookup and surface through checkDestination below, not this throw — an ERC-20 recipient that is itself a contract is confirm-tier, while an SPL owner that is program-owned is refuse-tier.) A malformed address is always a PARAMETER_ERROR and is not overridable.
// Throws IRREGULAR_DESTINATION — the zero address would burn the funds.
evm.buildTransferPayload({ to: "0x0000000000000000000000000000000000000000", amount: 1n });
// Proceeds — an explicit, acknowledged burn.
evm.buildTransferPayload(
{ to: "0x0000000000000000000000000000000000000000", amount: 1n },
{ forceUnsafeTransfer: true },
);To screen a destination without building — and with the RPC-backed classes too (recipient is a contract / an EIP-7702-delegated EOA / a mint / a program-owned account, plus the SVM warnings for a frozen token account or a non-transferable Token-2022 mint) — use address.checkDestination(req, opts?), which returns a DestinationReport (action: "allow" | "warn" | "confirm" | "refuse") instead of throwing, so a wallet UI can warn the user before the transaction is built.
TransferPayload
The lowered result is a discriminated union keyed on vm:
type TransferPayload =
| { readonly vm: "evm"; readonly to: string; readonly value: bigint; readonly data: string }
| { readonly vm: "svm"; readonly recipient: string; readonly amount: bigint;
readonly instructions?: ReadonlyArray<unknown> };Feeding the payload to buildTransaction
Spread the payload into buildTransaction(...), add whatever that step owns (chainId, rpcUrl), then send:
const evm = acc.wallets[0].evm;
const payload = evm.buildTransferPayload({
to: recipient,
asset: { symbol: "USDC" },
amount: 1_000_000n,
});
const tx = evm.buildTransaction({ ...payload, chainId: 1 });
const tracker = tx.send();
await tracker.whenConfirmed();SPL creates the recipient's token account
For an SPL transfer, the two-instruction payload is an idempotent create of the recipient's associated token account (ATA) followed by the SPL transfer itself. The create is a no-op when the account already exists, so a transfer never fails on a first-time recipient and needs no existence check; the sender pays the account rent when it does not yet exist. Only the recipient ATA is created — an absent sender ATA means a zero balance, which the transfer then refuses.
transfer() vs. buildTransferPayload()
Both exist — they are two altitudes on the same path:
transfer(req, opts?)— the one-call facade at the top of this page. Lowers, builds, signs, and sends in a single call and returns aTransactionTracker. Reach for it by default.buildTransferPayload(req)— the lowering step alone. Returns a non-sendableTransferPayloadthat you spread intobuildTransaction(...)and.send()— keepingbuildTransaction(...).send()the single send path, and giving you a seam to set gas / fees / nonce before anything is broadcast.
// One call — the common path.
const receipt = await address.transfer({ to, asset, amount }).whenConfirmed();
// Explicit — when you need to shape the transaction before sending.
const payload = address.buildTransferPayload({ to, asset, amount });
const tracker = address.buildTransaction({ ...payload, chainId }).send();History: transfer() was removed in 2.5.1 in favor of the explicit buildTransferPayload → buildTransaction → send path, then reintroduced in 2.6.2 as the one-call facade over exactly that path. Both are supported.