Skip to content

Commit a74dbfc

Browse files
Christian Shearerclaude
andcommitted
Bind sender address to user account to prevent payment front-running (#100)
When a crypto payment is confirmed, check if the from_address has been seen before. If it has, associate the new payment with the same user. If a different email tries to claim payments from an already-bound address, reject with 403. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1caec7b commit a74dbfc

File tree

1 file changed

+23
-2
lines changed

1 file changed

+23
-2
lines changed

src/server/api-routes.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,30 @@ export function createApiRoutes(
271271
usd_value_cents: usdCents,
272272
});
273273

274-
// Find or create user
274+
// Find or create user — bind from_address to prevent front-running
275275
const contactEmail = (typeof email === "string" && email.includes("@")) ? email.trim().toLowerCase() : null;
276-
let user = contactEmail ? getUserByEmail(db, contactEmail) : undefined;
276+
let user: User | undefined;
277+
278+
// Check if this sender address already has an associated user
279+
if (verified.fromAddress) {
280+
const existingPayment = db.prepare(
281+
"SELECT user_id FROM crypto_payments WHERE from_address = ? AND user_id IS NOT NULL AND status = 'provisioned' LIMIT 1"
282+
).get(verified.fromAddress) as { user_id: number } | undefined;
283+
284+
if (existingPayment) {
285+
// This address already belongs to a user — use that account
286+
user = db.prepare("SELECT * FROM users WHERE id = ?").get(existingPayment.user_id) as User | undefined;
287+
if (user && contactEmail && user.email && user.email.toLowerCase() !== contactEmail) {
288+
// Different email trying to claim payments from an address that belongs to someone else
289+
apiError(res, 403, "ADDRESS_BOUND", "This sender address is already associated with a different account.");
290+
return;
291+
}
292+
}
293+
}
294+
295+
if (!user) {
296+
user = contactEmail ? getUserByEmail(db, contactEmail) : undefined;
297+
}
277298
if (!user) {
278299
user = createUser(db, contactEmail, null);
279300
}

0 commit comments

Comments
 (0)