Skip to main content
The x402 payment flow is the recommended approach for handling payments in OnchainDB. The SDK automatically manages the entire payment process.

How It Works

1. App calls store()
       |
2. SDK sends request to broker
       |
3. Broker returns 402 Payment Required
       |
4. SDK invokes your payment callback
       |
5. Your callback executes payment
       |
6. Callback returns { txHash, network }
       |
7. SDK retries with payment proof
       |
8. Broker verifies and stores data
       |
9. SDK returns confirmed result

Basic Implementation

const result = await client.store(
  {
    collection: 'posts',
    data: [{
      title: 'My First Post',
      content: 'Stored on blockchain!',
      author: 'alice'
    }]
  },
  // Payment callback - SDK invokes this when server returns 402
  async (quote) => {
    console.log('Payment required:', quote.totalCostTia, 'TIA');
    console.log('Pay to:', quote.brokerAddress);

    // Execute blockchain payment
    const txHash = await wallet.sendTokens(
      quote.brokerAddress,
      quote.totalCostTia
    );

    // Return format is required - SDK uses this to retry the store
    return {
      txHash: txHash,
      network: 'mocha-4' // or 'celestia' for mainnet
    };
  },
  true // waitForConfirmation
);

console.log(`Confirmed at block ${result.block_height}`);

X402Quote Structure

The quote object passed to your callback contains:
interface X402Quote {
  quoteId: string;           // Unique quote identifier
  totalCostTia: number;      // Total cost in TIA
  amountRaw: string;         // Amount in smallest units (utia)
  brokerAddress: string;     // Address to pay
  description: string;       // Payment description
  expiresAt: number;         // Unix timestamp for expiry
  chainType: 'cosmos' | 'evm' | 'solana';
  network: string;           // Network identifier
  asset: string;             // Asset identifier (e.g., "utia")
  tokenSymbol: string;       // Token symbol
  tokenDecimals: number;     // Decimal places
  paymentMethod: 'native' | 'x402-facilitator';
  facilitator?: string;      // Facilitator URL if applicable
  allOptions: X402PaymentRequirement[];
}

Callback Return Value

Your callback must return:
interface PaymentResult {
  txHash: string;    // Transaction hash from blockchain
  network: string;   // Network identifier ('mocha-4' or 'celestia')
}

Wait for Confirmation Options

With Confirmation (Blocking)

Waits for blockchain confirmation before returning:
const confirmed = await client.store(
  { collection: 'products', data: products },
  paymentCallback,
  true  // Waits for on-chain confirmation
);

console.log('Data confirmed at height:', confirmed.block_height);

Without Confirmation (Non-Blocking)

Returns immediately with a task ticket:
const quick = await client.store(
  { collection: 'products', data: products },
  paymentCallback,
  false  // Returns immediately
);

console.log('Task ticket:', quick.ticket_id);

// Check status later
const status = await client.getTaskStatus(quick.ticket_id);
console.log('Status:', status.status);

Keplr Wallet Example

import { SigningStargateClient } from '@cosmjs/stargate';

const paymentCallback = async (quote) => {
  // Connect to Keplr
  await window.keplr.enable('mocha-4');
  const signer = await window.keplr.getOfflineSignerOnlyAmino('mocha-4');
  const accounts = await signer.getAccounts();
  const userAddress = accounts[0].address;

  // Create signing client
  const client = await SigningStargateClient.connectWithSigner(
    'https://rpc-mocha.pops.one',
    signer
  );

  // Send payment
  const result = await client.sendTokens(
    userAddress,
    quote.brokerAddress,
    [{ denom: 'utia', amount: quote.amountRaw }],
    { amount: [{ denom: 'utia', amount: '20000' }], gas: '200000' }
  );

  return {
    txHash: result.transactionHash,
    network: 'mocha-4'
  };
};

// Use the callback
await client.store(
  { collection: 'data', data: [{ content: 'example' }] },
  paymentCallback,
  true
);

Server-Side Example

import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { SigningStargateClient } from '@cosmjs/stargate';

const paymentCallback = async (quote) => {
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(
    process.env.MNEMONIC,
    { prefix: 'celestia' }
  );
  const [account] = await wallet.getAccounts();

  const client = await SigningStargateClient.connectWithSigner(
    'https://rpc-mocha.pops.one',
    wallet
  );

  const result = await client.sendTokens(
    account.address,
    quote.brokerAddress,
    [{ denom: 'utia', amount: quote.amountRaw }],
    { amount: [{ denom: 'utia', amount: '20000' }], gas: '200000' }
  );

  return {
    txHash: result.transactionHash,
    network: 'mocha-4'
  };
};

Error Handling

import { PaymentVerificationError, OnchainDBError } from '@onchaindb/sdk';

try {
  await client.store(
    { collection: 'data', data: [{ content: 'example' }] },
    paymentCallback,
    true
  );
} catch (error) {
  if (error instanceof PaymentVerificationError) {
    console.log('Payment verification failed');
    console.log('Transaction hash:', error.txHash);
    console.log('Error:', error.message);
  } else if (error instanceof OnchainDBError) {
    console.log('OnchainDB error:', error.code, error.statusCode);
  }
}

Next Steps