diff --git a/docs/base-account/guides/accept-payments.mdx b/docs/base-account/guides/accept-payments.mdx
index 6c6d7467a..2bd176355 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,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()`](/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';
-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,140 @@ export async function checkPayment(txId, testnet = false) {
}
```
+
+**Prevent Replay and Impersonation Attacks**
+
+- **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 both attack vectors:
+
+```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,
+ payerAddress: string, // From authenticated user (SIWE, JWT, etc.)
+ 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. Verify the payment sender matches the authenticated user
+ // This prevents a malicious user from claiming someone else's payment
+ if (sender.toLowerCase() !== payerAddress.toLowerCase()) {
+ throw new Error('Payment sender does not match authenticated user');
+ }
+
+ // 4. 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');
+ }
+
+ // 5. 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()
+ });
+
+ // 6. 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
+
+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.
+
+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: