Add Dynamic’s Sign-In With Ethereum (SIWE) authentication to your World Mini App, enabling secure user authentication with their Worldcoin wallet.

You can find the complete source code for this guide in our GitHub repository.

Setup

Dynamic Environment Setup

  1. Navigate to the Dynamic Dashboard
  2. Copy your Environment ID from the SDK and API Keys section - you’ll need this to initialize Dynamic in your World Mini App

Dynamic Next.js Starter

If you don’t have a project yet, create one:

npx create-dynamic-app@latest my-world-app
cd my-world-app

Getting Started

1. Install Required Dependencies

First, install the necessary packages:

npm install @dynamic-labs/sdk-api-core @dynamic-labs/utils @worldcoin/minikit-js @worldcoin/minikit-react eruda

2. Set Up Project Structure

Create the following project structure:

world-miniapp/
├── .env.local                   # Environment variables
├── app/
│   ├── page.tsx                 # Main app page
│   ├── page.css                 # Page styles
│   ├── layout.tsx               # App layout
│   ├── globals.css              # Global styles
│   └── components/
│       ├── WorldDynamicSIWE.tsx # SIWE component
│       ├── WorldDynamicSIWE.css # Styles for SIWE component
│       └── WorldMethods.tsx     # Additional World methods (optional)
├── lib/
│   ├── providers.tsx            # Dynamic and other providers
│   ├── dynamic.ts               # Dynamic specific utilities
│   ├── utils.ts                 # General utility functions
│   └── wagmi.ts                 # Wagmi configuration

3. Configure Environment Variables

Create a .env.local file with your credentials:

NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-dynamic-environment-id-here

Implementation

Set Up SIWE Authentication Providers

First, let’s configure the authentication providers that will connect Dynamic’s SIWE functionality with Worldcoin’s wallet:

lib/providers.tsx
"use client";

import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";
import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MiniKitProvider } from "@worldcoin/minikit-js/minikit-provider";
import { useEffect } from "react";
import { WagmiProvider } from "wagmi";
import { DYNAMIC_ENVIRONMENT_ID } from "./utils";
import { config } from "./wagmi";

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient();
  useEffect(() => {
    import("eruda").then((eruda) => {
      eruda.default.init();
    });
  }, []);

  return (
    <DynamicContextProvider
      theme="auto"
      settings={{
        environmentId: DYNAMIC_ENVIRONMENT_ID,
        walletConnectors: [EthereumWalletConnectors],
      }}
    >
      <WagmiProvider config={config}>
        <QueryClientProvider client={queryClient}>
          <DynamicWagmiConnector>
            <MiniKitProvider>{children}</MiniKitProvider>
          </DynamicWagmiConnector>
        </QueryClientProvider>
      </WagmiProvider>
    </DynamicContextProvider>
  );
}

We’ve added the MiniKitProvider from Worldcoin, for the app to work within a world ID mini app. We’ve also added eruda which will allow us to view logs and debug our app.

Create Utility Functions

Create the Dynamic API helper and other utility functions:

lib/utils.ts
"use client";

import { Configuration, SDKApi } from "@dynamic-labs/sdk-api-core";
import {
  getAuthToken,
  VERSION as SDKVersion,
} from "@dynamic-labs/sdk-react-core";
import { FetchService } from "@dynamic-labs/utils";

// Constants
export const DYNAMIC_ENVIRONMENT_ID =
  process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID

// Dynamic API
export const dynamicApi = () => {
  const settings = {
    basePath: "https://app.dynamicauth.com/api/v0",
    headers: {
      "Content-Type": "application/json",
      "x-dyn-version": `WalletKit/${SDKVersion}`,
      "x-dyn-api-version": "API/0.0.507",
      Authorization: "",
    },
  };

  const minJwt = getAuthToken();
  if (minJwt) {
    settings.headers.Authorization = `Bearer ${minJwt}`;
  }

  return new SDKApi(
    new Configuration({
      ...settings,
      fetchApi: FetchService.fetch,
    })
  );
};

We’re going to use these later to implement SIWE functionality.

Create SIWE Authentication Component

Now let’s implement the core component that handles the Sign-In With Ethereum authentication flow:

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

import { generateMessageToSign } from "@dynamic-labs/multi-wallet";
import { VerifyRequestFromJSON } from "@dynamic-labs/sdk-api-core";
import { useRefreshUser } from "@dynamic-labs/sdk-react-core";
import { MiniKit } from "@worldcoin/minikit-js";
import { FC, useState } from "react";
import { createWalletClient, http } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { DYNAMIC_ENVIRONMENT_ID, dynamicApi } from "@/lib/utils";
import "./WorldDynamicSIWE.css";

const WorldDynamicSIWE: FC = () => {
  const refreshUser = useRefreshUser();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [siweData, setSiweData] = useState<any>(null);

  const safeStringify = (obj: unknown): string => {
    const seen = new WeakSet();
    return JSON.stringify(
      obj,
      (_, value) => {
        if (typeof value === "object" && value !== null) {
          if (seen.has(value)) {
            return "[Circular]";
          }
          seen.add(value);
        }
        return value;
      },
      2
    );
  };

  const storeNonce = (nonce: string) => {
    localStorage.setItem("dynamic_nonce", JSON.stringify({ value: nonce }));
  };

  const consumeNonce = () => {
    const nonceString = localStorage.getItem("dynamic_nonce");
    if (nonceString) {
      const nonceObject = JSON.parse(nonceString);
      const nonce = nonceObject.value;
      localStorage.removeItem("dynamic_nonce");
      return nonce;
    }
    return null;
  };

  const generateDynamicNonce = async (): Promise<string> => {
    const options = { method: "GET" };

    const response = await fetch(
      `https://app.dynamicauth.com/api/v0/sdk/${DYNAMIC_ENVIRONMENT_ID}/nonce`,
      options
    );
    const data = await response.json();
    return data.nonce;
  };

  const genTestWallet = () => {
    const privateKey = generatePrivateKey();
    const account = privateKeyToAccount(privateKey);
    return createWalletClient({
      account,
      chain: mainnet,
      transport: http(),
    });
  };

  const callVerifyUser = async (
    messageToSign: string,
    address: string,
    signedMessage: string
  ) => {
    const verifyRequest = VerifyRequestFromJSON({
      chain: "EVM",
      messageToSign,
      network: "1",
      publicWalletAddress: address,
      signedMessage,
      walletName: "worldcoin",
      walletProvider: "browserExtension",
    });

    try {
      const response = await dynamicApi().verify({
        environmentId: DYNAMIC_ENVIRONMENT_ID,
        verifyRequest,
      });

      window.localStorage.setItem(
        "dynamic_authentication_token",
        JSON.stringify(response.jwt)
      );
      window.localStorage.setItem(
        "dynamic_min_authentication_token",
        JSON.stringify(response.jwt)
      );

      return response;
    } catch (error) {
      console.error("Verify error:", error);
      throw error;
    }
  };

  const handleWorldcoinSIWE = async () => {
    setIsLoading(true);
    setError(null);

    try {
      const dynamicNonce = await generateDynamicNonce();
      storeNonce(dynamicNonce);

      const { finalPayload } = await MiniKit.commandsAsync.walletAuth({
        nonce: dynamicNonce,
      });

      if (!finalPayload) {
        throw new Error("No payload received from Worldcoin wallet auth");
      }

      const wallet = genTestWallet();
      const [address] = await wallet.getAddresses();
      const nonce = consumeNonce();

      const messageToSign = generateMessageToSign({
        blockchain: "EVM",
        chainId: 1,
        domain: window.location.host,
        nonce,
        publicKey: address,
        requestId: DYNAMIC_ENVIRONMENT_ID,
        uri: window.location.href,
      });

      const signature = await wallet.signMessage({ message: messageToSign });

      const verifyResponse = await callVerifyUser(
        messageToSign,
        address,
        signature
      );

      await refreshUser();

      setSiweData({
        message: messageToSign,
        signature,
        address,
        verifyResponse,
        worldcoinPayload: JSON.parse(safeStringify(finalPayload)),
      });
    } catch (err) {
      console.error("SIWE error:", err);
      setError(
        err instanceof Error ? err.message : "SIWE authentication failed"
      );
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="world-dynamic-siwe">
      <h3>Dynamic SIWE with Worldcoin</h3>

      {error && (
        <div className="error-message">
          <p>Error: {error}</p>
        </div>
      )}

      <div className="button-group">
        <button
          onClick={handleWorldcoinSIWE}
          disabled={isLoading}
          className="siwe-button"
        >
          {isLoading
            ? "Authenticating..."
            : "Sign In With Dynamic"}
        </button>
      </div>

      {siweData && (
        <div className="siwe-data">
          <h4>SIWE Data</h4>
          <pre>{safeStringify(siweData)}</pre>
        </div>
      )}
    </div>
  );
};

export default WorldDynamicSIWE;

Here, we’re using Dynamic’s API to generate a nonce, which is then used in the SIWE flow. This component implements a complete Sign-In With Ethereum (SIWE) authentication flow that showcases how Dynamic’s platform integrates with Worldcoin’s MiniKit. The component generates a temporary test wallet, uses the Worldcoin MiniKit for initial authentication, creates and signs a standardized SIWE message, then verifies the signature through Dynamic’s API to establish a secure user session with JWT tokens stored in localStorage.

You can take a look at the CSS file in our GitHub repository.

Create Main App Page

Create the main page of your World Mini App:

app/page.tsx
"use client";

import { MiniKit } from "@worldcoin/minikit-js";
import Image from "next/image";
import { useEffect, useState } from "react";
import "./main.css";
import WorldDynamicSIWE from "./components/WorldDynamicSIWE";
import { useDarkMode } from "@/lib/useDarkMode";

export default function Main() {
  const { isDarkMode } = useDarkMode();
  const [isMiniKitInstalled, setIsMiniKitInstalled] = useState<boolean | null>(null);

  useEffect(() => {
    const checkMiniKitInstallation = async () => {
      try {
        const result = await MiniKit.install();
        setIsMiniKitInstalled(result.success);
      } catch (error) {
        console.error("Failed to install MiniKit:", error);
        setIsMiniKitInstalled(false);
      }
    };

    checkMiniKitInstallation();
  }, []);

  return (
    <div className={`container ${isDarkMode ? "dark" : "light"}`}>
      <div className="header">
        <Image
          className="logo"
          src={isDarkMode ? "/logo-light.png" : "/logo-dark.png"}
          alt="dynamic"
          width={200}
          height={30}
          priority
        />
      </div>

      {isMiniKitInstalled === false && (
        <div className="warning-banner">
          This is a World Mini App. Please open it within the World App for full functionality.
        </div>
      )}

      <div className="modal">
        <WorldDynamicSIWE />
      </div>

      <div className="footer">
        <div>Made with 💙 by Dynamic & World</div>
      </div>
    </div>
  );
}

We first check if the MiniKit is installed and ready to use. If not, we display a warning banner to inform users they should open the app within the World App for full functionality. The core of our page is the WorldDynamicSIWE component, which leverages Dynamic’s authentication system to handle the complete SIWE flow, demonstrating how Dynamic can integrate seamlessly with specialized wallets like Worldcoin.

Optional: Adding Message Signing Capabilities

After implementing Dynamic’s authentication system, you can optionally extend your app with message signing functionality through Dynamic’s integration with the Worldcoin wallet:

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

import { useState } from "react";
import {
  MiniKit,
  SignMessageInput
} from "@worldcoin/minikit-js";

export default function WorldMethods() {
  const [result, setResult] = useState("");
  const [isSigning, setIsSigning] = useState(false);

  // Format response
  const safeStringify = (obj: unknown): string => {
    const seen = new WeakSet();
    return JSON.stringify(
      obj,
      (key, value) => {
        if (typeof value === "object" && value !== null) {
          if (seen.has(value)) return "[Circular]";
          seen.add(value);
        }
        return value;
      },
      2
    );
  };

  // Clear results
  const clearResult = () => {
    setResult("");
  };

  // Message signing implementation using Dynamic's integration with Worldcoin wallet
  const signMessage = async () => {
    try {
      setIsSigning(true);

      if (!(await MiniKit.isInstalled())) {
        setResult(
          "MiniKit is not installed. Please open this app in World App."
        );
        return;
      }

      const signMessagePayload: SignMessageInput = {
        message: "Hello from Dynamic World App",
      };

      const { finalPayload } = await MiniKit.commandsAsync.signMessage(
        signMessagePayload
      );

      if (finalPayload.status === "error") {
        setResult(`Signing error: Signing failed`);
        return;
      }

      setResult(
        `Message signed successfully!\n` +
          `Signature: ${finalPayload.signature}\n` +
          `Address: ${finalPayload.address}`
      );
    } catch (error) {
      setResult(`Signing error: ${safeStringify(error)}`);
    } finally {
      setIsSigning(false);
    }
  };

  return (
    <div className="world-methods">
      <h3>Dynamic Worldcoin Integration</h3>
      <div className="methods-container">
        <button
          onClick={signMessage}
          disabled={isSigning}
          className="btn btn-primary"
        >
          {isSigning ? "Signing..." : "Sign Message with Dynamic"}
        </button>
      </div>

      {result && (
        <div className="results-container">
          <pre className="results-text">{result}</pre>
          <button className="btn" onClick={clearResult}>Clear</button>
        </div>
      )}
    </div>
  );
}

Add this component to your page:

<div className="modal">
  <WorldDynamicSIWE />
  <WorldMethods />
</div>

More details on implementing message signing through Dynamic and Worldcoin can be found in the Worldcoin MiniKit documentation.

Running Your World Mini App

To run your World Mini App with Worldcoin SIWE integration:

npm run dev

In the development environment, you can use something like Ngrok or cloudflared to expose your local server to the internet, allowing you to test the World Mini App in the World App.

Head over to the Worldcoin Developer dashboard, create a new app and add in your tunnel URL as the app URL. This will allow you to test your Dynamic-powered World Mini App in the World App. Finally, you can open it in the World App by scanning the QR code or entering the URL directly.

Make sure to add your domain to next.config.js in allowedDevorigins like this:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  allowedDevOrigins: [
    "http://localhost:3000",
    "https://*.ngrok-free.app",
  ],
};

module.exports = nextConfig;

Resources