Skip to main content

The Paymaster Pattern

A paymaster is a Clarity contract that atomically collects a SIP-010 fee from the user and executes a protocol action in a single transaction.

When Do You Need a Paymaster?

PolicyPaymaster Needed?
DEVELOPER_SPONSORS❌ No — call any contract directly
USER_PAYS✅ Yes — fee must be collected on-chain atomically
For DEVELOPER_SPONSORS, you build any contract call as a sponsored transaction and call velumx.sponsor(). No paymaster contract involved. For USER_PAYS, you need a paymaster because the SIP-010 fee transfer must happen atomically with the protocol action. If the action fails, the fee reverts. This guarantee requires an on-chain contract.

The Pattern

Every paymaster function follows the same three-step pattern:
(define-public (my-action
    (... action params ...)
    (fee-amount uint)
    (relayer principal)
    (fee-token <sip-010-trait>))
  (let ((user tx-sender))
    ;; Step 1: Collect fee atomically
    (try! (collect-fee fee-token fee-amount relayer))

    ;; Step 2: Execute protocol action
    (try! (contract-call? 'SP...target-contract action-function ...))

    ;; Step 3: Emit event
    (print { event: "my-action", user: user, fee-amount: fee-amount })
    (ok true)
  )
)
Atomicity guarantee: If Step 2 fails, the entire transaction reverts — including the fee transfer in Step 1. Users are never charged without their action executing.

The VelumX DeFi Reference Paymaster

VelumX deploys velumx-defi-paymaster-v1-1 for the VelumX DeFi frontend. It supports:
  • Bitflow StableSwap pool swaps (all pools, both directions)
  • Bitflow multi-hop router swaps (all router contracts)
  • Velar XYK and stableswap router swaps (single-hop through 4-hop)
  • USDCx bridge (burn for cross-chain withdrawal to Ethereum)
This contract is the canonical reference implementation. Developers copy it as a starting point for their own paymaster contracts.

Building Your Own Paymaster

  1. Copy velumx-defi-paymaster-v1-1.clar from the VelumX Contracts repo
  2. Replace the Bitflow/USDCx calls with your own protocol calls
  3. Keep the collect-fee private function exactly as-is
  4. Deploy to mainnet
  5. Point your SDK calls at your paymaster contract address

The collect-fee Function

This is the only piece of the paymaster that must stay unchanged. It handles the atomic fee transfer and enforces the two security invariants:
(define-constant ERR-ZERO-FEE      (err u101))
(define-constant ERR-SELF-TRANSFER (err u102))

(define-private (collect-fee
    (fee-token <sip-010-trait>)
    (fee-amount uint)
    (relayer principal))
  (let ((user tx-sender))
    (asserts! (> fee-amount u0) ERR-ZERO-FEE)
    (asserts! (not (is-eq user relayer)) ERR-SELF-TRANSFER)
    (contract-call? fee-token transfer fee-amount user relayer none)
  )
)
  • ERR-ZERO-FEE — prevents zero-fee calls that would let users bypass the fee entirely.
  • ERR-SELF-TRANSFER — prevents the user from setting themselves as the relayer to collect their own fee.

Minimal Paymaster Example

Replace do-thing with your own protocol call. The fee args (fee-amount, relayer, fee-token) are always the last three parameters by convention.
;; my-protocol-paymaster-v1.clar
(use-trait sip-010-trait .sip-010-trait.sip-010-trait)

(define-constant ERR-ZERO-FEE      (err u101))
(define-constant ERR-SELF-TRANSFER (err u102))

(define-private (collect-fee
    (fee-token <sip-010-trait>)
    (fee-amount uint)
    (relayer principal))
  (let ((user tx-sender))
    (asserts! (> fee-amount u0) ERR-ZERO-FEE)
    (asserts! (not (is-eq user relayer)) ERR-SELF-TRANSFER)
    (contract-call? fee-token transfer fee-amount user relayer none)
  )
)

(define-public (my-action
    (amount uint)
    (fee-amount uint)
    (relayer principal)
    (fee-token <sip-010-trait>))
  (let ((user tx-sender))
    ;; 1. Collect fee atomically — reverts with the action if the protocol call fails
    (try! (collect-fee fee-token fee-amount relayer))
    ;; 2. Execute your protocol action
    (try! (contract-call? 'SP...my-protocol do-thing user amount))
    ;; 3. Emit structured event for off-chain indexing
    (print { event: "my-action", user: user, amount: amount, fee-amount: fee-amount })
    (ok true)
  )
)

Calling Your Paymaster from the SDK

The relayer address always comes from estimateFee() — never hardcode it. The relayer address can change when VelumX rotates keys or reassigns your project to a different node.
import { buildSponsoredContractCall } from '@velumx/sdk';
import { uintCV, principalCV, contractPrincipalCV } from '@stacks/transactions';

const estimate = await velumx.estimateFee({
  feeToken: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.aeusdc',
  estimatedGas: 150_000,
});

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

const unsignedTx = await buildSponsoredContractCall({
  contractAddress: 'SP...your-paymaster',
  contractName: 'my-protocol-paymaster-v1',
  functionName: 'my-action',
  functionArgs: [
    uintCV(1_000_000n),                                                            // amount
    uintCV(BigInt(estimate.maxFee)),                                               // fee-amount
    principalCV(estimate.relayerAddress),                                          // relayer — from estimateFee
    contractPrincipalCV('SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR', 'aeusdc'), // fee-token
  ],
  publicKey: userPublicKey,
});

const txForSigning = unsignedTx instanceof Uint8Array
  ? Buffer.from(unsignedTx).toString('hex')
  : unsignedTx;

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

const { txid } = await velumx.sponsor(signResult.transaction, {
  feeToken: estimate.feeToken,
  feeAmount: estimate.maxFee,
});

What the Paymaster Does NOT Do

  • No on-chain token whitelist — the relayer validates supported tokens off-chain
  • No fee cap enforcement — the relayer enforces caps per API key
  • No admin key or upgrade mechanism — the contract is immutable once deployed
  • No VelumX-specific registry or trait required
The paymaster is a simple, stateless contract. The relayer handles all policy.