---
title: "Build a Dual-Chain Mini App with Nimiq Pay"
description: "Build a mini app that uses both Nimiq and Ethereum providers with user confirmation flows"
canonical_url: "https://nimiq.dev/mini-apps/tutorials/dual-chain-mini-app-tutorial"
---
# Build a Dual-Chain Mini App with Nimiq Pay

> Build a mini app that uses both Nimiq and Ethereum providers with user confirmation flows

In this tutorial, you will build a mini app that uses both injected providers:

- the Nimiq provider for Nimiq account and signing flows
- the Ethereum provider for EIP-1193 account and signing flows

You will request accounts and signatures through the wallet. The provider returned from `init()` throws Nimiq wallet errors, which the example handles in `catch`; see [wallet errors](https://nimiq.dev/mini-apps/api-reference/nimiq-provider#wallet-errors).

## What you'll build

The mini app includes two action buttons:

<table>
<thead>
  <tr>
    <th>
      Flow
    </th>
    
    <th>
      Methods
    </th>
    
    <th>
      User confirmation expected
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Nimiq
    </td>
    
    <td>
      <code>
        listAccounts()
      </code>
      
       -> <code>
        sign()
      </code>
    </td>
    
    <td>
      Account sharing when needed, then signing
    </td>
  </tr>
  
  <tr>
    <td>
      Ethereum
    </td>
    
    <td>
      <code>
        eth_requestAccounts
      </code>
      
       -> <code>
        personal_sign
      </code>
    </td>
    
    <td>
      2 prompts (account connection, signing)
    </td>
  </tr>
</tbody>
</table>

## Prerequisites

- **Node.js** (version 22+ required)
- **Nimiq Pay** app on a mobile device (or emulator)
- Phone and dev machine on the same Wi-Fi network
- At least one Ethereum account available in Nimiq Pay for the Ethereum success path

## 1. Create the project

Use Vite with Vue + TypeScript for this tutorial:

```bash
npm create vite@latest my-mini-app -- --template vue-ts
cd my-mini-app
npm install
```

## 2. Configure the dev server

Edit `vite.config.ts`:

```ts
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    host: true,
  },
})
```

## 3. Install the Nimiq Mini App SDK

Install the latest release of [`@nimiq/mini-app-sdk`](https://www.npmjs.com/package/@nimiq/mini-app-sdk). The [wallet error handling](https://nimiq.dev/mini-apps/api-reference/nimiq-provider#wallet-errors) used in this tutorial requires SDK `0.2.0` or later:

<code-group>

```bash [pnpm]
pnpm add @nimiq/mini-app-sdk
```

```bash [npm]
npm install @nimiq/mini-app-sdk
```

```bash [yarn]
yarn add @nimiq/mini-app-sdk
```

```bash [bun]
bun add @nimiq/mini-app-sdk
```

</code-group>

## 4. Add the dual-chain mini app

In `src/App.vue`, use separate script, template, and style blocks.

### 4.1 Add the script block

```vue
<script setup lang="ts">
import { init } from '@nimiq/mini-app-sdk'
import { onMounted, ref } from 'vue'

interface EthereumProvider {
  request: (args: { method: string, params?: unknown[] | Record<string, unknown> }) => Promise<any>
}

declare global {
  interface Window {
    ethereum?: EthereumProvider
  }
}

let nimiqPromise: ReturnType<typeof init> | null = null

const loading = ref(false)
const isNimiqConnecting = ref(true)
const status = ref<string | null>(null)
const errorMessage = ref<string | null>(null)

const nimiqAccounts = ref<string[] | null>(null)
const nimiqSignature = ref<string | null>(null)
const ethAccounts = ref<string[] | null>(null)
const ethSignature = ref<string | null>(null)

// Convert a UTF-8 string to hex for personal_sign.
function toHexUtf8(input: string) {
  return `0x${Array.from(new TextEncoder().encode(input))
    .map(byte => byte.toString(16).padStart(2, '0'))
    .join('')}`
}

function resetFeedback() {
  errorMessage.value = null
  status.value = null
}

onMounted(async () => {
  try {
    nimiqPromise = init({ timeout: 10_000 })
    await nimiqPromise
  }
  catch (error) {
    errorMessage.value = error instanceof Error ? error.message : String(error)
  }
  finally {
    isNimiqConnecting.value = false
  }
})

async function runNimiqFlow() {
  resetFeedback()
  nimiqAccounts.value = null
  nimiqSignature.value = null

  if (!nimiqPromise) {
    errorMessage.value = 'Nimiq provider not ready. Open this app inside Nimiq Pay.'
    status.value = 'Nimiq flow failed.'
    return
  }

  loading.value = true

  try {
    const nimiq = await nimiqPromise

    // Request account sharing if the provider has no cached accounts.
    status.value = 'Requesting Nimiq accounts...'
    const accounts = await nimiq.listAccounts()
    nimiqAccounts.value = accounts
    if (!accounts.length)
      throw new Error('No Nimiq accounts returned.')

    // Request signing confirmation.
    status.value = 'Requesting Nimiq signing confirmation...'
    const signatureResult = await nimiq.sign('Nimiq Pay dual-chain tutorial')
    nimiqSignature.value = JSON.stringify(signatureResult, null, 2)
    status.value = 'Nimiq flow completed.'
  }
  catch (error) {
    errorMessage.value = error instanceof Error ? error.message : String(error)
    status.value = 'Nimiq flow failed.'
  }
  finally {
    loading.value = false
  }
}

async function runEthereumFlow() {
  resetFeedback()
  ethAccounts.value = null
  ethSignature.value = null

  if (!window.ethereum) {
    errorMessage.value = 'Ethereum provider not found. Open this app inside Nimiq Pay.'
    status.value = 'Ethereum flow failed.'
    return
  }

  loading.value = true

  try {
    // Prompt 1: wallet connection confirmation.
    status.value = 'Requesting Ethereum accounts...'
    const accounts = await window.ethereum.request({
      method: 'eth_requestAccounts',
    }) as string[]
    ethAccounts.value = accounts
    if (!accounts.length)
      throw new Error('No Ethereum accounts returned.')

    // Request signing confirmation.
    const message = toHexUtf8('Nimiq Pay dual-chain tutorial')
    status.value = 'Requesting Ethereum signing confirmation...'
    const signature = await window.ethereum.request({
      method: 'personal_sign',
      params: [message, accounts[0]],
    }) as string
    ethSignature.value = signature
    status.value = 'Ethereum flow completed.'
  }
  catch (error) {
    errorMessage.value = error instanceof Error ? error.message : String(error)
    status.value = 'Ethereum flow failed.'
  }
  finally {
    loading.value = false
  }
}
</script>
```

### 4.2 Add the template block

```vue
<template>
  <div class="app">
    <h1>Dual-Chain Mini App</h1>

    <div class="actions">
      <button :disabled="loading || isNimiqConnecting" @click="runNimiqFlow">
        Run Nimiq flow
      </button>
      <button :disabled="loading" @click="runEthereumFlow">
        Run Ethereum flow
      </button>
    </div>

    <p v-if="isNimiqConnecting">
      Waiting for the Nimiq provider to initialize...
    </p>

    <p v-if="status">
      <strong>Status:</strong> {{ status }}
    </p>
    <p v-if="errorMessage" class="error">
      <strong>Error:</strong> {{ errorMessage }}
    </p>

    <h2>Nimiq Output</h2>
    <pre v-if="nimiqAccounts" class="result">Accounts: {{ JSON.stringify(nimiqAccounts, null, 2) }}</pre>
    <pre v-if="nimiqSignature" class="result">Signature: {{ nimiqSignature }}</pre>

    <h2>Ethereum Output</h2>
    <pre v-if="ethAccounts" class="result">Accounts: {{ JSON.stringify(ethAccounts, null, 2) }}</pre>
    <pre v-if="ethSignature" class="result">Signature: {{ ethSignature }}</pre>
  </div>
</template>
```

### 4.3 Add the style block (mobile-friendly)

```vue
<style scoped>
.app {
  max-width: 42rem;
  margin: 0 auto;
  padding: 1rem;
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  color: #1f3553;
}

h1 {
  margin: 0 0 1rem;
  font-size: clamp(1.75rem, 7vw, 2.4rem);
  line-height: 1.2;
}

h2 {
  margin: 1rem 0 0.5rem;
}

.actions {
  display: grid;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

button {
  width: 100%;
  min-height: 44px;
  border: none;
  border-radius: 0.625rem;
  padding: 0.625rem 0.875rem;
  font-weight: 600;
  background: #1f3553;
  color: #fff;
}

button:disabled {
  opacity: 0.55;
}

.error {
  color: #b31b1b;
}

.result {
  margin: 0 0 0.625rem;
  padding: 0.625rem;
  border-radius: 0.5rem;
  background: #f5f8fc;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

@media (min-width: 640px) {
  .actions {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
</style>
```

## 5. Add localization

Nimiq Pay injects the user's selected language at `window.nimiqPay.language`. If you want your app to match the user's Nimiq Pay language, read it at the top of your script:

```javascript
const language = window.nimiqPay?.language
  || navigator.language.split('-')[0]
  || 'en'
```

This reads the Nimiq Pay language first, falls back to the device locale, then to English. For a full translations setup, see [Localization in Mini Apps](https://nimiq.dev/mini-apps/features/localization).

## 6. Run the mini app

```bash
npm run dev -- --host
```

Copy the **Network** URL from the terminal output, for example:

```bash
http://192.168.1.42:5173
```

## 7. Test inside Nimiq Pay

1. Make sure your phone and dev machine are on the same Wi‑Fi network.
2. Open **Nimiq Pay**.
3. Go to **Mini Apps**.
4. Enter your network URL: `http://<your-ip>:5173`

Open your mini app, tap **Run Nimiq flow**, then tap **Run Ethereum flow**.

You should see:

- Nimiq accounts and a Nimiq signature response.
- Ethereum account(s) and an Ethereum signature response.

## Troubleshooting

**No Ethereum account returned**<br />


The Ethereum success path requires at least one account available through Nimiq Pay.

**Cannot open local URL from phone**<br />


Restart the dev server with `--host`, then use the terminal's Network URL.

**Secure-context-only API missing**<br />


If your app uses APIs such as `crypto.randomUUID()`, they may be unavailable when the mini app is loaded from a local network URL like `http://<your-ip>:5173`. Add feature detection and a fallback, or use local HTTPS if the API is required.

**Port 5173 is busy**<br />


Vite chooses another port automatically (for example `5174` or `5175`). Always use the exact Network URL shown in the terminal.


## Sitemap

See the full [sitemap](https://nimiq.dev/sitemap.md) for all pages.
