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.

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:
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:
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.
"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
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:
-
User connects their Smart Wallet: The user clicks "Sign in with Smart Wallet" and authorizes the connection.
-
App requests profile data: Your app calls
wallet_sendCalls
with thedataCallback
capability, a list of calls to be made, a list of requested data types, and a callback URL to call with the data. -
Smart Wallet prompts the user: The wallet shows a UI asking the user to share the requested information.
-
User provides data: The user enters/confirms their information in the Smart Wallet interface.
-
Data is validated: Smart Wallet sends the data to your callback URL for validation.
-
Validation response: Your API validates the data and returns success or errors as well as any new calls to be made.
-
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:
- Start your development server:
npm run dev
- Start an ngrok tunnel to your local server:
ngrok http 3000
-
Update the
ngrokUrl
variable in your code with the HTTPS URL from ngrok. -
Open your browser and navigate to your localhost URL.
-
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
-
Only request what you need: Ask for the minimum information necessary for your application.
-
Explain why: In your UI, clearly explain why you need each piece of information.
-
Be transparent: Inform users how their data will be used and stored.
-
Thorough validation: Validate all fields properly and provide helpful error messages.
-
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.