Commands

Wallet Auth (Sign in with Ethereum)

Wallet Auth is our native support for Sign in With Ethereum.

World App currently uses Safe addresses in order to do the SIWE flow. This means that it will not work with common libraries like SIWE which expect EOAs.

Creating the nonce

Since the user can modify the client, it's important to create the nonce in the backend. The nonce must be at least 8 alphanumeric characters in length.

app/api/nonce.ts

import {cookies} from "next/headers"; import {(NextRequest, NextResponse)} from "next/server";

export function GET(req: NextRequest) {
  // Expects only alphanumeric characters
  const nonce = crypto.randomUUID().replace(/-/g, "");

// The nonce should be stored somewhere that is not tamperable by the client
// Optionally you can HMAC the nonce with a secret key stored in your environment
cookies().set("siwe", nonce, { secure: true });
return NextResponse.json({ nonce });
}

Using the command

Async handlersEvent listeners

Sending & handling the command response

Below is the expected input for walletAuth.

interface WalletAuthInput {
	nonce: string
	expirationTime?: Date
	statement?: string
	requestId?: string
	notBefore?: Date
}

Using the async walletAuth command.

app/page.tsx

import { MiniKit, WalletAuthInput } from '@worldcoin/minikit-js'
// ...
const signInWithWallet = async () => {
	if (!MiniKit.isInstalled()) {
		return
	}
	const res = await fetch(`/api/nonce`)
	const { nonce } = await res.json()

	const {commandPayload: generateMessageResult, finalPayload} = await MiniKit.commandsAsync.walletAuth({
		nonce: nonce,
		requestId: '0', // Optional
		expirationTime: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
		notBefore: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
		statement: 'This is my statement and here is a link https://worldcoin.com/apps',
	})
	// ...

The returned message (in final payload) will include a signature compliant with ERC-191. You're welcome to use any third party libraries to verify the payloads for SIWE, but since World App uses Safe addresses that do not implement isValidSignature we created a helper function for you. Under the hood, we use the wallet's EOA private key to sign the message but the address is the Safe address.

type MiniAppWalletAuthSuccessPayload = {
	status: 'success'
	message: string
	signature: string
	address: string
	version: number
}

app/page.tsx

const signInWithWallet = async () => {
	if (!MiniKit.isInstalled()) {
		return
	}

	const res = await fetch(`/api/nonce`)
	const { nonce } = await res.json()

	const { commandPayload: generateMessageResult, finalPayload } = await MiniKit.commandsAsync.walletAuth({
		nonce: nonce,
		requestId: '0', // Optional
		expirationTime: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
		notBefore: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
		statement: 'This is my statement and here is a link https://worldcoin.com/apps',
	})

	if (finalPayload.status === 'error') {
		return
	} else {
		const response = await fetch('/api/complete-siwe', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify({
				payload: finalPayload,
				nonce,
			}),
		})
	}
}

You can now additionally access the user's wallet address from the minikit object.

const walletAddress = MiniKit.walletAddress
// or
const walletAddress = window.MiniKit?.walletAddress

Verifying the Login

Finally, complete the sign in by verifying the response from World App in your backend. Here we check the nonce matches the one we created earlier, and then verify the signature.

app/api/complete-siwe.ts

import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
import { MiniAppWalletAuthSuccessPayload, verifySiweMessage } from '@worldcoin/minikit-js'

interface IRequestPayload {
	payload: MiniAppWalletAuthSuccessPayload
	nonce: string
}

export const POST = async (req: NextRequest) => {
	const { payload, nonce } = (await req.json()) as IRequestPayload
	if (nonce != cookies().get('siwe')?.value) {
		return NextResponse.json({
			status: 'error',
			isValid: false,
			message: 'Invalid nonce',
		})
	}
	try {
		const validMessage = await verifySiweMessage(payload, nonce)
		return NextResponse.json({
			status: 'success',
			isValid: validMessage.isValid,
		})
	} catch (error: any) {
		// Handle errors in validation or processing
		return NextResponse.json({
			status: 'error',
			isValid: false,
			message: error.message,
		})
	}
}