Skip to main content
This command is essential for applications that need to facilitate payments directly within the app, enabling seamless transactions for users. At launch, WLD and USDC will be supported. Example: Enabling an e-commerce platform to allow users to purchase digital goods using cryptocurrencies, providing a smooth checkout experience. Payments are easy to use and only have three simple steps.
  1. Creating the transaction
  2. Sending the command
  3. Verifying the payment
For legal reasons, payments are not available in Indonesia and Philippines.

Setup

Payments are executed on-chain, so you’ll need an Ethereum compatible wallet. Next, whitelist the address in the Developer Portal. Whitelisting adds security to your mini app to prevent payments from being sent to an unauthorized addresses. Optionally you can disable this check in the Developer Portal. Whitelist an Address

Initiating the payment

For security, it’s important you initialize and store your payment operation in the backend.
app/api/initiate-pay/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
	const uuid = crypto.randomUUID().replace(/-/g, '')

	// TODO: Store the ID field in your database so you can verify the payment later

	return NextResponse.json({ id: uuid })
}

Using the command

Sending the command & handling the response

We currently support WLD and USDC payments on Worldchain. Below is the expected input for the Pay command. Since World App sponsors the gas fee, there is a minimum transfer amount of $0.1 for all tokens.
PayCommandInput
// Represents tokens you allow the user to pay with and amount for each
export type TokensPayload = {
  symbol: Tokens;
  token_amount: string;
};

export type PayCommandInput = {
  reference: string;
  to: string;
  tokens: TokensPayload[];
  network?: Network; // Optional
  description: string;
};
For convenience, we offer a public endpoint to query the current price of WLD in various currencies detailed here.
app/page.tsx
import { MiniKit, tokenToDecimals, Tokens, PayCommandInput } from '@worldcoin/minikit-js'

const sendPayment = async () => {
  const res = await fetch('/api/initiate-payment', {
    method: 'POST',
  })
  const { id } = await res.json()

  const payload: PayCommandInput = {
    reference: id,
    to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', // Test address
    tokens: [
      {
        symbol: Tokens.WLD,
        token_amount: tokenToDecimals(1, Tokens.WLD).toString(),
      },
      {
        symbol: Tokens.USDC,
        token_amount: tokenToDecimals(3, Tokens.USDC).toString(),
      },
    ],
    description: 'Test example payment for minikit',
  }

  if (!MiniKit.isInstalled()) {
    return
  }

  const { finalPayload } = await MiniKit.commandsAsync.pay(payload)

  if (finalPayload.status == 'success') {
    const res = await fetch(`/api/confirm-payment`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(finalPayload),
    })
    const payment = await res.json()
    if (payment.success) {
      // Congrats your payment was successful!
    }
  }
}

Verifying the payment

You should always verify the payment in your backend. Users can manipulate information in the frontend, so the response must be verified in a trusted environment.
There are two ways to verify a payment:
  • Developer Portal API: Call our API to get the current status of the transaction. Since payments are executed on-chain, it can take up to a few minutes to confirm. You can choose to optimistically accept the payments once they’ve landed on-chain, or poll the endpoint to wait until it’s successfully mined.
  • On-chain verification (advanced): Verify payments by inspecting the ERC-4337 UserOperationEvent emitted during the pay operation.

Developer Portal API

Use the Get Transaction endpoint to get the current status of the transaction. When the transaction has landed on-chain, the transaction_status will be mined.
app/confirm-payment/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { MiniAppPaymentSuccessPayload } from '@worldcoin/minikit-js'

interface IRequestPayload {
	payload: MiniAppPaymentSuccessPayload
}

export async function POST(req: NextRequest) {
	const { payload } = (await req.json()) as IRequestPayload

	// IMPORTANT: Here we should fetch the reference you created in /initiate-payment to ensure the transaction we are verifying is the same one we initiated
	const reference = await getReferenceFromDB()

	// 1. Check that the transaction we received from the mini app is the same one we sent
	if (payload.reference === reference) {
		const response = await fetch(
			`https://developer.worldcoin.org/api/v2/minikit/transaction/${payload.transaction_id}?app_id=${process.env.APP_ID}&type=payment`,
			{
				method: 'GET',
				headers: {
					Authorization: `Bearer ${process.env.DEV_PORTAL_API_KEY}`,
				},
			}
		)
		const transaction = await response.json()

		// 2. Here we optimistically confirm the transaction.
		//    Otherwise, you can poll until the transaction_status == mined
		if (transaction.reference === reference && transaction.transaction_status !== 'failed') {
			return NextResponse.json({ success: true })
		} else {
			return NextResponse.json({ success: false })
		}
	}
}

On-chain verification (advanced)

Verify payments by inspecting the ERC-4337 UserOperationEvent emitted during the pay operation.
The TransferReference event will no longer be emitted. Instead, the reference string is encoded in the nonceKey of the UserOperationEvent as described below.
World App encodes your reference and miniappId into the nonceKey of the UserOperationEvent, allowing you to verify the payment on-chain.

How it works

The UserOperationEvent nonce is a 32-byte value split into two parts:
  • Nonce key (top 24 bytes): contains your payment identifiers
  • Nonce sequence (bottom 8 bytes): a counter
The nonceKey is constructed as:
  • 1 byte: version (currently 1)
  • 13 bytes: truncated SHA-256 hash of your miniappId
  • 10 bytes: truncated SHA-256 hash of your reference (the unique ID you passed in PayCommandInput.reference)
To verify a payment, inspect the UserOperationEvent logs for the sender address. Find the matching event by comparing the embedded bytes in the nonceKey with the expected bytes.

Example

import { createHash } from 'crypto'
import { ethers } from 'ethers'

const MINIAPP_ID = 'app_YOUR_APP_ID'

const WORLDCHAIN_RPC_URL = 'https://worldchain-mainnet.g.alchemy.com/public'

const provider = new ethers.JsonRpcProvider(WORLDCHAIN_RPC_URL)

// ERC-4337 EntryPoint v0.7 contract address
const ENTRYPOINT_ADDRESS = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'

// Minimal ABI with only the UserOperationEvent definition
const ENTRYPOINT_ABI = [
  {
    anonymous: false,
    inputs: [
      { indexed: true, internalType: 'bytes32', name: 'userOpHash', type: 'bytes32' },
      { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
      { indexed: true, internalType: 'address', name: 'paymaster', type: 'address' },
      { indexed: false, internalType: 'uint256', name: 'nonce', type: 'uint256' },
      { indexed: false, internalType: 'bool', name: 'success', type: 'bool' },
      { indexed: false, internalType: 'uint256', name: 'actualGasCost', type: 'uint256' },
      { indexed: false, internalType: 'uint256', name: 'actualGasUsed', type: 'uint256' },
    ],
    name: 'UserOperationEvent',
    type: 'event',
  },
]

const ENTRYPOINT_INTERFACE = new ethers.Interface(ENTRYPOINT_ABI)

// Used to identify the UserOperationEvent logs
// Should be 0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f
const USER_OPERATION_EVENT_TOPIC = ENTRYPOINT_INTERFACE.getEvent('UserOperationEvent')!.topicHash

// In the EntryPoint contract, `UserOperationEvent` includes a `nonce` field.
// That nonce is structured as: [24-byte nonceKey][8-byte nonce sequence].
//
// nonceKey layout (24 bytes):
// [ 1B version ][ 13B sha256(miniappId)[0..13) ][ 10B sha256(reference)[0..10) ]

const VERSION_BYTES = 1
const MINIAPP_ID_BYTES = 13
const REFERENCE_BYTES = 10

async function verifyPaymentOnChain({
  senderAddress,
  reference,
  fromBlock,
}: {
  senderAddress: string
  /* The reference you generated when initiating the payment */
  reference: string
  /* Record provider.getBlockNumber() at payment initiation time */
  fromBlock: number
}): Promise<{
  verified: boolean;
  transactionHash: string | null;
  userOpHash: string | null;
}> {
  // 1. Query logs for UserOperationEvent for sender
  const logs = await provider.getLogs({
    address: ENTRYPOINT_ADDRESS,
    topics: [
      USER_OPERATION_EVENT_TOPIC, // Topic 0: Event signature
      null, // Topic 1: userOpHash (any)
      ethers.zeroPadValue(senderAddress, 32), // Topic 2: sender (indexed)
    ],
    fromBlock
  })

  // 2. Calculate expected hashes
  const expectedMiniappHash = createHash('sha256')
    .update(MINIAPP_ID, 'utf8')
    .digest()
    .subarray(0, MINIAPP_ID_BYTES)

  const expectedReferenceHash = createHash('sha256')
    .update(reference, 'utf8')
    .digest()
    .subarray(0, REFERENCE_BYTES)

  // 3. Iterate through the logs to find the matching event
  for (const log of logs) {
    const parsedEvent = ENTRYPOINT_INTERFACE.parseLog({
      topics: log.topics as string[],
      data: log.data,
    })
    if (!parsedEvent)
      continue

    const nonce = parsedEvent.args.nonce as bigint

    // The nonce is structured as: [24-byte nonce key][8-byte nonce sequence]
    // Extract the nonce key by shifting right by 64 bits
    const nonceKey = nonce >> 64n

    // Convert the nonce key to bytes (24 bytes)
    const nonceKeyHex = nonceKey.toString(16).padStart(48, '0')
    const nonceKeyBytes = Buffer.from(nonceKeyHex, 'hex')

    // Verify miniappId: bytes [1..14) of the nonce key
    const actualMiniappHash = nonceKeyBytes.subarray(VERSION_BYTES, VERSION_BYTES + MINIAPP_ID_BYTES)
    if (Buffer.compare(actualMiniappHash, expectedMiniappHash) !== 0)
      continue

    // Verify reference: bytes [14..24) of the nonce key
    const referenceOffset = VERSION_BYTES + MINIAPP_ID_BYTES
    const actualReferenceHash = nonceKeyBytes.subarray(referenceOffset, referenceOffset + REFERENCE_BYTES)
    if (Buffer.compare(actualReferenceHash, expectedReferenceHash) !== 0)
      continue

    // Check if the UserOperation was successful
    const success = parsedEvent.args.success as boolean

    return {
      verified: success,
      transactionHash: log.transactionHash,
      userOpHash: parsedEvent.args.userOpHash as string,
    }
  }

  return {
    verified: false,
    transactionHash: null,
    userOpHash: null,
  }
}

Success Result on World App

If implemented correctly, the user will see the following drawer on World App.