UTXO Recipes
Common Bitcoin flows — multi-recipient send, balance prechecks, confirmation polling.
Copy-paste flows for the Bitcoin tasks most apps need.
Multi-recipient payout
Send to N addresses in one transaction. Saves on fees vs N separate txs, since Bitcoin supports multiple outputs natively.
import { sendTransfer } from '@oviato/connect';
const r = await sendTransfer({
recipients: [
{ address: 'bc1q...', amount: 10000 },
{ address: 'bc1p...', amount: 5000 },
{ address: 'bc1q...', amount: 7500 },
],
fee: 'auto',
memo: 'Weekly payouts',
});
if (r.status === 'success' && r.type === 'sendTransferSuccess') {
console.log('Batch txid:', r.data.txid);
}Balance precheck before a send
import { rpc, sendTransfer, getSession } from '@oviato/connect';
async function sendWithPrecheck(to: string, amount: number) {
const session = getSession()!;
const { result } = await rpc.getBalance({ address: session.address });
if (!result || result.chainType !== 'utxo') throw new Error('Not a UTXO session');
const confirmed = result.balance.confirmed.raw;
const estFee = 1500; // rough sat estimate; better: query mempool fees
if (confirmed < amount + estFee) {
throw new Error(`Insufficient confirmed balance: ${confirmed} sats`);
}
return sendTransfer({
recipient: { address: to, amount },
fee: 'auto',
});
}Use confirmed (not total) for the precheck. Unconfirmed funds can be
replaced via RBF before they mine — relying on them for the next send
risks a double-spend.
Sign a server-built PSBT
Common pattern when coin selection happens server-side — your backend builds a PSBT, your frontend calls utxo.signPsbt to sign + broadcast.
import { utxo } from '@oviato/connect';
async function acceptListing(listingId: string) {
// 1. Backend returns a PSBT
const { psbt } = await fetch(`/api/listings/${listingId}/prepare`).then((r) => r.json());
// 2. User signs + broadcasts
const r = await utxo.signPsbt({ psbt, broadcast: true });
if (r.status === 'success' && r.type === 'signPsbtBroadcastSuccess') {
return { txid: r.data.txid };
}
throw new Error(r.status === 'error' ? r.message : 'Sign failed');
}Sign-only (counterparty broadcasts)
When a different party will broadcast after you sign:
const r = await utxo.signPsbt({ psbt: serverPsbt, broadcast: false });
if (r.status === 'success' && r.type === 'signPsbtSuccess') {
await fetch('/api/listings/complete', {
method: 'POST',
body: JSON.stringify({ signedPsbt: r.data.psbt }),
});
}Watch for confirmation
The SDK doesn't currently ship a waitForConfirmation for UTXO. Poll via the RPC client:
import { rpc } from '@oviato/connect';
async function waitForConfirmation(txid: string, address: string, minConfirms = 1) {
while (true) {
const r = await rpc.getTransactionHistory({ address, limit: 20 });
const match = r.result?.transactions.find((t) => t.txid === txid);
if (match && match.confirmations >= minConfirms) return match;
await new Promise((r) => setTimeout(r, 15_000)); // 15s poll
}
}For production, prefer a block-indexer webhook over polling.
Related
- Sign PSBT — full
utxo.signPsbtreference - Send Transfers — simpler native BTC sends