Export as

Wallets Guide

Understanding invisible wallets in OFFER-HUB — automatic Stellar wallet creation and management.

OFFER-HUB uses "invisible wallets" to give users blockchain capabilities without the complexity of managing private keys. Every user gets a Stellar wallet automatically, but they interact with it like a simple account balance.

Tip

Invisible wallets only apply when using PAYMENT_PROVIDER=crypto. AirTM mode uses traditional fiat accounts instead.

What Are Invisible Wallets?

Traditional crypto requires users to:

  • Install wallet software
  • Secure a seed phrase
  • Understand gas fees
  • Sign transactions manually

OFFER-HUB eliminates all of this. Users see a simple USD balance while the system manages Stellar wallets behind the scenes.

How It Works

Mermaid
Rendering diagram…

Benefits

BenefitDescription
No wallet setupUsers register with email, wallet created automatically
No seed phrasesPrivate keys secured server-side
No gas managementPlatform handles Stellar transaction fees
Familiar UXUsers see USD, not crypto amounts
Full custodyUsers can still withdraw to external wallets

Wallet Creation

When a user is created, a Stellar keypair is generated automatically:

bash
curl -X POST http://localhost:4000/api/v1/users \
  -H "Authorization: Bearer ohk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "externalUserId": "your-user-123",
    "email": "user@example.com",
    "type": "BUYER"
  }'

Behind the scenes:

  1. Generate keypair — New Stellar Ed25519 keypair created
  2. Encrypt private key — AES-256-GCM with WALLET_ENCRYPTION_KEY
  3. Store in database — Encrypted key saved with user record
  4. Fund account — Platform funds the account with minimum XLM for trustlines
  5. Add USDC trustline — Account configured to hold USDC

Response

json
{
  "data": {
    "id": "usr_abc123def456",
    "externalUserId": "your-user-123",
    "email": "user@example.com",
    "type": "BUYER",
    "status": "ACTIVE",
    "created_at": "2026-02-25T12:00:00.000Z"
  }
}
Note

The Stellar address is not included in the user response. Retrieve it separately via the deposit endpoint.

Getting the Deposit Address

Retrieve the user's Stellar address for receiving USDC:

bash
curl http://localhost:4000/api/v1/users/usr_abc123def456/wallet/deposit \
  -H "Authorization: Bearer ohk_live_your_api_key"

Response

json
{
  "data": {
    "provider": "crypto",
    "method": "stellar_address",
    "address": "GCV24WNJYXPG3QFNP6ZQMLVEMHQX5S6J2OWKGVF5U3XC6HF4QQHG7WMD",
    "asset": {
      "code": "USDC",
      "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
    },
    "network": "testnet",
    "instructions": "Send USDC to this Stellar address. Deposits are detected automatically within seconds."
  }
}

Display to Users

Show users their deposit address with clear instructions:

typescript
📥 Deposit USDC

Send USDC to your personal Stellar address:

GCV24WNJYXPG3QFNP6ZQMLVEMHQX5S6J2OWKGVF5U3XC6HF4QQHG7WMD

Network: Stellar Testnet
Asset: USDC (Circle)

Deposits are credited automatically within seconds.

Checking Balance

Get the user's current balance:

bash
curl http://localhost:4000/api/v1/users/usr_abc123def456/balance \
  -H "Authorization: Bearer ohk_live_your_api_key"

Response

json
{
  "data": {
    "userId": "usr_abc123def456",
    "available": "100.00",
    "reserved": "0.00",
    "currency": "USD"
  }
}

Balance Types

FieldDescription
availableCan be used for orders or withdrawn
reservedLocked for pending orders (before escrow funding)

Balance States During Order

StageAvailableReserved
Initial deposit100.000.00
After order reserve50.0050.00
After escrow funding50.000.00
After release (seller)50.00 + payment0.00

Wallet Security

Encryption

Private keys are encrypted using AES-256-GCM:

typescript
WALLET_ENCRYPTION_KEY=64-character-hex-key

The encryption process:

  1. Generate random 12-byte IV
  2. Encrypt private key with AES-256-GCM
  3. Store: IV || ciphertext || authTag
  4. Decrypt only when signing transactions

Key Management

Danger

The WALLET_ENCRYPTION_KEY protects ALL user private keys. If lost, wallet access is permanently lost. Back up this key securely and never commit it to version control.

Best practices:

  1. Generate securely — Use cryptographically secure random bytes
  2. Store in vault — Use HashiCorp Vault, AWS KMS, or platform secrets
  3. Rotate periodically — Plan for key rotation (re-encrypt all wallets)
  4. Separate environments — Use different keys for dev/staging/production
  5. Backup offline — Store encrypted backup in secure physical location

Generating the Encryption Key

bash
# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# Using OpenSSL
openssl rand -hex 32

Transaction Signing

When the user performs an action that requires on-chain activity (funding escrow, withdrawing), the system:

  1. Load encrypted key — Retrieve from database
  2. Decrypt in memory — Use WALLET_ENCRYPTION_KEY
  3. Build transaction — Create Stellar transaction
  4. Sign transaction — Sign with decrypted private key
  5. Submit to network — Send to Stellar Horizon
  6. Clear memory — Wipe decrypted key from memory
Note

Decrypted private keys exist only in memory during transaction signing and are immediately cleared. They are never logged or written to disk.

Using the SDK

typescript
import { OfferHubSDK } from '@offerhub/sdk';

const sdk = new OfferHubSDK({
  apiUrl: 'http://localhost:4000',
  apiKey: 'ohk_live_your_api_key'
});

// Create a user (wallet created automatically)
const user = await sdk.users.create({
  externalUserId: 'my-user-123',
  email: 'user@example.com',
  type: 'BUYER'
});

// Get deposit address
const deposit = await sdk.wallet.getDepositAddress(user.id);
console.log('Send USDC to:', deposit.address);

// Check balance
const balance = await sdk.users.getBalance(user.id);
console.log('Available:', balance.available);
console.log('Reserved:', balance.reserved);

Listening for Balance Changes

typescript
const events = sdk.events.subscribe();

events.on('balance.credited', (data) => {
  console.log(`${data.userId} received ${data.amount} ${data.currency}`);
  // Update UI
  updateUserBalance(data.userId, data.newBalance);
});

events.on('balance.debited', (data) => {
  console.log(`${data.userId} spent ${data.amount} ${data.currency}`);
  // Update UI
  updateUserBalance(data.userId, data.newBalance);
});

On-Chain vs Off-Chain

OFFER-HUB maintains both on-chain (Stellar) and off-chain (database) balances:

On-Chain (Stellar)

  • Actual USDC in the Stellar wallet
  • Updated when deposits received or withdrawals sent
  • Source of truth for blockchain state
  • Can be verified on any Stellar explorer

Off-Chain (Database)

  • Fast read/write for operations
  • Tracks available vs reserved split
  • Used for reservations and internal transfers
  • Synced with on-chain balance

Reconciliation

The system automatically reconciles on-chain and off-chain balances:

typescript
Stellar Balance = Off-chain Available + Off-chain Reserved + In Escrow

If discrepancies are detected, the system:

  1. Logs the discrepancy
  2. Syncs off-chain to match on-chain
  3. Alerts operators for review

Testnet vs Mainnet

Testnet (Development)

env
STELLAR_NETWORK=testnet
STELLAR_USDC_ISSUER=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
  • Use Stellar testnet friendbot for XLM
  • Test USDC from Circle's testnet faucet
  • No real value at stake

Mainnet (Production)

env
STELLAR_NETWORK=mainnet
STELLAR_USDC_ISSUER=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN
  • Real USDC from Circle
  • Real XLM for transaction fees
  • Production security required
Warning

Always test thoroughly on testnet before deploying to mainnet. On-chain transactions are irreversible.

Platform Wallet

The platform also has a wallet used for:

  1. Funding new accounts — Provides minimum XLM for trustlines
  2. Collecting fees — Receives platform fees from orders
  3. Co-signing escrow — Signs release/refund transactions

Configure in environment:

env
PLATFORM_USER_ID=usr_platform

Ensure the platform wallet is funded with sufficient XLM for operations.

Error Handling

ErrorCauseSolution
WALLET_NOT_FOUNDUser doesn't have a walletEnsure user was created in crypto mode
INSUFFICIENT_BALANCENot enough USDCUser needs to deposit more
TRUSTLINE_MISSINGAccount can't hold USDCPlatform needs to add trustline
DECRYPTION_FAILEDInvalid encryption keyCheck WALLET_ENCRYPTION_KEY

Next Steps