Skip to content

Deposit Service

Deposit Service provides permanent payment addresses for recurring user payments. It's ideal for gaming wallets, platform balances, and any scenario where users need a long-term address to deposit funds multiple times.

What is Deposit Service?

Deposit Service binds a permanent payment address to each user. Once bound, the address monitors multiple blockchain networks automatically, allowing users to deposit from any supported chain within the protocol family.

Key Characteristics:

  • 🔐 One user, one address - Each user gets a permanent deposit address
  • 🌐 Multi-chain monitoring - Single address monitors entire protocol family
  • No expiration - Address remains bound indefinitely
  • ♻️ Recurring deposits - Support unlimited deposits to same address
  • 🏢 Multi-tenant - Complete organization isolation

Deposit vs Order

Understanding the difference between Deposit and Order services:

FeatureDeposit ServiceOrder Payment
AddressPermanent (bound to user)Temporary (recycled after use)
LifetimeLong-term (months/years)Minutes to hours
MonitoringMultiple chains (protocol family)Single chain specified at creation
Use CaseUser wallets, recurring top-upsE-commerce checkout, invoices
ExpirationNo expirationYes (payment window + grace period)
AmountAny amount, any timeSpecific amount expected

When to use Deposit Service:

  • Gaming wallet top-ups
  • User balance recharges
  • Platform wallet addresses
  • Membership recurring payments
  • Loyalty program deposits
  • SaaS subscription payments

Quick Start

Prerequisites

Before creating deposit references, ensure you have:

  1. PayIn Account - Registered at testnet.payin.com or app.payin.com
  2. API Key - Generated from Admin dashboard
  3. Address Pool - At least a few addresses imported (see Address Pool Setup)
  4. Supported Network - Choose protocol family: EVM or Tron

Check Address Pool First

If your address pool is empty, deposit binding will fail with "No available addresses in pool". Import addresses before proceeding.

Example 1: Bind Your First Deposit Address (MCP)

Using PayIn with Claude Desktop or Cline:

Create a deposit reference:
- Deposit Reference: user_12345
- Protocol: evm

The AI assistant will:

  1. Call the create_deposit_reference tool
  2. Bind a permanent address from your pool
  3. Start monitoring all EVM chains (Ethereum, Polygon, etc.)
  4. Return deposit address and monitoring details

Expected Response:

✅ Deposit address bound successfully!

Deposit Reference: user_12345
Protocol: evm
Deposit Address: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1

Monitoring:
- ethereum-sepolia: USDT, USDC
- polygon-amoy: USDT, USDC

Users can deposit any supported token from any of these chains.
The address will monitor all transactions automatically.

Example 2: Bind Deposit Address via API

Using cURL:

bash
curl -X POST https://testnet.payin.com/api/v1/deposits/bind \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "depositReference": "user_12345",
    "protocol": "evm"
  }'

Using TypeScript:

typescript
const response = await fetch('https://testnet.payin.com/api/v1/deposits/bind', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    depositReference: 'user_12345',
    protocol: 'evm'
  })
});

const result = await response.json();
console.log('Deposit address bound:', result.data);

Response:

json
{
  "success": true,
  "data": {
    "depositReference": "user_12345",
    "protocol": "evm",
    "depositAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
    "monitoringTargets": [
      {
        "chain": "ethereum-sepolia",
        "contract": "0x...",
        "token": "USDT"
      },
      {
        "chain": "ethereum-sepolia",
        "contract": "0x...",
        "token": "USDC"
      },
      {
        "chain": "polygon-amoy",
        "contract": "0x...",
        "token": "USDT"
      },
      {
        "chain": "polygon-amoy",
        "contract": "0x...",
        "token": "USDC"
      }
    ],
    "bindingCreatedAt": "2025-01-28T14:30:00Z"
  },
  "message": "Deposit address bound successfully"
}

Example 3: List User's Deposit History

Via API:

bash
curl "https://testnet.payin.com/api/v1/deposits/references?depositReference=user_12345" \
  -H "Authorization: Bearer YOUR_API_KEY"

Via MCP:

Show deposit history for user_12345

Response:

json
{
  "success": true,
  "data": {
    "depositReference": "user_12345",
    "protocol": "evm",
    "depositAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
    "totalDeposits": 5,
    "totalAmount": "127.50",
    "deposits": [
      {
        "id": "dep_123",
        "amount": "50.00",
        "currency": "USDT",
        "chain": "ethereum-sepolia",
        "status": "confirmed",
        "txHash": "0xabc...",
        "confirmedAt": "2025-01-28T10:15:00Z"
      },
      {
        "id": "dep_124",
        "amount": "77.50",
        "currency": "USDC",
        "chain": "polygon-amoy",
        "status": "confirmed",
        "txHash": "0xdef...",
        "confirmedAt": "2025-01-28T14:22:00Z"
      }
    ]
  }
}

Deposit Lifecycle

Complete Flow Diagram

1. Bind Address (to user ID)

2. Start Multi-Chain Monitoring (all protocol chains)

3. [User deposits from any chain]

4. Detect Transaction (on any monitored chain)

5. Create Transfer Record

6. Wait for Confirmations (chain-specific: 3-10 blocks)

7. Deposit Confirmed

8. Send Webhook Notification

9. Continue Monitoring (address stays bound)

Status Transitions

Deposits use a 3-state confirmation flow:

pending → confirmed → completed

Status Meanings:

  • pending: Transaction detected on blockchain, waiting for confirmations
  • confirmed: Required block confirmations reached
  • completed: Deposit processed and credited to user account

No Expiration

Unlike orders, deposits never expire. The address remains active indefinitely and can receive unlimited deposits.

Multi-Chain Monitoring

When you bind an address to a protocol, PayIn automatically monitors all chains in that protocol family:

EVM Protocol:

  • Ethereum (Mainnet + Sepolia)
  • Polygon (Mainnet + Amoy)
  • All supported tokens (USDT, USDC, DAI)

Tron Protocol:

  • Tron (Mainnet + Nile Testnet)
  • All supported tokens (USDT, USDC)

Solana Protocol:

  • Solana (Mainnet + Devnet)
  • All supported tokens (USDT, USDC)

Example Scenario:

typescript
// User binds EVM address
const binding = await payin.deposits.bind({
  depositReference: 'user_12345',
  protocol: 'evm'
});
// Returns address: 0x742d35...

// User can now deposit from:
// ✅ Ethereum Sepolia USDT
// ✅ Ethereum Sepolia USDC
// ✅ Polygon Amoy USDT
// ✅ Polygon Amoy USDC
// All to the SAME address: 0x742d35...

// PayIn monitors all chains automatically
// No need to create separate bindings

API Reference

Bind Deposit Address

Bind a permanent deposit address to a user.

Endpoint: POST /api/v1/deposits/bind

Required Fields:

FieldTypeDescription
depositReferencestringYour system's unique user identifier
protocolstringProtocol family: evm, tron, or solana

Optional Fields:

FieldTypeDescription
metadataobjectCustom data to attach to binding

Example Request:

json
{
  "depositReference": "user_12345",
  "protocol": "evm",
  "metadata": {
    "user_email": "player@example.com",
    "game_id": "game_xyz",
    "registration_date": "2025-01-15"
  }
}

Response:

json
{
  "success": true,
  "data": {
    "depositReference": "user_12345",
    "protocol": "evm",
    "depositAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
    "monitoringTargets": [
      {
        "chain": "ethereum-sepolia",
        "contract": "0x...",
        "token": "USDT"
      },
      {
        "chain": "polygon-amoy",
        "contract": "0x...",
        "token": "USDC"
      }
    ],
    "bindingCreatedAt": "2025-01-28T14:30:00Z"
  }
}

Idempotent Operation

If you call bind with an existing depositReference, PayIn returns the existing binding instead of creating a new one. This ensures each user has only one address per protocol.

Unbind Deposit Address

Unbind a deposit address from a user (stops monitoring).

Endpoint: POST /api/v1/deposits/unbind

Method 1: By Deposit Reference

Unbinds all addresses for this deposit reference.

json
{
  "depositReference": "user_12345"
}

Method 2: By Address and Protocol

Unbinds a specific address.

json
{
  "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
  "protocol": "evm"
}

Response:

json
{
  "success": true,
  "message": "Deposit address unbound successfully"
}

Unbinding Stops Monitoring

After unbinding, PayIn will no longer monitor the address. Any deposits sent to this address will not be detected or credited.

List Deposit References

List all deposit references with statistics.

Endpoint: GET /api/v1/deposits/references

Query Parameters:

ParameterTypeDescription
depositReferencestringFilter by specific deposit reference
protocolstringFilter by protocol: evm, tron, solana
pagenumberPage number (default: 1)
limitnumberItems per page (default: 20, max: 100)
searchstringSearch in deposit references

Example Request:

bash
curl "https://testnet.payin.com/api/v1/deposits/references?protocol=evm&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

json
{
  "success": true,
  "data": {
    "references": [
      {
        "depositReference": "user_12345",
        "protocol": "evm",
        "depositAddress": "0x742d35...",
        "totalDeposits": 5,
        "totalAmount": "127.50",
        "lastDepositAt": "2025-01-28T14:22:00Z",
        "boundAt": "2025-01-20T09:00:00Z"
      },
      {
        "depositReference": "user_67890",
        "protocol": "evm",
        "depositAddress": "0x9a8b7c...",
        "totalDeposits": 12,
        "totalAmount": "543.20",
        "lastDepositAt": "2025-01-28T16:45:00Z",
        "boundAt": "2025-01-18T14:30:00Z"
      }
    ],
    "total": 250,
    "page": 1,
    "limit": 10
  }
}

Get Deposit Details

Retrieve detailed deposit history for a specific reference.

Endpoint: GET /api/v1/deposits/references/:depositReference

Response:

json
{
  "success": true,
  "data": {
    "depositReference": "user_12345",
    "protocol": "evm",
    "depositAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
    "boundAt": "2025-01-20T09:00:00Z",
    "totalDeposits": 5,
    "totalAmount": "127.50",
    "deposits": [
      {
        "id": "dep_123",
        "amount": "50.00",
        "currency": "USDT",
        "chain": "ethereum-sepolia",
        "status": "confirmed",
        "txHash": "0xabc123...",
        "blockNumber": 1234567,
        "confirmations": 12,
        "requiredConfirmations": 3,
        "detectedAt": "2025-01-28T10:13:45Z",
        "confirmedAt": "2025-01-28T10:15:00Z"
      }
    ]
  }
}

Integration Examples

Gaming Wallet System

Complete integration example for a gaming platform:

typescript
import { PayInClient } from '@payin/sdk';

class GamingWalletService {
  private payin: PayInClient;

  constructor(apiKey: string) {
    this.payin = new PayInClient({
      apiKey,
      baseUrl: 'https://testnet.payin.com/api/v1',
    });
  }

  /**
   * Step 1: User registers - create deposit address
   */
  async onUserRegistration(userId: string, userEmail: string) {
    try {
      // Bind deposit address for new user
      const binding = await this.payin.deposits.bind({
        depositReference: `player_${userId}`,
        protocol: 'evm',
        metadata: {
          user_id: userId,
          email: userEmail,
          created_at: new Date().toISOString(),
          game_region: 'us-west',
        },
      });

      // Store deposit address in your database
      await this.db.users.update(userId, {
        depositAddress: binding.depositAddress,
        depositProtocol: binding.protocol,
        depositAddressBoundAt: binding.bindingCreatedAt,
      });

      // Show deposit address to user
      await this.sendWelcomeEmail(userEmail, binding.depositAddress);

      return binding;
    } catch (error) {
      console.error('Failed to create deposit address:', error);
      throw new Error('Wallet setup failed. Please try again.');
    }
  }

  /**
   * Step 2: User wants to deposit - show instructions
   */
  async getDepositInstructions(userId: string) {
    const user = await this.db.users.findById(userId);

    if (!user.depositAddress) {
      throw new Error('User has no deposit address. Please contact support.');
    }

    return {
      depositAddress: user.depositAddress,
      instructions: [
        'Send any amount of USDT or USDC to the address above',
        'Supported networks: Ethereum Sepolia, Polygon Amoy',
        'Your balance will be credited automatically after confirmation',
        'Minimum deposit: 1 USDT/USDC',
      ],
      supportedTokens: ['USDT', 'USDC'],
      supportedChains: ['ethereum-sepolia', 'polygon-amoy'],
      qrCode: this.generateQRCode(user.depositAddress),
    };
  }

  /**
   * Step 3: Handle deposit webhook notification
   */
  async handleDepositWebhook(event: PayInWebhookEvent) {
    // Verify webhook signature (important for security!)
    if (!this.payin.webhooks.verify(event)) {
      throw new Error('Invalid webhook signature');
    }

    if (event.type === 'deposit.confirmed') {
      const { depositReference, amount, currency, chain, txHash } = event.data;

      // Extract user ID from deposit reference
      const userId = depositReference.replace('player_', '');

      // Credit user's game balance
      await this.db.transactions.create({
        user_id: userId,
        type: 'deposit',
        amount: parseFloat(amount),
        currency,
        chain,
        tx_hash: txHash,
        status: 'completed',
        created_at: new Date(),
      });

      // Update user balance
      await this.db.users.incrementBalance(userId, parseFloat(amount));

      // Send confirmation notification
      const user = await this.db.users.findById(userId);
      await this.notificationService.send(userId, {
        title: 'Deposit Confirmed',
        message: `${amount} ${currency} has been added to your wallet`,
        type: 'success',
      });

      // Send email confirmation
      await this.emailService.sendDepositConfirmation(user.email, {
        amount,
        currency,
        chain,
        txHash,
        newBalance: user.balance + parseFloat(amount),
      });

      // Log for analytics
      await this.analytics.track('deposit_completed', {
        user_id: userId,
        amount,
        currency,
        chain,
      });

      console.log('Deposit processed successfully:', {
        userId,
        amount,
        currency,
        txHash,
      });
    }
  }

  /**
   * Step 4: User wants to withdraw - process withdrawal
   */
  async processWithdrawal(userId: string, amount: number, withdrawalAddress: string) {
    const user = await this.db.users.findById(userId);

    if (user.balance < amount) {
      throw new Error('Insufficient balance');
    }

    // Deduct from user balance
    await this.db.users.decrementBalance(userId, amount);

    // Create withdrawal record
    await this.db.transactions.create({
      user_id: userId,
      type: 'withdrawal',
      amount: -amount,
      currency: 'USDT',
      withdrawal_address: withdrawalAddress,
      status: 'pending',
    });

    // Process withdrawal through your payment processor
    // ... withdrawal logic ...
  }

  /**
   * Step 5: User account deletion - unbind address
   */
  async onUserDeletion(userId: string) {
    const user = await this.db.users.findById(userId);

    if (user.depositAddress) {
      // Unbind deposit address (stops monitoring)
      await this.payin.deposits.unbind({
        depositReference: `player_${userId}`,
      });

      console.log('Deposit address unbound for deleted user:', userId);
    }
  }

  private generateQRCode(address: string): string {
    // Generate QR code for deposit address
    // Implementation depends on your QR library
    return `data:image/png;base64,...`;
  }

  private async sendWelcomeEmail(email: string, depositAddress: string) {
    // Send welcome email with deposit instructions
    // ...
  }
}

SaaS Subscription Platform

Example for subscription-based services:

typescript
class SubscriptionPaymentService {
  async onSubscriptionCreated(customerId: string, plan: string) {
    // Bind deposit address for customer
    const binding = await fetch('https://testnet.payin.com/api/v1/deposits/bind', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.PAYIN_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        depositReference: `customer_${customerId}`,
        protocol: 'evm',
        metadata: {
          customer_id: customerId,
          subscription_plan: plan,
          created_at: new Date().toISOString(),
        },
      }),
    });

    const result = await binding.json();

    if (!result.success) {
      throw new Error(`Failed to create deposit address: ${result.message}`);
    }

    // Store in database
    await this.db.customers.update(customerId, {
      cryptoDepositAddress: result.data.depositAddress,
      depositProtocol: result.data.protocol,
    });

    // Send setup email with payment instructions
    await this.sendSubscriptionSetupEmail(customerId, result.data);

    return result.data;
  }

  async handleSubscriptionPayment(event: PayInWebhookEvent) {
    if (event.type === 'deposit.confirmed') {
      const { depositReference, amount, currency } = event.data;
      const customerId = depositReference.replace('customer_', '');

      // Credit customer's balance
      await this.db.customers.incrementBalance(customerId, parseFloat(amount));

      // Check if enough for subscription renewal
      const customer = await this.db.customers.findById(customerId);
      const subscriptionCost = this.getSubscriptionCost(customer.plan);

      if (customer.balance >= subscriptionCost) {
        // Auto-renew subscription
        await this.renewSubscription(customerId, subscriptionCost);

        // Deduct from balance
        await this.db.customers.decrementBalance(customerId, subscriptionCost);

        // Send renewal confirmation
        await this.emailService.sendRenewalConfirmation(customer.email, {
          plan: customer.plan,
          amount: subscriptionCost,
          nextRenewal: this.getNextRenewalDate(),
        });
      }
    }
  }

  private async sendSubscriptionSetupEmail(customerId: string, depositData: any) {
    const customer = await this.db.customers.findById(customerId);

    const emailContent = `
      Welcome to ${customer.plan} Plan!

      Your Crypto Payment Address:
      ${depositData.depositAddress}

      To activate your subscription:
      1. Send USDT or USDC to the address above
      2. Supported networks: Ethereum, Polygon
      3. Minimum: ${this.getSubscriptionCost(customer.plan)} USDT

      Your subscription will activate automatically after payment confirmation.
    `;

    await this.emailService.send(customer.email, emailContent);
  }

  private getSubscriptionCost(plan: string): number {
    const costs = {
      basic: 9.99,
      pro: 29.99,
      enterprise: 99.99,
    };
    return costs[plan] || 0;
  }

  private getNextRenewalDate(): Date {
    const next = new Date();
    next.setMonth(next.getMonth() + 1);
    return next;
  }

  private async renewSubscription(customerId: string, cost: number) {
    await this.db.subscriptions.create({
      customer_id: customerId,
      renewal_date: new Date(),
      next_renewal: this.getNextRenewalDate(),
      amount_paid: cost,
      payment_method: 'crypto',
    });
  }
}

Advanced Configuration

Deposit Reference Design

Use clear, structured deposit references:

Good Examples:

player_12345           // Gaming
customer_67890         // SaaS
user_alice_wallet      // Generic platform
member_premium_999     // Membership tier

Avoid:

12345                  // No context
temp_user              // Not permanent
deposit_1              // Generic
user                   // Not unique

Uniqueness is Critical

Deposit references must be unique within your organization. Each depositReference can only have one binding per protocol.

Multi-Currency Strategy

Single address can receive multiple tokens:

typescript
// User has ONE EVM address
const binding = await payin.deposits.bind({
  depositReference: 'user_12345',
  protocol: 'evm'
});
// Address: 0x742d35...

// This address can receive:
// ✅ USDT on Ethereum
// ✅ USDC on Ethereum
// ✅ USDT on Polygon
// ✅ USDC on Polygon
// All deposits credited to same user account

Benefits:

  • Simple user experience (one address to remember)
  • Unified deposit history
  • Automatic multi-chain support
  • No need to choose network beforehand

Minimum Deposit Amounts

Implement minimum deposit checks in your application:

typescript
async handleDepositWebhook(event: PayInWebhookEvent) {
  const { amount, currency } = event.data;
  const minDeposit = 1.0; // Minimum 1 USDT/USDC

  if (parseFloat(amount) < minDeposit) {
    // Handle below-minimum deposit
    await this.notifyUserBelowMinimum(event.data.depositReference, amount);

    // Option 1: Still credit it (user-friendly)
    await this.creditUserBalance(event.data);

    // Option 2: Hold until user deposits more
    await this.addToPendingBalance(event.data);

    return;
  }

  // Normal deposit processing
  await this.creditUserBalance(event.data);
}

Redirect URLs

Configure where users go after deposit confirmation:

typescript
// Organization-wide deposit redirect URL
// Set in Admin dashboard or via API

// When deposit confirmed, PayIn appends parameters:
// https://yourgame.com/wallet/success
//   ?deposit_reference=player_12345
//   &deposit_id=dep_123
//   &status=confirmed
//   &amount=50.00
//   &currency=USDT
//   &chain_id=ethereum-sepolia
//   &confirmed_at=2025-01-28T10:15:00Z
//   &tx_hash=0xabc123...

Configuration-Level Only

Unlike orders, deposits don't support per-transaction redirect URLs. Configure organization-wide redirect in Admin dashboard.

Best Practices

Address Binding

Do:

  • ✅ Bind address during user registration
  • ✅ Store deposit address in your database
  • ✅ Display address prominently in user's wallet page
  • ✅ Provide QR code for easy scanning
  • ✅ Show supported networks and tokens clearly

Don't:

  • ❌ Create multiple bindings for same user
  • ❌ Delete user's deposit address unless account deleted
  • ❌ Hide the deposit address from users
  • ❌ Forget to handle webhook notifications

Webhook Processing

Critical: Idempotency

typescript
async handleDepositWebhook(event: PayInWebhookEvent) {
  // MUST: Verify signature
  if (!verifySignature(event)) {
    throw new Error('Invalid signature');
  }

  // MUST: Check for duplicate processing
  const existing = await this.db.transactions.findByTxHash(event.data.txHash);
  if (existing) {
    console.log('Deposit already processed, skipping');
    return; // Idempotent - safe to process multiple times
  }

  // MUST: Use database transaction
  await this.db.transaction(async (trx) => {
    // Credit user balance
    await trx.users.incrementBalance(userId, amount);

    // Create transaction record
    await trx.transactions.create({
      tx_hash: event.data.txHash,
      user_id: userId,
      amount: amount,
      status: 'completed',
    });
  });

  // Return 200 quickly (process async if needed)
  return { success: true };
}

Balance Management

Atomic Operations:

typescript
// ❌ Bad: Race condition
const user = await db.users.findById(userId);
const newBalance = user.balance + depositAmount;
await db.users.update(userId, { balance: newBalance });

// ✅ Good: Atomic increment
await db.users.increment(userId, 'balance', depositAmount);

// ✅ Better: With transaction
await db.transaction(async (trx) => {
  await trx.users.increment(userId, 'balance', depositAmount);
  await trx.transactions.create({ /* ... */ });
});

Monitoring and Alerts

Set up monitoring for:

typescript
// 1. Webhook failures
if (webhookFailed) {
  await alerting.critical('Deposit webhook failed', {
    depositReference,
    txHash,
    error,
  });
}

// 2. Large deposits
if (parseFloat(amount) > 10000) {
  await alerting.info('Large deposit detected', {
    depositReference,
    amount,
    currency,
  });
}

// 3. Unusual activity
const recentDeposits = await getRecentDeposits(depositReference, '24h');
if (recentDeposits.length > 10) {
  await alerting.warning('Unusual deposit frequency', {
    depositReference,
    count: recentDeposits.length,
  });
}

Testing Strategy

1. Testnet Testing:

typescript
// Use testnet for all development
const payin = new PayInClient({
  apiKey: process.env.PAYIN_TESTNET_API_KEY,
  baseUrl: 'https://testnet.payin.com/api/v1',
});

// Test complete flow:
// 1. Bind address
// 2. Send testnet tokens
// 3. Verify webhook received
// 4. Check balance credited

2. Test Scenarios:

  • ✅ First deposit for new user
  • ✅ Multiple deposits to same address
  • ✅ Deposits from different chains
  • ✅ Deposits with different tokens
  • ✅ Below-minimum deposits
  • ✅ Large deposits (>$1000)
  • ✅ Webhook retry mechanism
  • ✅ Duplicate webhook handling

Troubleshooting

"No available addresses in pool"

Problem: Binding fails with address pool error.

Solution:

  1. Check address pool status in Admin dashboard
  2. Import more addresses following Address Pool Setup
  3. Ensure addresses for the correct protocol (EVM/Tron)
  4. Check if existing addresses are already bound (deposits don't auto-release)
bash
# Check pool status via API
curl https://testnet.payin.com/api/v1/address-pool/status \
  -H "Authorization: Bearer YOUR_API_KEY"

Deposit Not Detected

Problem: User sent deposit but webhook not received.

Causes:

  • Wrong network (e.g., sent ETH mainnet instead of Sepolia)
  • Wrong token (e.g., sent ETH instead of USDT)
  • Unsupported token
  • Transaction still pending

Solutions:

  1. Check transaction on block explorer
  2. Verify correct network and token
  3. Wait for required confirmations
  4. Check PayIn monitoring status

Duplicate Deposit Credits

Problem: User balance credited twice for same deposit.

Cause: Missing idempotency check in webhook handler.

Solution:

typescript
// Add idempotency check
async handleDeposit(event: PayInWebhookEvent) {
  const { txHash, depositReference } = event.data;

  // Check if already processed
  const existing = await this.db.transactions.findOne({
    tx_hash: txHash,
    deposit_reference: depositReference,
  });

  if (existing) {
    console.log('Deposit already processed:', txHash);
    return { success: true }; // Return success to acknowledge webhook
  }

  // Process deposit...
}

Wrong Chain Deposit

Problem: User deposited on unsupported chain.

Prevention:

  • Clearly list supported networks on deposit page
  • Show network names (not just IDs)
  • Provide network-specific instructions

If it happens:

  • Contact PayIn support with transaction hash
  • PayIn can help verify address ownership
  • Funds sent to wrong chain may not be recoverable

Next Steps

Essential Guides

Technical References

Support

Need help with Deposit Service?

Released under the MIT License.