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
- Navigate to the Dynamic Dashboard
- 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
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:
"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:
"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:
"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:
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:
/** @type {import('next').NextConfig} */
const nextConfig = {
allowedDevOrigins: [
"http://localhost:3000",
"https://*.ngrok-free.app",
],
};
module.exports = nextConfig;
Resources