1414 * - Ticket details
1515 */
1616import type { NextApiRequest , NextApiResponse } from 'next'
17- import { verifyPayment , verifyPaymentDirect , verifyPaymentNativeEth , getPaymentRecipient , usdToUsdcAmount , encodeSettlementResponseHeader } from 'services/x402'
17+ import { verifyPayment , verifyPaymentDirect , verifyPaymentNativeEth , getPaymentRecipient , usdToUsdcAmount , encodeSettlementResponseHeader , getPublicClientForChainId } from 'services/x402'
1818import { getUsdcConfig } from 'services/relayer'
1919import type { SettleResponse } from 'types/x402'
2020import { createOrder , confirmOrderPayment } from 'services/pretix'
@@ -52,6 +52,22 @@ interface VerifyRequest {
5252 symbol ?: string
5353 /** Token contract address. Required for multi-token chains (e.g. USDC vs USDT0 on Arbitrum). Falls back to USDC if omitted. */
5454 tokenAddress ?: string
55+ /**
56+ * EIP-191 signature proving the caller owns the `payer` wallet.
57+ * REQUIRED when `symbol === 'ETH'`. Prevents cross-order tx reuse attacks
58+ * on the native ETH path (USDC/USDT0 are already bound via EIP-3009).
59+ */
60+ ethPayerSignature ?: string
61+ }
62+
63+ /** Build the ETH-only message that the payer signs before their /verify call. */
64+ function buildEthPayerMessage ( paymentReference : string , payer : string , chainId : number ) : string {
65+ return (
66+ 'Devcon ticket payment (ETH)\n' +
67+ `Payment reference: ${ paymentReference } \n` +
68+ `Payer: ${ payer } \n` +
69+ `Chain: ${ chainId } `
70+ )
5571}
5672
5773interface VerifySuccessResponse {
@@ -197,6 +213,55 @@ export default async function handler(
197213 } )
198214 }
199215
216+ // For native ETH, require a signature over paymentReference+payer+chainId to
217+ // cryptographically prove the caller owns the `payer` wallet. USDC/USDT0 are
218+ // already bound via EIP-3009 at prepare-authorization time so no extra
219+ // signature is needed for those paths.
220+ if ( body . symbol === 'ETH' ) {
221+ if ( ! body . ethPayerSignature || typeof body . ethPayerSignature !== 'string' ) {
222+ return res . status ( 400 ) . json ( {
223+ success : false ,
224+ error : 'ethPayerSignature is required for native ETH payments' ,
225+ } )
226+ }
227+ if ( body . chainId == null ) {
228+ return res . status ( 400 ) . json ( {
229+ success : false ,
230+ error : 'chainId is required for native ETH payments' ,
231+ } )
232+ }
233+ const message = buildEthPayerMessage ( body . paymentReference , body . payer , body . chainId )
234+ // Use verifyMessage which handles BOTH EOA (ECDSA recovery) and smart
235+ // wallet (ERC-1271 isValidSignature) signatures. For contracts it calls
236+ // isValidSignature on the payer address and checks the 0x1626ba7e magic value.
237+ const client = getPublicClientForChainId ( body . chainId )
238+ if ( ! client ) {
239+ return res . status ( 400 ) . json ( {
240+ success : false ,
241+ error : `Unsupported chainId for signature verification: ${ body . chainId } ` ,
242+ } )
243+ }
244+ let sigValid = false
245+ try {
246+ sigValid = await client . verifyMessage ( {
247+ address : body . payer as `0x${string } `,
248+ message,
249+ signature : body . ethPayerSignature as `0x${string } `,
250+ } )
251+ } catch ( e ) {
252+ return res . status ( 400 ) . json ( {
253+ success : false ,
254+ error : `ethPayerSignature verification failed: ${ ( e as Error ) . message } ` ,
255+ } )
256+ }
257+ if ( ! sigValid ) {
258+ return res . status ( 403 ) . json ( {
259+ success : false ,
260+ error : 'ethPayerSignature does not match the payer address' ,
261+ } )
262+ }
263+ }
264+
200265 // Verify payment on-chain
201266 const expectedAmount = usdToUsdcAmount ( pendingOrder . totalUsd )
202267 const paymentProof : X402PaymentProof = {
@@ -211,12 +276,16 @@ export default async function handler(
211276 console . log ( '[Verify] Attempting primary verification via x402 service' , body . chainId != null ? `(chain ${ body . chainId } )` : '' )
212277 let verification = await verifyPayment ( paymentProof )
213278
214- // Only try native ETH when client indicates ETH payment; otherwise we'd wrongly run it for USDC and fail (from/to mismatch)
279+ // Only try native ETH when client indicates ETH payment; otherwise we'd wrongly run it for USDC and fail (from/to mismatch).
280+ // Error prefix match (not exact) because verifyPayment may append diagnostic detail to the message.
215281 if (
216282 ! verification . verified &&
217283 body . symbol === 'ETH' &&
218284 body . chainId != null &&
219- ( verification . error === 'No matching transfer found in transaction' || verification . error === 'Invalid payment reference' )
285+ (
286+ verification . error ?. startsWith ( 'No matching transfer found in transaction' ) ||
287+ verification . error === 'Invalid payment reference'
288+ )
220289 ) {
221290 const expectedAmountWei = pendingOrder . expectedEthAmountWeiByChain ?. [ String ( body . chainId ) ]
222291 if ( ! expectedAmountWei ) {
0 commit comments