Payment Confirmation System Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Payment Confirmation System Development
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1217
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1046
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Development of Payment Confirmation System

Naive implementation of crypto payment verification looks like this: received transaction → checked amount → credited. This scheme breaks at the first reorg, double-spend, or when a user sends payment an hour after session expiration. A reliable confirmation system is a finite state machine with explicit transitions between states and protection against all edge cases.

Payment State Model

Each payment goes through strictly defined states:

PENDING → DETECTED → CONFIRMING → CONFIRMED → SETTLED
              ↓                     ↓
          EXPIRED              UNDERPAID / OVERPAID
                                    ↓
                                REFUNDED
State Description
PENDING Address issued, waiting for transaction
DETECTED Transaction in mempool (0 confirmations)
CONFIRMING 1+ confirmations, not yet final
CONFIRMED Confirmations threshold reached, amount correct
SETTLED Business logic executed (order created, subscription activated)
EXPIRED Timer expired, transaction not received
UNDERPAID Transaction received, but amount is less than expected

Database schema for payment:

CREATE TABLE payments (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id      UUID NOT NULL REFERENCES orders(id),
  address       VARCHAR(64) NOT NULL UNIQUE,
  network       VARCHAR(32) NOT NULL,  -- 'ethereum', 'bsc', 'tron'
  token         VARCHAR(32) NOT NULL,  -- 'USDT', 'ETH', 'TRX'
  expected_amount NUMERIC(36, 18) NOT NULL,
  received_amount NUMERIC(36, 18) DEFAULT 0,
  status        VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  tx_hash       VARCHAR(128),
  confirmations INTEGER DEFAULT 0,
  required_confirmations INTEGER NOT NULL,
  expires_at    TIMESTAMPTZ NOT NULL,
  confirmed_at  TIMESTAMPTZ,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),

  CONSTRAINT valid_status CHECK (
    status IN ('PENDING','DETECTED','CONFIRMING','CONFIRMED','SETTLED','EXPIRED','UNDERPAID','OVERPAID','REFUNDED')
  )
);

CREATE INDEX idx_payments_address ON payments(address) WHERE status IN ('PENDING', 'DETECTED', 'CONFIRMING');
CREATE INDEX idx_payments_expires_at ON payments(expires_at) WHERE status = 'PENDING';

Blockchain Monitoring: Architecture

Monolithic monitoring of all networks in one process is a bad idea. Better: separate worker for each network with independent retry mechanism:

// Abstract monitor interface
interface ChainMonitor {
  network: string;
  start(): Promise<void>;
  stop(): void;
  onTransaction(handler: (tx: IncomingTransaction) => Promise<void>): void;
}

// Implementation for EVM networks
class EvmChainMonitor implements ChainMonitor {
  private provider: ethers.JsonRpcProvider;
  private watchedAddresses = new Set<string>();

  async start() {
    // Load active addresses from DB
    const activePayments = await db.query(
      "SELECT address FROM payments WHERE status IN ('PENDING', 'DETECTING', 'CONFIRMING')"
    );
    activePayments.rows.forEach(p => this.watchedAddresses.add(p.address));

    // Subscribe to new blocks
    this.provider.on('block', async (blockNumber) => {
      await this.processBlock(blockNumber);
    });
  }

  private async processBlock(blockNumber: number) {
    const block = await this.provider.getBlock(blockNumber, true);

    for (const tx of block.transactions) {
      // Native ETH
      if (tx.to && this.watchedAddresses.has(tx.to.toLowerCase())) {
        await this.handleNativeTransfer(tx, blockNumber);
      }
    }

    // ERC-20 Transfer events
    await this.scanErc20Transfers(blockNumber);
  }
}

Handling Reorgs

Reorg — chain reorganization, when an accepted block is replaced by another. On Ethereum, probability is low (PoS, 1–2 blocks), on Polygon — higher. Correct protection:

async function processConfirmations(paymentId: string) {
  const payment = await db.findPayment(paymentId);
  const currentBlock = await provider.getBlockNumber();

  // Get fresh receipt
  const receipt = await provider.getTransactionReceipt(payment.txHash);

  if (!receipt) {
    // Transaction disappeared from chain — reorg or dropped
    await db.updatePayment(paymentId, {
      status: 'DETECTED',  // revert to previous state
      confirmations: 0,
      reorgDetected: true,
    });
    return;
  }

  const confirmations = currentBlock - receipt.blockNumber + 1;
  const isConfirmed = confirmations >= payment.requiredConfirmations;

  await db.updatePayment(paymentId, {
    confirmations,
    status: isConfirmed ? 'CONFIRMED' : 'CONFIRMING',
    confirmedAt: isConfirmed ? new Date() : null,
  });
}

Check receipt again every N blocks for CONFIRMING payments — don't trust old data.

Tolerance Window and Exchange Rate Error

User pays "47.50 USDT", but after wei conversion and back may result in 47.499999... due to floating point. Plus: exchanges often take fees from the transfer amount. A reasonable tolerance window:

function isAmountSufficient(
  received: bigint,    // in minimal units (wei, sun)
  expected: bigint,
  toleranceBps: number = 50  // 0.5% by default
): 'exact' | 'underpaid' | 'overpaid' {
  const tolerance = expected * BigInt(toleranceBps) / 10000n;
  const min = expected - tolerance;
  const max = expected + expected / 10n;  // overpaid — up to 10%

  if (received >= min && received <= max) return 'exact';
  if (received < min) return 'underpaid';
  return 'overpaid';
}

Idempotency and Duplicate Protection

One txHash should be credited exactly once:

INSERT INTO payment_transactions (payment_id, tx_hash, amount, block_number)
VALUES ($1, $2, $3, $4)
ON CONFLICT (tx_hash) DO NOTHING
RETURNING id;

If RETURNING returned empty result — transaction already processed, skip.

Notifications and Webhooks

After transition to CONFIRMED — immediate notification to system:

async function dispatchPaymentConfirmed(payment: Payment) {
  // Internal event bus
  await eventBus.emit('payment.confirmed', {
    paymentId: payment.id,
    orderId: payment.orderId,
    amount: payment.receivedAmount,
    txHash: payment.txHash,
  });

  // External webhook if configured
  if (payment.webhookUrl) {
    await webhookQueue.add('payment-webhook', {
      url: payment.webhookUrl,
      payload: { event: 'payment.confirmed', data: payment },
    }, {
      attempts: 5,
      backoff: { type: 'exponential', delay: 2000 },
    });
  }
}

Webhooks — via queue (Bull/BullMQ) with retry. Direct HTTP call in block handler — this is the path to losing events when recipient is temporarily unavailable.