This guide provides step-by-step instructions for platform operators managing the CrowdPay campaign wallet infrastructure. Follow these procedures for setup, monitoring, and maintenance.
- Node.js 18+ installed
- PostgreSQL 14+ running
- Access to Stellar testnet/mainnet
- Environment variables configured
- Platform operator credentials
Generate a secure 256-bit encryption key for wallet secrets:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Add to .env:
WALLET_ENCRYPTION_KEY=<generated_key>
The platform account funds new campaign wallets and acts as a multisig co-signer.
On Testnet:
cd contracts/stellar
node campaignWallet.js --setup-platformThis will:
- Generate a new Stellar keypair
- Fund it via Friendbot (10,000 XLM)
- Display public and secret keys
On Mainnet:
- Generate keypair manually or use existing account
- Fund with sufficient XLM (recommend 1000+ XLM for reserves)
- Verify account is active on Stellar Expert
Add to .env:
PLATFORM_PUBLIC_KEY=GXXX...
PLATFORM_SECRET_KEY=SXXX...
Run migrations to add wallet secret storage:
cd backend
psql $DATABASE_URL -f db/schema.sql
psql $DATABASE_URL -f db/migrations/001_add_campaign_wallet_secrets.sqlVerify schema:
psql $DATABASE_URL -c "\d campaigns"Should show wallet_secret_encrypted column.
cd backend
npm install
npm run devVerify services:
- Backend API: http://localhost:3001/health
- Ledger monitor: Check console for "Monitoring ledger..." messages
Check logs for successful wallet creation:
tail -f backend/logs/app.log | grep "Campaign wallet created"Verify on Stellar:
node contracts/stellar/campaignWallet.js --inspect <wallet_public_key>Expected output:
- Balance: ~2 XLM (base reserve)
- Signers: 2 (creator + platform)
- Thresholds: low=1, med=2, high=2
- Trustlines: USDC established
View incoming contributions in real-time:
# Database query
psql $DATABASE_URL -c "
SELECT c.title, co.amount, co.asset, co.created_at
FROM contributions co
JOIN campaigns c ON c.id = co.campaign_id
ORDER BY co.created_at DESC
LIMIT 10;
"Check ledger monitor status:
curl http://localhost:3001/api/monitor/statusList pending withdrawals:
psql $DATABASE_URL -c "
SELECT id, campaign_id, amount, destination_key, creator_signed, platform_signed
FROM withdrawal_requests
WHERE status = 'pending';
"Review and approve withdrawal:
-
Verify Request:
- Check campaign has sufficient balance
- Verify destination address is valid
- Confirm creator has signed
-
Platform Sign:
curl -X POST http://localhost:3001/api/withdrawals/:id/platform-sign \
-H "Authorization: Bearer $PLATFORM_ADMIN_TOKEN"- Verify Submission:
# Check transaction on Stellar
curl "https://horizon.stellar.org/transactions/<tx_hash>"node contracts/stellar/campaignWallet.js --inspect $PLATFORM_PUBLIC_KEYEnsure sufficient XLM for:
- Creating new campaign accounts (2 XLM each)
- Transaction fees (0.00001 XLM per operation)
Recommended minimum: 100 XLM
Verify encrypted secrets are intact:
psql $DATABASE_URL -c "
SELECT id, title,
CASE WHEN wallet_secret_encrypted IS NOT NULL THEN 'ENCRYPTED' ELSE 'MISSING' END as secret_status
FROM campaigns
WHERE created_at > NOW() - INTERVAL '30 days';
"All campaigns should show ENCRYPTED.
Backup database including encrypted secrets:
pg_dump $DATABASE_URL | gzip > backup_$(date +%Y%m%d).sql.gzStore backups:
- Encrypted at rest
- Geographically distributed
- Retained for 90 days minimum
Rotate encryption key for enhanced security:
- Generate New Key:
NEW_KEY=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
echo $NEW_KEY- Re-encrypt Secrets (run migration script):
node scripts/rotate-encryption-key.js --old-key $OLD_KEY --new-key $NEW_KEY- Update Environment:
WALLET_ENCRYPTION_KEY=$NEW_KEY
- Restart Services:
pm2 restart crowdpay-backendSymptoms: API returns 500 error, logs show "Account not found"
Diagnosis:
# Check platform account exists
curl "https://horizon.stellar.org/accounts/$PLATFORM_PUBLIC_KEY"
# Check platform account balance
node contracts/stellar/campaignWallet.js --inspect $PLATFORM_PUBLIC_KEYResolution:
- If account not found: Re-run platform setup
- If balance too low: Fund platform account
- If secret key wrong: Update
.envwith correct key
Symptoms: Recovery endpoint returns 500 error, logs show decryption failure
Diagnosis:
# Verify encryption key in environment
echo $WALLET_ENCRYPTION_KEY | wc -c # Should be 65 (64 hex chars + newline)
# Test encryption/decryption
node -e "
const {encryptSecret, decryptSecret} = require('./backend/src/services/walletService');
const test = 'SXXX...';
const enc = encryptSecret(test);
const dec = decryptSecret(enc);
console.log(dec === test ? 'OK' : 'FAIL');
"Resolution:
- If key wrong: Restore from backup
- If key lost: Secrets cannot be recovered (wallets still accessible via multisig)
Symptoms: Contributions visible on Stellar but not in database
Diagnosis:
# Check monitor is running
curl http://localhost:3001/api/monitor/status
# Check last processed ledger
psql $DATABASE_URL -c "SELECT * FROM ledger_cursor;"
# Manually check for transactions
curl "https://horizon.stellar.org/accounts/<campaign_wallet>/transactions?limit=10"Resolution:
# Restart ledger monitor
pm2 restart crowdpay-backend
# If cursor stuck, reset to recent ledger
psql $DATABASE_URL -c "UPDATE ledger_cursor SET last_ledger = <recent_ledger>;"Symptoms: Withdrawal request has both signatures but not submitted
Diagnosis:
# Check withdrawal status
psql $DATABASE_URL -c "
SELECT id, creator_signed, platform_signed, status, tx_hash
FROM withdrawal_requests
WHERE id = '<withdrawal_id>';
"
# Verify XDR has both signatures
node -e "
const {Transaction} = require('@stellar/stellar-sdk');
const xdr = '<unsigned_xdr>';
const tx = new Transaction(xdr, 'TESTNET');
console.log('Signatures:', tx.signatures.length);
"Resolution:
# Manually submit transaction
curl -X POST http://localhost:3001/api/withdrawals/:id/submit \
-H "Authorization: Bearer $PLATFORM_ADMIN_TOKEN"Operator Access Levels:
- Level 1 (Read-Only): View campaigns, contributions, balances
- Level 2 (Operator): Approve withdrawals, monitor systems
- Level 3 (Admin): Access encrypted secrets, rotate keys
Authentication:
# Generate operator token
curl -X POST http://localhost:3001/api/auth/operator-login \
-H "Content-Type: application/json" \
-d '{"username":"operator","password":"<secure_password>"}'If Platform Secret Key Compromised:
-
Immediate Actions:
- Rotate platform keypair
- Update all campaign wallet signers
- Notify affected campaign creators
-
Recovery Steps:
# Generate new platform keypair
node contracts/stellar/campaignWallet.js --setup-platform
# For each campaign, update signers (requires creator cooperation)
# This is a manual process requiring coordinationIf Encryption Key Compromised:
-
Immediate Actions:
- Generate new encryption key
- Re-encrypt all wallet secrets
- Audit access logs for unauthorized recovery attempts
-
Recovery Steps:
# Run key rotation script
node scripts/rotate-encryption-key.js --emergencyEnable comprehensive audit logs:
# Log all wallet operations
export AUDIT_LOG_LEVEL=verbose
# Review audit logs
tail -f backend/logs/audit.log | grep "wallet_operation"Log entries should include:
- Timestamp
- Operator ID
- Operation type (create, recover, withdraw)
- Campaign ID
- Result (success/failure)
-
Platform Account Balance:
- Alert if < 50 XLM
- Critical if < 10 XLM
-
Campaign Wallet Creation Rate:
- Track daily creation count
- Alert on unusual spikes
-
Withdrawal Processing Time:
- Target: < 1 hour from request to submission
- Alert if > 24 hours
-
Ledger Monitor Lag:
- Target: < 5 ledgers behind current
- Alert if > 100 ledgers behind
Using Prometheus + Grafana:
# prometheus.yml
scrape_configs:
- job_name: 'crowdpay'
static_configs:
- targets: ['localhost:3001']
metrics_path: '/metrics'Using CloudWatch (AWS):
# Install CloudWatch agent
aws cloudwatch put-metric-data \
--namespace CrowdPay \
--metric-name PlatformBalance \
--value $(curl -s http://localhost:3001/api/platform/balance | jq .XLM)Immediate Action:
# Fund platform account
# Testnet: Use Friendbot
curl "https://friendbot.stellar.org?addr=$PLATFORM_PUBLIC_KEY"
# Mainnet: Transfer from reserve account
# (requires manual intervention)Recovery Steps:
- Stop backend services
- Restore from latest backup
- Verify encryption key matches backup
- Test wallet recovery on sample campaign
- Restart services
- Reconcile on-chain state with database
Monitoring:
# Check Horizon status
curl https://horizon.stellar.org/
# Check Stellar status page
curl https://status.stellar.org/api/v2/status.jsonActions:
- Queue withdrawal requests for later processing
- Notify users of temporary service disruption
- Monitor Stellar status page for updates
- Never expose private keys in logs or error messages
- Always verify transaction XDRs before signing
- Maintain offline backups of encryption keys
- Test recovery procedures quarterly
- Document all manual interventions
- Use separate accounts for testnet and mainnet
- Implement rate limiting on sensitive endpoints
- Review withdrawal requests before platform signing
- Monitor for unusual activity patterns
- Keep Stellar SDK and dependencies updated
- Stellar Network Issues: https://stellar.stackexchange.com/
- SDK Issues: https://github.com/stellar/js-stellar-sdk/issues
- Platform Issues: [Internal support channel]
# Check campaign wallet details
psql $DATABASE_URL -c "SELECT id, title, wallet_public_key, status FROM campaigns WHERE id = '<campaign_id>';"
# View recent contributions
psql $DATABASE_URL -c "SELECT * FROM contributions ORDER BY created_at DESC LIMIT 10;"
# Check pending withdrawals
psql $DATABASE_URL -c "SELECT * FROM withdrawal_requests WHERE status = 'pending';"
# Verify encryption key
echo $WALLET_ENCRYPTION_KEY | wc -c
# Test Stellar connection
curl https://horizon.stellar.org/
# Check backend health
curl http://localhost:3001/health# Stellar Configuration
STELLAR_NETWORK=testnet|mainnet
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
PLATFORM_PUBLIC_KEY=GXXX...
PLATFORM_SECRET_KEY=SXXX...
USDC_ISSUER=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/crowdpay
# Security
JWT_SECRET=<random_string>
WALLET_ENCRYPTION_KEY=<64_hex_chars>
# Server
PORT=3001
NODE_ENV=production- Generate new mainnet platform account
- Fund platform account with sufficient XLM
- Generate new production encryption key
- Update environment variables
- Deploy database schema
- Configure monitoring and alerts
- Test wallet creation on mainnet
- Test contribution flow
- Test withdrawal flow
- Document mainnet-specific procedures
- Train operators on mainnet procedures
Document Version: 1.0
Last Updated: 2026-04-24
Next Review: 2026-07-24