Skip to content

Gas-Free Transfers

Gas-Free lets users send RGB assets without holding BTC for mining fees. The service provides BTC inputs; the user contributes only the RGB asset being transferred.

How it works

User                    Service
 │                          │
 │── requestFeeQuote ──────>│
 │<─ FeeQuote (expiresAt) ──│
 │                          │
 │── buildPSBT (local) ─────┤ (user builds PSBT with service UTXOs from quote)
 │── submitPSBT ───────────>│
 │<─ serviceSigned PSBT ────│
 │                          │
 │── broadcastTransfer──────┤ (user signs their inputs, broadcasts)
 │── verifyTransfer ───────>│
 │<─ { status: "verified" }─│

Enable in config

typescript
const sdk = new Orbis1SDK({
  apiKey: process.env.ORBIS_API_KEY!,
  environment: Environment.TESTNET4,
  wallet: { enabled: true, keys },
  features: {
    gasFree: { enabled: true, timeout: 30_000 },
  },
});
await sdk.initialize();

For most use cases, use confirmTransfer — it runs the full flow:

typescript
const gasFree = sdk.gasFree();

// Step 1: Get a quote
const quote = await gasFree.requestFeeQuote({
  userId: 'user-123',
  assetId: 'rgb:2dkSob9…',
  amount: '100250000',       // base units (precision 6 → 100.25 tokens)
  recipientInvoice: 'rgb:~/~/…',
  numInputs: 1,              // optional
  numOutputs: 2,             // optional
});

console.log(`Fee: ${quote.miningFeeSats} sats (expires ${quote.expiresAt})`);
console.log(`Service fee: ${quote.serviceFeeAmount} base units`);

// Step 2: Confirm (builds PSBT → submits → signs → broadcasts → verifies)
const result = await gasFree.confirmTransfer(
  {
    userId: 'user-123',
    assetId: 'rgb:2dkSob9…',
    amount: '100250000',
    recipientInvoice: 'rgb:~/~/…',
  },
  quote
);

console.log(`Broadcast txid: ${result.txid}`);
console.log(`Status: ${result.status}`);       // 'verified' | 'pending_verification'

FeeQuote structure

FieldTypeDescription
quoteIdstringUnique quote identifier
expiresAtstring (ISO 8601)Quote expiry time
miningFeeSatsnumberBTC mining fee covered by service
feeRateSatPerVBytenumberFee rate used for PSBT construction
serviceFeeAmountstringRGB fee charged to user (base units)
serviceFeeInvoicestringRGB invoice for service fee
serviceFeeRecipientIdstringRecipient ID for service fee
witnessUtxoFundingSatsnumberWitness UTXO funding amount (0 for blinded invoices, >0 for witness invoices)
miningUTXOobjectService-provided BTC input UTXO
miningChangeUTXOobject?Service-provided BTC change output (optional - undefined when entire UTXO consumed)
statusstring'pending' | 'accepted' | 'expired' | 'completed' | 'failed'

Advanced staged API

Call individual stages for fine-grained control, retries, or UI progress updates:

typescript
const gasFree = sdk.gasFree();

// Stage 1: Get quote (same as above)
const quote = await gasFree.requestFeeQuote({ userId, assetId, amount, recipientInvoice });

// Stage 2a: Build unsigned PSBT locally (includes service mining UTXOs from quote)
const psbtResult = await gasFree.buildPSBT(
  { userId, assetId, amount, recipientInvoice },
  quote
);
// psbtResult: { unsignedPsbt: string, miningFee: number, externalInputs: [...] }

// Stage 2b: Submit unsigned PSBT + consignment to service for co-signing
const submitResult = await gasFree.submitPSBT(
  quote.quoteId,
  psbtResult,
  assetId
);
// submitResult: { signedPsbtBase64, transactionId, consignmentBase64, ... }

// Stage 2c: Sign user RGB inputs and broadcast
const broadcastResult = await gasFree.broadcastTransfer(submitResult.signedPsbtBase64);
// broadcastResult: { txid, userSignedPsbt }

// Stage 2d: Notify service of successful broadcast
const verifyResult = await gasFree.verifyTransfer(
  quote.quoteId,
  broadcastResult.txid,
  broadcastResult.userSignedPsbt
);
// verifyResult: { status, inMempool, message, transactionId, verifiedAt, quoteId }

verifyResult.status values: 'verified' | 'pending_verification' | 'failed' | 'errored'

Transfer state

Poll getState() to display progress in UI or logs:

typescript
const state = gasFree.getState();
state.statusMeaning
'idle'No transfer in progress
'quote-requested'Quote obtained, waiting for confirmation
'psbt-built'PSBT constructed locally
'submitted'PSBT sent to service; awaiting co-signing
'broadcasted'Transaction broadcast to network
'verified'Service confirmed receipt
'failed'Transfer failed; see state.error

state.quoteId and state.txid are populated at the relevant stages.

Error handling

typescript
import {
  GasFreeError,
  GasFreeErrorCode,
  QuoteExpiredError,
  InvalidPSBTError,
  ConsignmentVerificationError,
  ServiceUnavailableError,
} from 'orbis1-sdk-node';

try {
  const result = await gasFree.confirmTransfer(request, quote);
} catch (error) {
  if (error instanceof QuoteExpiredError) {
    // Quote expired — re-request a fresh quote
    const newQuote = await gasFree.requestFeeQuote(request);
    return gasFree.confirmTransfer(request, newQuote);
  }
  if (error instanceof ServiceUnavailableError) {
    console.error('Service unreachable:', error.message);
  }
  if (error instanceof GasFreeError) {
    console.error(`Gas-Free error [${error.gasFreeCode}]:`, error.message);
  }
  throw error;
}

Precision handling

All monetary values are in base units (integer strings):

ValuePrecision 6 display
"100250000"100.25 tokens
"1000000"1.00 token

Convert for display: Number(baseUnits) / 10 ** precision

Constraints

  • Gas-Free requires wallet.enabled: true.
  • Gas-Free is not available in REGTEST.
  • Quotes expire — always check quote.expiresAt before calling confirmTransfer.

Released under the MIT License.