
Transfer
- 169 installs
- 9 repo stars
- Updated July 16, 2026
- aibtcdev/skills
Execute reliable STX, BTC, and token transfers with fee estimation, memo handling, and confirmation UX in agents and wallets.
About
Guides agents through safe Bitcoin and Stacks transfer implementation: validate recipients and amounts, estimate fees, attach memos or contract calls when needed, broadcast with correct sequencing, poll confirmations, and prevent duplicate sends or silent failures in automated wallets.
- Recipient and amount validation
- Fee estimation and change handling
- Memo and contract-call attachment rules
- Confirmation and receipt polling
- Idempotency and duplicate-send prevention
Transfer by the numbers
- 169 all-time installs (skills.sh)
- Ranked #115 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aibtcdev/skills --skill transferAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 169 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 16, 2026 |
| Repository | aibtcdev/skills ↗ |
What it does
Execute reliable STX, BTC, and token transfers with fee estimation, memo handling, and confirmation UX in agents and wallets.
Files
Transfer Skill
Unified asset transfer skill for the Stacks L2 — sends STX, SIP-010 fungible tokens, and SIP-009 NFTs to a recipient address. All three subcommands require an unlocked wallet.
Usage
bun run transfer/transfer.ts <subcommand> [options]Subcommands
stx
Transfer STX to a recipient address. Amount is specified in micro-STX (1 STX = 1,000,000 micro-STX).
bun run transfer/transfer.ts stx \
--recipient <address> \
--amount <microStx> \
[--memo <text>] \
[--fee low|medium|high|<microStx>]Options:
--recipient(required) — Stacks address of the recipient (starts with SP or ST)--amount(required) — Amount in micro-STX (e.g.,2000000for 2 STX)--memo(optional) — Memo text to attach to the transfer (max 34 bytes)--fee(optional) — Fee preset (low,medium,high) or micro-STX amount; auto-estimated if omitted
Output:
{
"success": true,
"txid": "abc123...",
"from": "SP2...",
"recipient": "SP3...",
"amount": "2 STX",
"amountMicroStx": "2000000",
"memo": null,
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/abc123..."
}token
Transfer any SIP-010 fungible token to a recipient. Accepts a well-known token symbol (sBTC, USDCx, ALEX, DIKO) or a full contract ID.
bun run transfer/transfer.ts token \
--recipient <address> \
--amount <uint> \
--contract <token-symbol-or-contract-id> \
[--memo <text>] \
[--fee low|medium|high|<microStx>]Options:
--recipient(required) — Stacks address of the recipient--amount(required) — Amount in the token's smallest unit (check token decimals)--contract(required) — Token symbol (e.g.,sBTC) or full contract ID (e.g.,SP2...my-token)--memo(optional) — Memo text (max 34 bytes)--fee(optional) — Fee preset or micro-STX amount; auto-estimated if omitted
Output:
{
"success": true,
"txid": "abc123...",
"from": "SP2...",
"recipient": "SP3...",
"contract": "SP2...my-token",
"amount": "1000000",
"memo": null,
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/abc123..."
}nft
Transfer a SIP-009 NFT to a recipient. Requires the NFT collection contract ID and the specific token ID.
bun run transfer/transfer.ts nft \
--recipient <address> \
--token-id <uint> \
--contract <contract-id> \
[--fee low|medium|high|<microStx>]Options:
--recipient(required) — Stacks address of the recipient--token-id(required) — Integer token ID of the NFT to transfer--contract(required) — NFT collection contract ID (e.g.,SP2...my-nft)--fee(optional) — Fee preset or micro-STX amount; auto-estimated if omitted
Output:
{
"success": true,
"txid": "abc123...",
"from": "SP2...",
"recipient": "SP3...",
"contract": "SP2...my-nft",
"tokenId": 42,
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/abc123..."
}Notes
- Wallet must be unlocked before any transfer. Use
bun run wallet/wallet.ts unlockfirst. - For STX, 1 STX = 1,000,000 micro-STX. For tokens, check decimals with the
tokensskill'sget-infosubcommand. - NFT token IDs are integers. Use the
nftskill'sget-holdingssubcommand to list owned token IDs. - Fee is paid in STX regardless of the asset being transferred. Ensure sufficient STX balance for the fee.
- The
--memooption is available for STX and token transfers, but not NFT transfers (not part of the SIP-009 standard).
Transfer Agent
This agent handles unified asset transfers on the Stacks L2. Use it to send STX, fungible tokens (SIP-010), or NFTs (SIP-009) to any Stacks address. All three subcommands sign and broadcast a transaction and require an unlocked wallet with enough STX for fees.
Prerequisites
- Wallet unlocked via
bun run wallet/wallet.ts unlock - Sufficient STX balance for transaction fees (check with
bun run stx/stx.ts get-balance) - For
stx: sufficient STX balance to cover--amountplus fees - For
token: sufficient token balance (check withbun run tokens/tokens.ts get-balance --token <contract>) - For
nft: ownership of the NFT being transferred (check withbun run nft/nft.ts get-holdings)
Decision Logic
| Asset type | Subcommand | Key options |
|---|---|---|
| Native STX | stx | --recipient, --amount (micro-STX), optional --memo |
| SIP-010 fungible token | token | --recipient, --amount (smallest unit), --contract (symbol or contract ID) |
| SIP-009 NFT | nft | --recipient, --token-id, --contract (collection contract ID) |
Use stx for native Stacks token transfers. Use token for any SIP-010 compliant fungible token (sBTC, USDCx, ALEX, DIKO, or custom). Use nft for SIP-009 NFT ownership transfers.
Safety Checks
- Before
stx: verify recipient address starts withSP(mainnet) orST(testnet) and the sender has enough balance (stx get-balance) - Before
stx: 1 STX = 1,000,000 micro-STX — double-check unit conversion before sending large amounts - Before
token: confirm token decimals withbun run tokens/tokens.ts get-info --token <contract>to avoid unit errors - Before
token: verify sender balance withbun run tokens/tokens.ts get-balance --token <contract> - Before
nft: confirm ownership withbun run nft/nft.ts get-holdingsand verify--token-idmatches an owned token - Never reuse a memo containing sensitive information — memos are stored on-chain permanently
- Fees are paid in STX for all three asset types — always maintain a small STX reserve for fees (~0.01 STX minimum)
Error Handling
| Error message | Cause | Fix |
|---|---|---|
| "No active wallet found." | Wallet not unlocked or session expired | Run bun run wallet/wallet.ts unlock |
| "--token-id must be a non-negative integer" | Non-numeric or negative token ID | Pass a valid positive integer for --token-id |
| "--amount must be a positive integer" | Zero or non-numeric amount | Pass a valid positive integer for --amount |
| "Insufficient balance" | Not enough STX or token balance | Check balance with stx/stx.ts get-balance or tokens/tokens.ts get-balance |
| "Token not found" | Unknown token symbol or wrong contract ID | Verify contract ID with bun run tokens/tokens.ts get-info --token <contract> |
| "NFT not owned by sender" | Sending an NFT you do not own | Confirm ownership with bun run nft/nft.ts get-holdings |
Output Handling
All three subcommands return a JSON object on success:
txid— use this to track the transaction withbun run stx/stx.ts get-transaction-status --txid <txid>explorerUrl— direct link to the transaction in the Hiro Explorersuccess: trueconfirms the transaction was broadcast (not confirmed — poll status separately if needed)
Example Invocations
# Send 5 STX to another address
bun run transfer/transfer.ts stx \
--recipient SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159 \
--amount 5000000
# Send 100 USDCx tokens
bun run transfer/transfer.ts token \
--recipient SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159 \
--amount 100000000 \
--contract USDCx
# Transfer an NFT (token #42 from a collection)
bun run transfer/transfer.ts nft \
--recipient SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159 \
--token-id 42 \
--contract SP2...my-nft-collection#!/usr/bin/env bun
/**
* Transfer skill CLI
* Unified STX, SIP-010 token, and SIP-009 NFT transfers on Stacks L2
*
* Usage: bun run transfer/transfer.ts <subcommand> [options]
*/
import { Command } from "commander";
import { NETWORK, getExplorerTxUrl } from "../src/lib/config/networks.js";
import { getAccount } from "../src/lib/services/x402.service.js";
import { transferStx } from "../src/lib/transactions/builder.js";
import { getTokensService } from "../src/lib/services/tokens.service.js";
import { getNftService } from "../src/lib/services/nft.service.js";
import { resolveFee } from "../src/lib/utils/fee.js";
import { printJson, handleError } from "../src/lib/utils/cli.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Format micro-STX as a human-readable STX string.
*/
function formatStx(microStx: string): string {
const micro = BigInt(microStx);
const stx = micro / BigInt(1_000_000);
const remainder = micro % BigInt(1_000_000);
if (remainder === 0n) {
return stx.toString() + " STX";
}
const padded = remainder.toString().padStart(6, "0").replace(/0+$/, "");
return `${stx}.${padded} STX`;
}
// ---------------------------------------------------------------------------
// Program
// ---------------------------------------------------------------------------
const program = new Command();
program
.name("transfer")
.description(
"Unified Stacks L2 asset transfers: send STX, SIP-010 fungible tokens, or SIP-009 NFTs to any address. All subcommands require an unlocked wallet."
)
.version("0.1.0");
// ---------------------------------------------------------------------------
// stx
// ---------------------------------------------------------------------------
program
.command("stx")
.description(
"Transfer STX to a recipient address. " +
"Requires an unlocked wallet. " +
"1 STX = 1,000,000 micro-STX."
)
.requiredOption(
"--recipient <address>",
"Stacks address to send to (starts with SP or ST)"
)
.requiredOption(
"--amount <microStx>",
"Amount in micro-STX (e.g., '2000000' for 2 STX)"
)
.option(
"--memo <text>",
"Optional memo message to include with the transfer (max 34 bytes)"
)
.option(
"--fee <fee>",
"Fee preset (low|medium|high) or micro-STX amount; auto-estimated if omitted"
)
.action(
async (opts: {
recipient: string;
amount: string;
memo?: string;
fee?: string;
}) => {
try {
let amountBigInt: bigint;
try {
amountBigInt = BigInt(opts.amount);
} catch {
throw new Error("--amount must be a positive integer (whole micro-STX, no decimals)");
}
if (amountBigInt <= 0n) {
throw new Error("--amount must be a positive integer");
}
const account = await getAccount();
const resolvedFee = await resolveFee(opts.fee, NETWORK, "token_transfer");
const result = await transferStx(
account,
opts.recipient,
amountBigInt,
opts.memo,
resolvedFee
);
printJson({
success: true,
txid: result.txid,
from: account.address,
recipient: opts.recipient,
amount: formatStx(opts.amount),
amountMicroStx: opts.amount,
memo: opts.memo || null,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// token
// ---------------------------------------------------------------------------
program
.command("token")
.description(
"Transfer a SIP-010 fungible token to a recipient address. " +
"Accepts a token symbol (sBTC, USDCx, ALEX, DIKO) or full contract ID. " +
"Requires an unlocked wallet."
)
.requiredOption(
"--recipient <address>",
"Stacks address to send to"
)
.requiredOption(
"--amount <uint>",
"Amount in smallest token unit (check decimals with: bun run tokens/tokens.ts get-info --token <contract>)"
)
.requiredOption(
"--contract <token>",
"Token symbol (e.g., 'USDCx', 'sBTC') or full contract ID"
)
.option(
"--memo <text>",
"Optional memo message (max 34 bytes)"
)
.option(
"--fee <fee>",
"Fee preset (low|medium|high) or micro-STX amount; auto-estimated if omitted"
)
.action(
async (opts: {
recipient: string;
amount: string;
contract: string;
memo?: string;
fee?: string;
}) => {
try {
let amountBigInt: bigint;
try {
amountBigInt = BigInt(opts.amount);
} catch {
throw new Error("--amount must be a positive integer (whole token atoms, no decimals)");
}
if (amountBigInt <= 0n) {
throw new Error("--amount must be a positive integer");
}
const tokensService = getTokensService(NETWORK);
const account = await getAccount();
const resolvedFee = await resolveFee(opts.fee, NETWORK, "contract_call");
const result = await tokensService.transfer(
account,
opts.contract,
opts.recipient,
amountBigInt,
opts.memo,
resolvedFee
);
printJson({
success: true,
txid: result.txid,
from: account.address,
recipient: opts.recipient,
contract: opts.contract,
amount: opts.amount,
memo: opts.memo || null,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// nft
// ---------------------------------------------------------------------------
program
.command("nft")
.description(
"Transfer a SIP-009 NFT to a recipient address. " +
"Requires the NFT collection contract ID and the specific token ID. " +
"Requires an unlocked wallet."
)
.requiredOption(
"--recipient <address>",
"Stacks address to send to"
)
.requiredOption(
"--token-id <uint>",
"Integer token ID of the NFT to transfer"
)
.requiredOption(
"--contract <contractId>",
"NFT collection contract ID (e.g., SP2...my-nft)"
)
.option(
"--fee <fee>",
"Fee preset (low|medium|high) or micro-STX amount; auto-estimated if omitted"
)
.action(
async (opts: {
recipient: string;
tokenId: string;
contract: string;
fee?: string;
}) => {
try {
let tokenId: number;
try {
const tokenIdBigInt = BigInt(opts.tokenId);
if (tokenIdBigInt < 0n) {
throw new Error("--token-id must be a non-negative integer");
}
tokenId = Number(tokenIdBigInt);
} catch {
throw new Error("--token-id must be a non-negative integer");
}
const nftService = getNftService(NETWORK);
const account = await getAccount();
const resolvedFee = await resolveFee(opts.fee, NETWORK, "contract_call");
const result = await nftService.transfer(
account,
opts.contract,
tokenId,
opts.recipient,
resolvedFee
);
printJson({
success: true,
txid: result.txid,
from: account.address,
recipient: opts.recipient,
contract: opts.contract,
tokenId,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// Parse
// ---------------------------------------------------------------------------
program.parse(process.argv);