From 7a91936d32f93045ee770515c76a6030b65b40fd Mon Sep 17 00:00:00 2001 From: youssefea Date: Thu, 15 Jan 2026 18:47:32 +0100 Subject: [PATCH 1/3] update backend section for base pay --- docs/base-account/guides/accept-payments.mdx | 157 ++++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/docs/base-account/guides/accept-payments.mdx b/docs/base-account/guides/accept-payments.mdx index 6c6d7467a..3be33b093 100644 --- a/docs/base-account/guides/accept-payments.mdx +++ b/docs/base-account/guides/accept-payments.mdx @@ -122,12 +122,18 @@ You can use the `callbackURL` to validate the user's information on the server s Learn more about this in the [callbackURL reference](/base-account/reference/core/capabilities/datacallback). -## Polling example +## Server side + +When accepting payments, your backend must validate transactions and user info received from the frontend. This section covers two critical aspects: verifying transaction completion and validating user information. + +### Verify user transaction + +Use `getPaymentStatus()` on your backend to confirm that a payment has been completed before fulfilling orders. Never trust payment confirmations from the frontend alone. ```ts Backend (SDK) import { getPaymentStatus } from '@base-org/account'; -export async function checkPayment(txId, testnet = false) { +export async function checkPayment(txId: string, testnet = false) { const status = await getPaymentStatus({ id: txId, testnet // Must match the testnet setting from the original pay() call @@ -138,6 +144,153 @@ export async function checkPayment(txId, testnet = false) { } ``` + +**Prevent Replay Attacks** + +A malicious user could submit the same valid transaction ID multiple times to receive goods or services repeatedly. Always track processed transaction IDs in your database. + + +Here's an example that prevents replay attacks by storing processed transactions: + +```ts Backend (with replay protection) expandable +import { getPaymentStatus } from '@base-org/account'; + +// Example using a database to track processed transactions +// Replace with your actual database implementation (PostgreSQL, MongoDB, etc.) +const processedTransactions = new Map(); // In production, use a persistent database + +export async function verifyAndFulfillPayment( + txId: string, + orderId: string, + testnet = false +) { + // 1. Check if this transaction was already processed + if (processedTransactions.has(txId)) { + throw new Error('Transaction already processed'); + } + + // 2. Verify the payment status on-chain + const { status, sender, amount, recipient } = await getPaymentStatus({ + id: txId, + testnet + }); + + if (status !== 'completed') { + throw new Error(`Payment not completed. Status: ${status}`); + } + + // 3. Validate the payment details match your order + // This ensures the user paid the correct amount to the correct address + const expectedAmount = await getOrderAmount(orderId); + const expectedRecipient = process.env.PAYMENT_ADDRESS; + + if (amount !== expectedAmount) { + throw new Error('Payment amount mismatch'); + } + + if (recipient.toLowerCase() !== expectedRecipient.toLowerCase()) { + throw new Error('Payment recipient mismatch'); + } + + // 4. Mark transaction as processed BEFORE fulfilling + // Store sender for easy lookup (e.g., to query all payments from a user) + // In production, use a database transaction to ensure atomicity + processedTransactions.set(txId, { + orderId, + sender, + amount, + timestamp: new Date() + }); + + // 5. Fulfill the order + await fulfillOrder(orderId); + + return { success: true, orderId, sender }; +} +``` + + +**Database recommendations for tracking transactions:** + +- Store the transaction ID, order ID, sender address, amount, timestamp, and fulfillment status +- Use a unique constraint on the transaction ID to prevent duplicates +- Consider adding an index on the transaction ID for fast lookups + + +### Validate user info with dataCallback + +If you're collecting user information (email, phone, shipping address) during checkout, use the `callbackURL` parameter to validate this data server-side before the transaction is submitted. + +When you provide a `callbackURL` in your `payerInfo` configuration, the wallet will POST the user's information to your endpoint for validation. This gives you the opportunity to: + +- Verify email addresses are valid and not disposable +- Validate shipping addresses against your supported regions +- Check phone numbers are correctly formatted +- Block suspicious or fraudulent information + +```ts Client-side setup +const payment = await pay({ + amount: '25.00', + to: '0xRecipient', + payerInfo: { + requests: [ + { type: 'email' }, + { type: 'physicalAddress' } + ], + callbackURL: 'https://your-api.com/validate-payment-info' + } +}); +``` + +Your callback endpoint receives the user's information and must respond with either a success or error response: + +```ts Backend (validation endpoint) +export async function POST(request: Request) { + const requestData = await request.json(); + const { requestedInfo } = requestData.capabilities.dataCallback; + const errors: Record = {}; + + // Validate email + if (requestedInfo.email) { + const blockedDomains = ['tempmail.com', 'throwaway.com']; + const domain = requestedInfo.email.split('@')[1]; + if (blockedDomains.includes(domain)) { + errors.email = 'Please use a valid email address'; + } + } + + // Validate shipping address + if (requestedInfo.physicalAddress) { + const addr = requestedInfo.physicalAddress; + const supportedCountries = ['US', 'CA', 'GB']; + if (!supportedCountries.includes(addr.countryCode)) { + errors.physicalAddress = { + countryCode: 'We currently only ship to US, Canada, and UK' + }; + } + } + + // Return errors if validation failed + if (Object.keys(errors).length > 0) { + return Response.json({ errors }); + } + + // Success - return the request to proceed with the transaction + return Response.json({ request: requestData }); +} +``` + + +The callback is invoked **before** the transaction is submitted. If you return errors, the user is prompted to correct their information. If you return success, the transaction proceeds. + + +For complete details on the callback request/response format and all supported data types, see the [dataCallback reference](/base-account/reference/core/capabilities/datacallback). + ## Add the Base Pay Button Use the pre-built component for a native look-and-feel: From d3108fc4811fa89921428c135c38f2bdaaea373a Mon Sep 17 00:00:00 2001 From: youssefea Date: Thu, 15 Jan 2026 19:15:16 +0100 Subject: [PATCH 2/3] add backend example --- docs/base-account/guides/accept-payments.mdx | 33 ++++---------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/docs/base-account/guides/accept-payments.mdx b/docs/base-account/guides/accept-payments.mdx index 3be33b093..96004b8c0 100644 --- a/docs/base-account/guides/accept-payments.mdx +++ b/docs/base-account/guides/accept-payments.mdx @@ -25,7 +25,7 @@ If you intend on using the BasePayButton, please follow the [Brand Guidelines](/ ## Client-side (Browser SDK) -**Interactive Playground:** Try out the `pay()` and `getPaymentStatus()` functions in our [Base Pay SDK Playground](https://base.github.io/account-sdk/pay-playground) before integrating them into your app. +**Interactive Playground:** Try out the [`pay()`](/base-account/reference/base-pay/pay) and [`getPaymentStatus()`](/base-account/reference/base-pay/getPaymentStatus) functions in our [Base Pay SDK Playground](https://base.github.io/account-sdk/pay-playground) before integrating them into your app. ```ts Browser (SDK) @@ -53,7 +53,7 @@ try { ``` -**Important:** The `testnet` parameter in `getPaymentStatus()` must match the value used in the original `pay()` call. If you initiated a payment on testnet with `testnet: true`, you must also pass `testnet: true` when checking its status. +**Important:** The `testnet` parameter in [`getPaymentStatus()`](/base-account/reference/base-pay/getPaymentStatus) must match the value used in the original [`pay()`](/base-account/reference/base-pay/pay) call. If you initiated a payment on testnet with `testnet: true`, you must also pass `testnet: true` when checking its status. This is what the user will see when prompted to pay: @@ -122,13 +122,13 @@ You can use the `callbackURL` to validate the user's information on the server s Learn more about this in the [callbackURL reference](/base-account/reference/core/capabilities/datacallback). -## Server side +## Server Side When accepting payments, your backend must validate transactions and user info received from the frontend. This section covers two critical aspects: verifying transaction completion and validating user information. -### Verify user transaction +### Verify User Transaction -Use `getPaymentStatus()` on your backend to confirm that a payment has been completed before fulfilling orders. Never trust payment confirmations from the frontend alone. +Use [`getPaymentStatus()`](/base-account/reference/base-pay/getPaymentStatus) on your backend to confirm that a payment has been completed before fulfilling orders. Never trust payment confirmations from the frontend alone. ```ts Backend (SDK) import { getPaymentStatus } from '@base-org/account'; @@ -222,31 +222,10 @@ export async function verifyAndFulfillPayment( - Consider adding an index on the transaction ID for fast lookups -### Validate user info with dataCallback +### Validate User Info If you're collecting user information (email, phone, shipping address) during checkout, use the `callbackURL` parameter to validate this data server-side before the transaction is submitted. -When you provide a `callbackURL` in your `payerInfo` configuration, the wallet will POST the user's information to your endpoint for validation. This gives you the opportunity to: - -- Verify email addresses are valid and not disposable -- Validate shipping addresses against your supported regions -- Check phone numbers are correctly formatted -- Block suspicious or fraudulent information - -```ts Client-side setup -const payment = await pay({ - amount: '25.00', - to: '0xRecipient', - payerInfo: { - requests: [ - { type: 'email' }, - { type: 'physicalAddress' } - ], - callbackURL: 'https://your-api.com/validate-payment-info' - } -}); -``` - Your callback endpoint receives the user's information and must respond with either a success or error response: ```ts Backend (validation endpoint) From 205eeadae567ecf17111e6c6a09023f380912914 Mon Sep 17 00:00:00 2001 From: youssefea Date: Fri, 16 Jan 2026 17:12:00 +0100 Subject: [PATCH 3/3] update to add impersonation attack --- docs/base-account/guides/accept-payments.mdx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/base-account/guides/accept-payments.mdx b/docs/base-account/guides/accept-payments.mdx index 96004b8c0..2bd176355 100644 --- a/docs/base-account/guides/accept-payments.mdx +++ b/docs/base-account/guides/accept-payments.mdx @@ -145,12 +145,13 @@ export async function checkPayment(txId: string, testnet = false) { ``` -**Prevent Replay Attacks** +**Prevent Replay and Impersonation Attacks** -A malicious user could submit the same valid transaction ID multiple times to receive goods or services repeatedly. Always track processed transaction IDs in your database. +- **Replay attacks:** A malicious user could submit the same valid transaction ID multiple times. Always track processed transaction IDs in your database. +- **Impersonation attacks:** A malicious user could submit someone else's transaction ID to fulfill their own order. Always verify that the payment sender matches the authenticated user. -Here's an example that prevents replay attacks by storing processed transactions: +Here's an example that prevents both attack vectors: ```ts Backend (with replay protection) expandable import { getPaymentStatus } from '@base-org/account'; @@ -167,6 +168,7 @@ const processedTransactions = new Map