Notus API
Authentication

Privy

Privy is a simple, powerful library to connect users to your web3 product. It allows users to sign up with familiar Web2 methods (email, social logins) or connect their existing crypto wallets, providing a seamless onboarding experience.

Why combine Privy with Notus API?
By integrating Privy with Notus API, developers can:

  • Offer flexible login options (email, social, wallet) for easy user onboarding.
  • Abstract away blockchain complexities, associating a smart wallet (ERC-4337) with each user seamlessly.
  • Enable gasless transactions and other Account Abstraction benefits via Notus API, signed using Privy's embedded wallets.
  • Securely manage user sessions and interactions, with Privy handling wallet creation/management and Notus API powering onchain actions.

This combination allows you to build user-friendly dApps that appeal to both Web2 and Web3 audiences.

This guide demonstrates how to use Privy with the Notus API in a Next.js App Router project. You'll learn how to authenticate a user, retrieve their Externally Owned Account (EOA), use that EOA to interact with Notus API via backend API Route Handlers to register a smart wallet, request a swap quote, and execute a UserOperation.

Eager to get started? Check out our Privy + Notus API Example on GitHub for a complete implementation using Next.js App Router.

How to Use Privy with Notus API

Let's walk through the full integration step by step, emphasizing that Notus API calls are made from your backend (Next.js API Route Handlers) for security and best practices.

1. Start a Next.js Project

First, create a new Next.js project if you haven't already (ensure you are using the App Router, which is the default for recent versions of Next.js):

npx create-next-app@latest my-privy-notus-app
cd my-privy-notus-app

2. Install Privy SDK

Install the Privy React SDK:

npm install @privy-io/react-auth

3. Frontend: Create a Login Button

Create a simple client component for logging in and out with Privy. This will be used on your main page.

app/components/PrivyLoginButton.tsx
"use client";

import { usePrivy } from '@privy-io/react-auth';

export default function PrivyLoginButton() {
  const { ready, authenticated, login, logout } = usePrivy();

  // Wait for Privy to be ready before rendering
  if (!ready) {
    return null; 
  }

  return (
    <div>
      {authenticated ? (
        <button onClick={logout}>
          Log Out
        </button>
      ) : (
        <button onClick={login}>
          Log In with Privy
        </button>
      )}
    </div>
  );
}

4. Server-Side Authentication and Smart Wallet Management

Create a server-side authentication function that handles both Privy authentication and smart wallet management. This function will be used in your server components to get the authenticated user and their smart wallet address.

app/auth.ts
import { cookies } from "next/headers";
import { privy } from "./privy";
import { notusAPI, FACTORY_ADDRESS } from "./notusAPI";

// Server-side authentication function that handles Privy auth and smart wallet setup
export async function auth() {
  try {
    // Get Privy token from cookies
    const { get } = await cookies();
    const token = get("privy-id-token");

    if (!token) {
      return null;
    }

    // Get user from Privy
    let user = await privy.getUser({ idToken: token.value });

    // Create wallet if user doesn't have one
    if (!user.wallet?.address) {
      user = await privy.createWallets({
        userId: user.id,
        createEthereumWallet: true,
      });
    }

    // Check if smart wallet exists
    const { wallet } = await notusAPI
      .get("wallets/address", {
        searchParams: {
          externallyOwnedAccount: user.wallet?.address as string,
          factory: FACTORY_ADDRESS,
        },
      })
      .json<{
        wallet: { accountAbstraction: string; registeredAt: string | null };
      }>();

    // Register smart wallet if it doesn't exist
    if (!wallet.registeredAt) {
      await notusAPI.post("wallets/register", {
        json: {
          externallyOwnedAccount: user.wallet?.address as string,
          factory: FACTORY_ADDRESS,
          salt: "0",
        },
      });
    }

    // Return user with smart wallet address
    return {
      ...user,
      accountAbstractionAddress: wallet.accountAbstraction,
    };
  } catch (error) {
    console.error(error);
    return null;
  }
}

Then, use this auth() function in your server components to get the authenticated user and their smart wallet address:

app/page.tsx
import { auth } from "./auth";
import { redirect } from "next/navigation";

export default async function Home() {
  const user = await auth();

  if (!user) {
    redirect("/login");
  }

  return (
    <main>
      <h1>Welcome, {user.email}</h1>
      <p>Your Smart Wallet: {user.accountAbstractionAddress}</p>
      {/* Add your app content here */}
    </main>
  );
}

The auth() function handles:

  • Retrieving the Privy ID token from cookies
  • Getting the user from Privy
  • Creating a wallet if the user doesn't have one
  • Checking if a smart wallet exists for the user's EOA
  • Registering a new smart wallet if needed
  • Returning the user with their smart wallet address

This approach is more secure as it keeps all authentication and wallet management logic on the server side.

Note: At this stage, the smart wallet address is not yet deployed onchain. Deployment happens automatically with the user's first onchain transaction (e.g., swap or transfer) via a UserOperation.

5. Server-Side Transfer Quote Generation

Create a server action to handle transfer quote generation. This keeps the Notus API key secure on the server side.

app/actions/transfer.ts
"use server";

import { notusAPI } from "../notusAPI";

export async function getTransferQuote({
  eoa,
  smartWalletAddress,
  toAddress,
  token,
  amount,
  chainId = 137, // Example: Polygon
}: {
  eoa: string;
  smartWalletAddress: string;
  toAddress: string;
  token: string;
  amount: string;
  chainId?: number;
}) {
  try {
    const response = await notusAPI.post("crypto/transfer", {
      json: {
        amount: String(amount),
        chainId: Number(chainId),
        gasFeePaymentMethod: "ADD_TO_AMOUNT",
        payGasFeeToken: token,
        token,
        walletAddress: smartWalletAddress,
        toAddress,
        transactionFeePercent: 0,
      },
    }).json();

    return { success: true, data: response };
  } catch (error: any) {
    console.error("Error getting transfer quote:", error);
    return { success: false, error: error.message || "Failed to get transfer quote" };
  }
}

To transfer native tokens (e.g., MATIC on Polygon), use 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee as the token address.

Note: The payGasFeeToken field should contain the address of an ERC-20 token held in the smart wallet. This token will be used to pay both the partner’s transactionFeePercent and the gas fees for the UserOperation. In most cases, payGasFeeToken is the same as token, typically the token the user already holds in their wallet.

6. Server-Side User Operation Execution

Create a server action to handle the execution of user operations. This is where the magic happens - after getting a quote and signing it with Privy, we send the signature to Notus API to execute the transaction.

app/actions/execute.ts
"use server";

export async function executeUserOperation({
  quoteId,
  signature,
}: {
  quoteId: string;
  signature: string;
}) {
  try {
    const response = await fetch("https://api.notus.team/api/v1/crypto/execute-user-op", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        quoteId,
        signature,
      }),
    });

    const data = await response.json();
    return { success: true, data };
  } catch (error: any) {
    console.error("Error executing user operation:", error);
    return { success: false, error: error.message || "Failed to execute user operation" };
  }
}

How User Operation Execution Works:

  1. The quoteId from the transfer/swap quote is signed using Privy's signMessage
  2. The signature is sent to Notus API's /execute-user-op endpoint
  3. Notus API verifies the signature matches the EOA that created the quote
  4. If valid, Notus API creates and submits a UserOperation - a special transaction format for smart contract wallets
  5. The transaction is executed through Account Abstraction (ERC-4337)

7. Frontend: Transfer Execution Component

Create a client component that handles the transfer execution process, using the server action for quote generation and Privy for message signing.

app/components/TransferExecutor.tsx
"use client";

import { useSignMessage } from '@privy-io/react-auth';
import { useState } from 'react';
import { getTransferQuote } from '../actions/transfer';

export default function TransferExecutor({ eoa, smartWalletAddress }: { eoa: string; smartWalletAddress: string; }) {
  const [quoteId, setQuoteId] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [userOpHash, setUserOpHash] = useState<string | null>(null);

  const { signMessage } = useSignMessage({
    onSuccess: async (signature) => {
      if (!quoteId) return;
      await executeTransaction(quoteId, signature);
    },
    onError: (error) => {
      setError("Failed to sign message");
      setIsLoading(false);
    }
  });

  const handleTransfer = async () => {
    setIsLoading(true);
    setError(null);
    
    try {
      const result = await getTransferQuote({
        eoa,
        smartWalletAddress,
        toAddress: "0x123...",
        token: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", // USDC
        amount: "1",
        chainId: 137,
      });

      if (!result.success) throw new Error(result.error);
      
      setQuoteId(result.data.quoteId);
      await signMessage(result.data.quoteId);
    } catch (error) {
      setError(error instanceof Error ? error.message : "Transfer failed");
      setIsLoading(false);
    }
  };

  const executeTransaction = async (quoteId: string, signature: string) => {
    try {
      const response = await fetch('/api/notus/execute-op', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ quoteId, signature }),
      });
      
      const result = await response.json();
      if (!response.ok) throw new Error("Transaction failed");
      
      setUserOpHash(result.userOpHash);
    } catch (error) {
      setError(error instanceof Error ? error.message : "Execution failed");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <button onClick={handleTransfer} disabled={isLoading || !eoa}>
        {isLoading ? 'Processing...' : 'Transfer 1 USDC'}
      </button>
      {quoteId && <p>Quote ID: {quoteId}</p>}
      {userOpHash && <p>UserOp Hash: {userOpHash}</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

That's it! You've successfully integrated Privy authentication with Notus for gasless transactions. You can now build powerful web3 applications with a seamless user experience.

If you have any questions, make sure to check out Privy's documentation and join their Discord community for support.