Question: "What if user sends query directly to our backend API using Postman with random hash?"
# Attacker tries to fake a payment
curl -X POST https://yourapp.com/api/verify-payment \
-H "Content-Type: application/json" \
-d '{
"transactionHash": "0xFAKE123456",
"senderAddress": "0x1234...",
"credits": 999999
}'Require user to be logged in before calling this endpoint.
// In /api/verify-payment/route.ts
const session = await getServerSession();
if (!session) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}Result: Anonymous attackers blocked ✅
Verify the sender address belongs to the logged-in user.
const userWallet = await db.query(
'SELECT wallet_address FROM users WHERE id = $1',
[session.user.id]
);
if (senderAddress.toLowerCase() !== userWallet.toLowerCase()) {
return NextResponse.json(
{ success: false, error: 'Wallet mismatch' },
{ status: 403 }
);
}Result: Attacker can't claim credits for someone else's wallet ✅
Prevent same transaction hash from being used twice (replay attack).
// Check if already processed
const existing = await supabase
.from('payments')
.select('id')
.eq('transaction_hash', transactionHash)
.single();
if (existing.data) {
return NextResponse.json(
{ success: false, error: 'Transaction already processed' },
{ status: 400 }
);
}
// After confirming, store it
await supabase.from('payments').insert({
transaction_hash: transactionHash,
user_id: session.user.id,
wallet_address: senderAddress,
amount_usdc: amount,
credits: credits,
status: 'confirmed',
created_at: new Date()
});Result: Same transaction can't be reused ✅
Verify transaction on Mirror Node asynchronously.
// Don't block the response, verify in background
setTimeout(() => {
verifyOnMirrorNode(transactionHash)
.then(result => {
if (result.verified) {
// Update DB: status = 'verified'
} else {
// 🚨 FRAUD ALERT!
// Update DB: status = 'fraud_suspected'
// Send alert to admin
// Freeze user credits
}
});
}, 10000); // Wait 10s for indexingResult: Fake transactions flagged for review ✅
- ✅ Authentication check
- ✅ Wallet ownership verification
- ✅ Transaction deduplication (store in DB)
- Background Mirror Node verification
- Fraud alert system
- Rate limiting (max 10 purchases per hour)
- Amount limits (max $100 per transaction)
- Admin dashboard for reviewing flagged transactions
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id),
wallet_address VARCHAR(42) NOT NULL,
transaction_hash VARCHAR(66) UNIQUE NOT NULL, -- Prevents duplicates
amount_usdc DECIMAL(10, 2) NOT NULL,
credits INTEGER NOT NULL,
status VARCHAR(20) DEFAULT 'confirmed', -- confirmed, verified, fraud_suspected
verified_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_user_id (user_id),
INDEX idx_transaction_hash (transaction_hash),
INDEX idx_status (status)
);# First call: Success
# Second call: "Transaction already processed" ✅# User A's wallet: 0xAAA...
# Attacker tries: senderAddress: 0xAAA, but logged in as User B
# Result: "Wallet mismatch" ✅# No session cookie
# Result: "Unauthorized" ✅Even if attacker sends fake transaction hash:
- ❌ No auth → Blocked at Layer 1
- ❌ Wrong wallet → Blocked at Layer 2
- ❌ Reused hash → Blocked at Layer 3
- ❌ Fake hash → Flagged at Layer 4 (background check)
All 4 layers must be bypassed = Nearly impossible!
- Implement Layer 1-3 now (authentication + deduplication)
- Add background verification (flags fraud without blocking UX)
- Monitor logs for fraud attempts
- Review flagged transactions weekly
This gives you enterprise-grade security without cron jobs or complex infrastructure!