Skip to content

Profiles

In this guide, we'll explore how to use the Profiles feature in Smart Wallet to request and validate user information like email addresses and physical addresses for a simple checkout flow.

Profiles DemoProfiles Demo

What you'll achieve

By the end of this guide, you will:

  • Set up a NextJS+Wagmi project to work with Smart Wallet Profiles
  • Create a simple interface to request user profile data
  • Implement a validation API to verify the data
  • Handle successful responses and errors

Skip ahead

If you want to skip ahead and just get the final code, you can find it here:

Smart Wallet Profiles Demohttps://github.com/base/demos/tree/master/smart-wallet/profiles-demo

Understanding Profiles

The Profiles feature allows developers to request personal information from Smart Wallet users during a transaction. This is useful for applications that need:

  • Email addresses
  • Physical addresses
  • Phone numbers
  • Names

Smart Wallet handles the collection of this information and it is left to you to validate it and store it as you see fit while following the standards and regulations in place.

Project Setup

Let's create a simple app that demonstrates the Profiles feature. We'll be using NextJS with Wagmi hooks to integrate with Smart Wallet.

Configure Wagmi

First, ensure your Wagmi configuration is set up correctly:

wagmi.ts
import { http, cookieStorage, createConfig, createStorage } from "wagmi";
import { baseSepolia } from "wagmi/chains";
import { coinbaseWallet } from "wagmi/connectors";
 
const cbWalletConnector = coinbaseWallet({
  appName: "Profiles Demo",
  preference: {
    options: "smartWalletOnly",
  },
});
 
export function getConfig() {
  return createConfig({
    chains: [baseSepolia],
    connectors: [cbWalletConnector],
    storage: createStorage({
      storage: cookieStorage,
    }),
    ssr: true,
    transports: {
      [baseSepolia.id]: http(),
    },
  });
}
 
declare module "wagmi" {
  interface Register {
    config: ReturnType<typeof getConfig>;
  }
}

Creating the User Interface

Now, let's create a simple UI to request profile data from users along with a transaction. We'll create a page with checkboxes to select which data to request and a button to submit the request along with a transfer of 0.01 USDC to an address on Base Sepolia.

app/page.tsx
"use client";
 
import { useEffect, useState } from "react";
import { encodeFunctionData, erc20Abi, numberToHex, parseUnits } from "viem";
import { useConnect, useSendCalls } from "wagmi";
 
interface DataRequest {
  email: boolean;
  address: boolean;
}
 
interface ProfileResult {
  success: boolean;
  email?: string;
  address?: string;
  error?: string;
}
 
export default function Home() {
  const [dataToRequest, setDataToRequest] = useState<DataRequest>({
    email: true,
    address: true
  });
  const [result, setResult] = useState<ProfileResult | null>(null);
 
  const { sendCalls, data, error, isPending } = useSendCalls();
  const { connect, connectors } = useConnect()
 
 
  // Function to get callback URL - replace in production
  function getCallbackURL() {
    return "https://your-ngrok-url.ngrok-free.app/api/data-validation";
  }
 
  // Handle response data when sendCalls completes
  useEffect(() => {
    if (data?.capabilities?.dataCallback) {
      const callbackData = data.capabilities.dataCallback;
      const newResult: ProfileResult = { success: true };
 
      // Extract email if provided
      if (callbackData.email) newResult.email = callbackData.email;
 
      // Extract address if provided
      if (callbackData.physicalAddress) {
        const addr = callbackData.physicalAddress.physicalAddress;
        newResult.address = [
          addr.address1,
          addr.address2,
          addr.city,
          addr.state,
          addr.postalCode,
          addr.countryCode
        ].filter(Boolean).join(", ");
      }
 
      setResult(newResult);
    } else if (data && !data.capabilities?.dataCallback) {
      setResult({ success: false, error: "Invalid response - no data callback" });
    }
  }, [data]);
 
  // Handle errors
  useEffect(() => {
    if (error) {
      setResult({
        success: false,
        error: error.message || "Transaction failed"
      });
    }
  }, [error]);
 
  // Handle form submission
  async function handleSubmit() {
    try {
      setResult(null);
 
      // Build requests array based on checkboxes
      const requests = [];
      if (dataToRequest.email) requests.push({ type: "email", optional: false });
      if (dataToRequest.address) requests.push({ type: "physicalAddress", optional: false });
 
      if (requests.length === 0) {
        setResult({ success: false, error: "Select at least one data type" });
        return;
      }
 
      // Send calls using wagmi hook
      sendCalls({
        connector: connectors[0],
        account: null,
        calls: [
          {
            to: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC contract address on Base Sepolia
            data: encodeFunctionData({
              abi: erc20Abi,
              functionName: "transfer",
              args: [
                "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
                parseUnits("0.01", 6),
              ],
            }),
          },
        ],
        chainId: 84532, // Base Sepolia
        capabilities: {
          dataCallback: {
            requests: requests,
            callbackURL: getCallbackURL(),
          },
        },
      });
    } catch (err) {
      setResult({
        success: false,
        error: err instanceof Error ? err.message : "Unknown error occurred"
      });
    }
  }
 
  return (
    <div style={{ maxWidth: "600px", margin: "0 auto", padding: "20px" }}>
      <h1>Profiles Demo</h1>
 
      {/* Data Request Form */}
      <div style={{ marginTop: "20px" }}>
        <h2>Checkout</h2>
 
        <div>
          <label>
            <input
              type="checkbox"
              checked={dataToRequest.email}
              onChange={() => setDataToRequest(prev => ({ ...prev, email: !prev.email }))}
            />
            Email Address
          </label>
        </div>
 
        <div>
          <label>
            <input
              type="checkbox"
              checked={dataToRequest.address}
              onChange={() => setDataToRequest(prev => ({ ...prev, address: !prev.address }))}
            />
            Physical Address
          </label>
        </div>
 
        <button
          onClick={handleSubmit}
          disabled={isPending}
        >
          {isPending ? "Processing..." : "Checkout"}
        </button>
      </div>
 
      {/* Results Display */}
      {result && (
        <div style={{
          marginTop: "20px",
          padding: "15px",
          backgroundColor: result.success ? "#d4edda" : "#f8d7da",
          borderRadius: "5px"
        }}>
          {result.success ? (
            <>
              <h3>Data Received</h3>
              {result.email && <p><strong>Email:</strong> {result.email}</p>}
              {result.address && <p><strong>Address:</strong> {result.address}</p>}
            </>
          ) : (
            <>
              <h3>Error</h3>
              <p>{result.error}</p>
            </>
          )}
        </div>
      )}
    </div>
  );
}

Implementing the Validation API

Now, let's create an API endpoint to validate the data received from Smart Wallet. This endpoint will check for valid formats and return errors if needed.

app/api/data-validation/route.ts
// app/api/data-validation/route.ts
export async function POST(request: Request) {
  const requestData = await request.json();
 
  try {
    // Extract data from request
    const email = requestData.requestedInfo.email;
    const physicalAddress = requestData.requestedInfo.physicalAddress;
 
    const errors = {};
 
    // Example: Reject example.com emails
    if (email && email.endsWith("@example.com")) {
      errors.email = "Example.com emails are not allowed";
    }
 
    // Example: Validate physical address
    if (physicalAddress) {
      if (physicalAddress.postalCode && physicalAddress.postalCode.length < 5) {
        if (!errors.physicalAddress) errors.physicalAddress = {};
        errors.physicalAddress.postalCode = "Invalid postal code";
      }
 
      if (physicalAddress.countryCode === "XY") {
        if (!errors.physicalAddress) errors.physicalAddress = {};
        errors.physicalAddress.countryCode = "We don't ship to this country";
      }
    }
 
    // Return errors if any found
    if (Object.keys(errors).length > 0) {
      return Response.json({
        errors,
        /*request: {
          calls: [], // Replace the old calls with new ones
          chainId: numberToHex(84532), // Base Sepolia
          version: "1.0",
        },*/
      });
    }
 
    // Success - no validation errors - you HAVE to return the original calls
    return Response.json({
      request: {
        calls: requestData.calls,
        chainId: requestData.chainId,
        version: requestData.version,
      },
    });
 
  } catch (error) {
    console.error("Error processing data:", error);
    return Response.json({
      errors: { server: "Server error validating data" }
    });
  }
}

How It Works

When a user visits your application and connects their Smart Wallet, the following process occurs:

  1. User connects their Smart Wallet: The user clicks "Sign in with Smart Wallet" and authorizes the connection.

  2. App requests profile data: Your app calls wallet_sendCalls with the dataCallback capability, a list of calls to be made, a list of requested data types, and a callback URL to call with the data.

  3. Smart Wallet prompts the user: The wallet shows a UI asking the user to share the requested information.

  4. User provides data: The user enters/confirms their information in the Smart Wallet interface.

  5. Data is validated: Smart Wallet sends the data to your callback URL for validation.

  6. Validation response: Your API validates the data and returns success or errors as well as any new calls to be made.

  7. Transaction completion: If validation passes, the wallet returns the data to your application.

Understanding the Response Format

When a user provides profile information, Smart Wallet returns a response object with the following structure:

{
  callsId: string,
  requestedInfo: {
    email?: string,
    physicalAddress?: {
      physicalAddress: {
          address1: string,
          address2?: string,
          city: string,
          state: string,
          postalCode: string,
          countryCode: string,
          name?: {
            firstName: string,
            familyName: string,
          }
        },
        isPrimary: boolean,
      },
      walletAddress?: string,
      phoneNumber?: {
        number: string,
        country: string,
        isPrimary: boolean,
      },
      name?: {
        firstName: string,
        familyName: string,
      }
    }
  }
}

Each field will only be present if it was requested and provided by the user.

Validation Error Format

If your validation API finds issues with the data, return an errors object with this structure:

{
  errors: {
    email?: string,
    name?: {
      firstName?: string,
      lastName?: string,
    },
    phoneNumber?: {
      countryCode?: string,
      number?: string,
    },
    physicalAddress?: {
      address1?: string,
      address2?: string,
      city?: string,
      state?: string,
      postalCode?: string,
      country?: string,
    },
    walletAddress?: string,
  }
}

Smart Wallet will show these error messages to the user and prompt them to correct the information.

Testing Your Integration

To test your integration:

  1. Start your development server:
Terminal
npm run dev
  1. Start an ngrok tunnel to your local server:
Terminal
ngrok http 3000
  1. Update the ngrokUrl variable in your code with the HTTPS URL from ngrok.

  2. Open your browser and navigate to your localhost URL.

  3. Connect your Smart Wallet and test requesting and validating user data.

Making Fields Optional

You can make any requested field optional by setting the optional parameter:

requests: [
  { type: "email", optional: false },  // Required
  { type: "physicalAddress", optional: true },  // Optional
]

Optional fields will be marked as such in the Smart Wallet interface, allowing users to skip them.

Best Practices

  1. Only request what you need: Ask for the minimum information necessary for your application.

  2. Explain why: In your UI, clearly explain why you need each piece of information.

  3. Be transparent: Inform users how their data will be used and stored.

  4. Thorough validation: Validate all fields properly and provide helpful error messages.

  5. Handle errors gracefully: Always account for cases where users decline to provide information.

Conclusion

The Profiles feature in Smart Wallet provides a secure and user-friendly way to collect necessary user information for your onchain app. By following this guide, you've learned how to:

  • Request profile data from Smart Wallet users
  • Validate that data through a callback API
  • Process and display the information in your application

This integration simplifies user onboarding and data collection while maintaining user privacy and control.