Skip to main content

SDK Reference

The @velumx/sdk package is the primary integration point for VelumX gasless transactions.

Installation

npm install @velumx/sdk

Initialization

import { VelumXClient } from '@velumx/sdk';

const velumx = new VelumXClient({
  paymasterUrl: '/api/velumx/proxy', // Your secure backend proxy
  network: 'mainnet',                // 'mainnet' | 'testnet'
});

Core Methods

estimateFee(params)

Estimates the required fee in a SIP-010 token based on estimated gas units. Always call this before building a USER_PAYS transaction — the response tells you the active policy, the fee amount, and the relayer address to pass into the paymaster.
const estimate = await velumx.estimateFee({
  feeToken: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.aeusdc', // optional
  estimatedGas: 150000, // optional, defaults to 150000
});

// Returns:
// {
//   maxFee: string,           // fee in token micro-units → pass as fee-amount to paymaster
//   feeToken: string,         // resolved fee token principal
//   policy: 'DEVELOPER_SPONSORS' | 'USER_PAYS',
//   relayerAddress: string,   // relayer's Stacks address → pass as relayer to paymaster
// }
relayerAddress is the source of truth for the relayer arg in USER_PAYS transactions. Do not hardcode it or read it from an env var — always use estimate.relayerAddress. The relayer address can change when VelumX rotates keys or when your project is reassigned to a different relayer node.
const estimate = await velumx.estimateFee({ feeToken: '...aeusdc', estimatedGas: 200_000 });

// DEVELOPER_SPONSORS: relayerAddress is not used in the transaction at all.
// USER_PAYS: pass it directly to the paymaster as principalCV.
if (estimate.policy === 'USER_PAYS') {
  if (!estimate.relayerAddress) throw new Error('Relayer address missing from fee estimate');
  // use estimate.relayerAddress in functionArgs below
}

sponsorBatch(transactions, options?)

Sponsors up to 25 transactions in a single API call. Each transaction is processed independently — one failure does not abort the rest. Always returns HTTP 200; check each item’s error field for per-item failures. When to use batch vs. single sponsor():
  • Airdrop distributions — submit tokens to many recipients without looping over sponsor().
  • Multi-step DeFi setup — approve + stake + claim in one round-trip instead of three sequential calls.
  • Queue draining — flush a backlog of pending user actions (e.g. queued bridge withdrawals) in one shot.
const result = await velumx.sponsorBatch([
  { txHex: signedTx1 },
  { txHex: signedTx2 },
  { txHex: signedTx3 },
  // For USER_PAYS, add feeAmount per item:
  // { txHex: signedTx4, feeAmount: estimate.maxFee },
]);

result.results.forEach(item => {
  if ('error' in item) {
    console.error(`TX[${item.index}] failed:`, item.error);
  } else {
    console.log(`TX[${item.index}] sponsored:`, item.txid);
  }
});
console.log(result.summary);
// { total: 3, succeeded: 2, failed: 1 }
Rate limits: 5 batch calls/min per API key, 10/min per IP.
The primary method. Submits a signed sponsored transaction to the VelumX relayer for co-signing and broadcast.
const result = await velumx.sponsor(signedTxHex, {
  feeToken: 'SP...aeusdc',  // required for USER_PAYS, omit for DEVELOPER_SPONSORS
  feeAmount: '250000',       // required for USER_PAYS
  network: 'mainnet',        // optional, defaults to client network
});

// Returns: { txid: string }
// Throws: RelayerError if rejected
For DEVELOPER_SPONSORS — omit feeToken and feeAmount:
const { txid } = await velumx.sponsor(signedTxHex);
For USER_PAYS — include fee params:
const { txid } = await velumx.sponsor(signedTxHex, {
  feeToken: estimate.feeToken,
  feeAmount: estimate.maxFee,
});

buildSponsoredContractCall(params)

Helper that builds an unsigned sponsored ContractCall transaction ready for wallet signing.
import { buildSponsoredContractCall } from '@velumx/sdk';
import { uintCV, contractPrincipalCV } from '@stacks/transactions';

// Token CVs for the Bitflow STX/stSTX pool (real example)
const tokenX = contractPrincipalCV('SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG', 'wstx');
const tokenY = contractPrincipalCV('SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG', 'ststx');

const unsignedTx = await buildSponsoredContractCall({
  contractAddress: 'SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M',
  contractName: 'stableswap-stx-ststx-v-1-2',
  functionName: 'swap-x-for-y',
  functionArgs: [
    uintCV(1n),           // pool id
    tokenX,               // token-x (STX)
    tokenY,               // token-y (stSTX)
    uintCV(1_000_000n),   // amount-in: 1 STX
    uintCV(990_000n),     // min-amount-out: 1% slippage
  ],
  publicKey: userPublicKey,   // user's Stacks public key — see note below
  nonce: 5n,                  // optional — see nonce note below
  network: 'mainnet',         // optional, defaults to 'mainnet'
});

// Returns: Uint8Array (serialized unsigned sponsored tx)
Getting publicKey: Use stx_getAddresses from @stacks/connect after wallet connection:
const result = await request('stx_getAddresses') as {
  addresses: Array<{ address: string; publicKey: string }>;
};
const publicKey = result.addresses.find(a => a.address === userAddress)?.publicKey
  ?? result.addresses[0].publicKey;
Passing to stx_signTransaction: buildSponsoredContractCall returns a Uint8Array. Some wallet versions expect a hex string. Convert if needed:
const txForSigning = unsignedTx instanceof Uint8Array
  ? Buffer.from(unsignedTx).toString('hex')
  : unsignedTx;

const signResult = await request('stx_signTransaction', {
  transaction: txForSigning,
  broadcast: false,
});
// signResult.transaction is always a hex string — pass directly to velumx.sponsor()
Nonce handling: If nonce is omitted, the SDK fetches it from the Stacks API. This is fine for single transactions, but for rapid sequential calls (e.g. batch setup flows where you build multiple txs before any confirms), fetch the nonce once and increment manually:
const accountRes = await fetch(`https://api.mainnet.hiro.so/v2/accounts/${userAddress}?proof=0`);
const { nonce: currentNonce } = await accountRes.json();

const tx1 = await buildSponsoredContractCall({ ..., nonce: BigInt(currentNonce) });
const tx2 = await buildSponsoredContractCall({ ..., nonce: BigInt(currentNonce) + 1n });
const tx3 = await buildSponsoredContractCall({ ..., nonce: BigInt(currentNonce) + 2n });

Types

interface SponsorOptions {
  feeToken?: string;   // SIP-010 contract principal
  feeAmount?: string;  // fee in token micro-units
  network?: 'mainnet' | 'testnet';
}

interface FeeEstimateResult {
  maxFee: string;
  feeToken: string;
  policy: 'DEVELOPER_SPONSORS' | 'USER_PAYS';
  relayerAddress: string;
}

interface SponsorResult {
  txid: string;
  status: string;
}

interface BatchSponsorItem {
  /** Signed sponsored transaction hex */
  txHex: string;
  /** Fee in token micro-units — omit for DEVELOPER_SPONSORS */
  feeAmount?: string;
}

type BatchSponsorItemResult =
  | ({ index: number } & SponsorResult)
  | { index: number; error: string };

interface BatchSponsorResult {
  results: BatchSponsorItemResult[];
  summary: {
    total: number;
    succeeded: number;
    failed: number;
  };
}

interface ContractCallParams {
  contractAddress: string;
  contractName: string;
  functionName: string;
  functionArgs: ClarityValue[];
  publicKey: string;
  nonce?: bigint;
  network?: 'mainnet' | 'testnet';
}

class RelayerError extends Error {
  reason: string;
}

Error Handling

velumx.sponsor() and velumx.sponsorBatch() throw RelayerError when the relayer rejects a request. Always catch it separately from generic errors so you can surface a useful message to the user.
import { VelumXClient, RelayerError } from '@velumx/sdk';

try {
  const { txid } = await velumx.sponsor(signedTxHex);
} catch (err) {
  if (err instanceof RelayerError) {
    // err.reason is a machine-readable string from the relayer
    // e.g. 'insufficient_relayer_balance', 'invalid_sponsor_sig', 'fee_too_low'
    console.error('Relayer rejected the transaction:', err.reason);
    showToast(`Transaction failed: ${err.reason}`);
  } else {
    // Network error, proxy misconfiguration, etc.
    throw err;
  }
}
For batch calls, per-item failures are returned inline — only a top-level network/auth failure throws:
try {
  const result = await velumx.sponsorBatch([
    { txHex: signedTx1 },
    { txHex: signedTx2 },
  ]);

  for (const item of result.results) {
    if ('error' in item) {
      console.warn(`TX ${item.index} failed: ${item.error}`);
    } else {
      console.log(`TX ${item.index} confirmed: ${item.txid}`);
    }
  }
} catch (err) {
  if (err instanceof RelayerError) {
    // Entire batch request was rejected (auth failure, rate limit, etc.)
    console.error('Batch rejected:', err.reason);
  }
}

Complete Example — DEVELOPER_SPONSORS

import { VelumXClient, buildSponsoredContractCall, RelayerError } from '@velumx/sdk';
import { uintCV, contractPrincipalCV } from '@stacks/transactions';
import { request } from '@stacks/connect';

const velumx = new VelumXClient({ paymasterUrl: '/api/velumx', network: 'mainnet' });

// Get the user's public key after wallet connection
const addrResult = await request('stx_getAddresses') as {
  addresses: Array<{ address: string; publicKey: string }>;
};
const userPublicKey = addrResult.addresses[0].publicKey;

// Token contracts for the Bitflow STX/stSTX pool
const STX_TOKEN  = contractPrincipalCV('SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG', 'wstx');
const STST_TOKEN = contractPrincipalCV('SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG', 'ststx');

// 1. Build unsigned sponsored tx
//    DEVELOPER_SPONSORS: call the protocol contract directly — no fee args needed
const unsignedTx = await buildSponsoredContractCall({
  contractAddress: 'SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M',
  contractName: 'stableswap-stx-ststx-v-1-2',
  functionName: 'swap-x-for-y',
  functionArgs: [
    uintCV(1n),          // pool id
    STX_TOKEN,           // token-x
    STST_TOKEN,          // token-y
    uintCV(1_000_000n),  // amount-in (1 STX)
    uintCV(990_000n),    // min-amount-out (1% slippage)
  ],
  publicKey: userPublicKey,
});

// 2. User signs (no broadcast)
// buildSponsoredContractCall returns Uint8Array — convert to hex for the wallet
const txForSigning = unsignedTx instanceof Uint8Array
  ? Buffer.from(unsignedTx).toString('hex')
  : unsignedTx;

const signResult = await request('stx_signTransaction', {
  transaction: txForSigning,
  broadcast: false,
});

// 3. Relayer co-signs and broadcasts — no fee params needed for DEVELOPER_SPONSORS
try {
  const { txid } = await velumx.sponsor(signResult.transaction);
  console.log('txid:', txid);
} catch (err) {
  if (err instanceof RelayerError) console.error('Rejected:', err.reason);
  else throw err;
}

Complete Example — USER_PAYS

import { buildSponsoredContractCall, RelayerError } from '@velumx/sdk';
import { uintCV, principalCV, contractPrincipalCV } from '@stacks/transactions';

// 1. Estimate fee — this gives you the policy, fee amount, AND relayer address
const estimate = await velumx.estimateFee({
  feeToken: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.aeusdc',
  estimatedGas: 200_000,
});

// Guard: relayerAddress is required for USER_PAYS — always comes from estimateFee
if (!estimate.relayerAddress) throw new Error('Relayer address missing from fee estimate');

// 2. Build call to your paymaster contract
//    The paymaster atomically collects the fee then executes the action.
//    fee-amount, relayer, and fee-token are always the last three args.
//    relayer = estimate.relayerAddress — never hardcode this value.
const unsignedTx = await buildSponsoredContractCall({
  contractAddress: 'SP...your-paymaster',
  contractName: 'my-paymaster-v1',
  functionName: 'my-action',
  functionArgs: [
    uintCV(1_000_000n),                                                            // your action param
    uintCV(BigInt(estimate.maxFee)),                                               // fee-amount
    principalCV(estimate.relayerAddress),                                          // relayer — from estimateFee
    contractPrincipalCV('SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR', 'aeusdc'), // fee-token
  ],
  publicKey: userPublicKey,
});

// 3. User signs (no broadcast)
const txForSigning = unsignedTx instanceof Uint8Array
  ? Buffer.from(unsignedTx).toString('hex')
  : unsignedTx;

const signResult = await request('stx_signTransaction', {
  transaction: txForSigning,
  broadcast: false,
});

// 4. Sponsor — pass fee params so the relayer can validate the on-chain fee transfer
try {
  const { txid } = await velumx.sponsor(signResult.transaction, {
    feeToken: estimate.feeToken,
    feeAmount: estimate.maxFee,
  });
  console.log('txid:', txid);
} catch (err) {
  if (err instanceof RelayerError) console.error('Rejected:', err.reason);
  else throw err;
}

Complete Example — Batch Sponsorship

sponsorBatch is useful any time you need to submit multiple independent transactions in one shot — without waiting for each to confirm before sending the next. Common use cases:
  • Airdrop distributions — send tokens to hundreds of recipients in a single API call instead of looping over sponsor().
  • Multi-step DeFi setup — approve + stake + claim in one batch rather than three sequential round-trips.
  • Processing a queue — drain a backlog of pending user actions (e.g. queued bridge withdrawals) atomically.
Each transaction is processed independently. One failure does not block the rest.
import { RelayerError } from '@velumx/sdk';

// Build and sign each tx independently (same flow as single sponsor)
const [signedTx1, signedTx2, signedTx3] = await Promise.all([
  buildAndSign(params1, userPublicKey),
  buildAndSign(params2, userPublicKey),
  buildAndSign(params3, userPublicKey),
]);

try {
  const result = await velumx.sponsorBatch([
    { txHex: signedTx1 },
    { txHex: signedTx2 },
    { txHex: signedTx3 },
    // For USER_PAYS, add feeAmount per item:
    // { txHex: signedTx4, feeAmount: estimate.maxFee },
  ]);

  for (const item of result.results) {
    if ('error' in item) {
      console.warn(`TX[${item.index}] failed: ${item.error}`);
    } else {
      console.log(`TX[${item.index}] sponsored: ${item.txid}`);
    }
  }

  console.log(result.summary);
  // { total: 3, succeeded: 3, failed: 0 }
} catch (err) {
  if (err instanceof RelayerError) {
    // Top-level rejection — auth failure, rate limit, malformed request
    console.error('Batch rejected:', err.reason);
  }
}
Rate limits: 5 batch calls/min per API key, 10/min per IP.