> For the complete documentation index, see [llms.txt](https://docs.aboutcircles.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aboutcircles.com/miniapps/create-or-connect-a-circles-account-from-a-mini-app.md).

# Create or Connect a Circles Account from a Mini App

Use `@aboutcircles/miniapp-sdk` to let users create a new Circles account or connect an existing one without leaving your mini app.

This guide explains the recommended integration pattern for mini apps embedded inside the Circles host. The examples work as a foundation for vanilla JavaScript, React, Vue, Svelte, and other browser-based frameworks.

***

### Overview

A Circles mini app runs inside an `<iframe>` on its own origin. The surrounding **Circles host** owns the wallet experience and handles account creation, passkeys, Safe setup, and on-chain Circles registration.

Your mini app should not attempt to create or manage the account directly. It only needs to:

1. Ask the host to start the account flow with `requestCreateAccount()`.
2. Subscribe to wallet state changes with `onWalletChange()`.
3. Render the correct UI for connected, disconnected, and standalone states.

```
Mini app iframe                         Circles host
      │                                      │
      │  requestCreateAccount()              │
      ├─────────────────────────────────────▶│
      │                                      │  Opens passkey and account flow
      │                                      │
      │  onWalletChange(address)             │
      │◀─────────────────────────────────────┤
      │                                      │
```

The SDK wraps the cross-origin messaging protocol for you. In the normal integration path, your app does not need to call `window.parent.postMessage()` or manage browser message listeners.

When the flow succeeds, the returned `address` represents a registered Circles human account. You can use it immediately to read profile data, request transaction signing, or request message signing.

***

### Prerequisites

Before integrating the SDK, make sure you have:

* A browser-based application that can be served over HTTP or HTTPS.
* A Circles host environment that embeds your application by URL.
* A visible button or another direct user action that can start the sign-up flow.

The account flow opens a WebAuthn passkey prompt. Browsers require this prompt to originate from a real user gesture, such as a button click.

***

### Install the SDK

Choose the command that matches your package manager:

```bash
npm install @aboutcircles/miniapp-sdk
```

```bash
pnpm add @aboutcircles/miniapp-sdk
```

```bash
yarn add @aboutcircles/miniapp-sdk
```

Import the account-related SDK functions:

```ts
import {
  isMiniappMode,
  onWalletChange,
  requestCreateAccount,
} from '@aboutcircles/miniapp-sdk';
```

***

### Core API Concepts

#### `requestCreateAccount()`

```ts
requestCreateAccount(): Promise<{
  authenticated: boolean;
  address: string;
}>
```

Call this function when a user clicks your sign-up or connect button.

The host determines which experience the user needs:

* If the user is already connected, the promise resolves with their address.
* If the user is not connected, the host opens the account creation or login flow.
* If the user cancels the flow or the request fails, the promise rejects.

Always handle the rejection with `try/catch` so you can restore the UI cleanly.

#### `onWalletChange()`

```ts
onWalletChange(
  callback: (address: string | null) => void,
): () => void
```

Use this function as the source of truth for authentication state.

The callback runs immediately with the current state and runs again whenever the wallet state changes. The value is:

* A wallet address when the user is connected.
* `null` when the user is not connected or before the host returns a connected account.

The function returns an unsubscribe callback. Use it during component cleanup in framework-based applications.

#### `isMiniappMode()`

```ts
isMiniappMode(): boolean
```

Use this function to detect whether your app is running inside the Circles host.

If the app is opened as a standalone webpage, there is no host available to answer SDK requests. In that state, show an explanatory message instead of an inactive sign-up button.

***

### Recommended Integration Pattern

Treat wallet state as reactive application state:

1. Subscribe to `onWalletChange()` when the app initializes.
2. Render your connected or disconnected interface from the callback value.
3. Call `requestCreateAccount()` directly from a user click.
4. Use the returned promise only for immediate button state and error handling.
5. Continue to use `onWalletChange()` as the authoritative state source.

This matters because wallet state can change independently of the current click handler, including reconnects and changes initiated elsewhere.

***

### Minimal Vanilla JavaScript Example

The following example contains the complete account integration for a simple browser app.

```html
<div id="status">Checking account status…</div>
<button id="signup" type="button" hidden>
  Create or connect Circles account
</button>

<script type="module">
  import {
    isMiniappMode,
    onWalletChange,
    requestCreateAccount,
  } from '@aboutcircles/miniapp-sdk';

  const statusElement = document.getElementById('status');
  const signupButton = document.getElementById('signup');

  function renderWalletState(address) {
    if (address) {
      statusElement.textContent = `Connected: ${address}`;
      signupButton.hidden = true;
      return;
    }

    statusElement.textContent = 'No Circles account connected.';
    signupButton.hidden = false;
  }

  if (!isMiniappMode()) {
    statusElement.textContent = 'Open this mini app inside the Circles host.';
    signupButton.hidden = true;
  } else {
    const unsubscribe = onWalletChange(renderWalletState);

    signupButton.addEventListener('click', async () => {
      signupButton.disabled = true;

      try {
        const { address } = await requestCreateAccount();
        console.log('Circles account ready:', address);
      } catch (error) {
        const message =
          error instanceof Error ? error.message : 'Unknown account error';
        console.warn('Account flow cancelled or failed:', message);
      } finally {
        signupButton.disabled = false;
      }
    });

    // Call unsubscribe() if your application later removes this view.
  }
</script>
```

#### Why this works

* The button calls `requestCreateAccount()` directly from the click handler.
* `onWalletChange()` controls the rendered wallet state.
* Standalone mode is handled explicitly.
* The button is disabled while the host flow is active, preventing duplicate requests.

***

### React Integration

A reusable hook keeps the host integration isolated from the rest of your UI.

```tsx
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
  isMiniappMode,
  onWalletChange,
  requestCreateAccount,
} from '@aboutcircles/miniapp-sdk';

type UseCirclesAccountResult = {
  address: string | null;
  inHost: boolean;
  isConnecting: boolean;
  error: string | null;
  createOrConnectAccount: () => Promise<string | null>;
};

export function useCirclesAccount(): UseCirclesAccountResult {
  const inHost = useMemo(() => isMiniappMode(), []);
  const [address, setAddress] = useState<string | null>(null);
  const [isConnecting, setIsConnecting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!inHost) return;
    return onWalletChange(setAddress);
  }, [inHost]);

  const createOrConnectAccount = useCallback(async () => {
    if (!inHost) {
      setError('Open this mini app inside the Circles host.');
      return null;
    }

    setIsConnecting(true);
    setError(null);

    try {
      const result = await requestCreateAccount();
      return result.address;
    } catch (unknownError) {
      const message =
        unknownError instanceof Error
          ? unknownError.message
          : 'Account creation or connection failed.';

      setError(message);
      return null;
    } finally {
      setIsConnecting(false);
    }
  }, [inHost]);

  return {
    address,
    inHost,
    isConnecting,
    error,
    createOrConnectAccount,
  };
}
```

Use the hook in a component:

```tsx
import { useCirclesAccount } from './useCirclesAccount';

export function CirclesAccountButton() {
  const {
    address,
    inHost,
    isConnecting,
    error,
    createOrConnectAccount,
  } = useCirclesAccount();

  if (!inHost) {
    return <p>Open this mini app inside the Circles host.</p>;
  }

  if (address) {
    return <p>Connected: {address}</p>;
  }

  return (
    <div>
      <button
        type="button"
        disabled={isConnecting}
        onClick={createOrConnectAccount}
      >
        {isConnecting ? 'Connecting…' : 'Create or connect Circles account'}
      </button>

      {error ? <p role="alert">{error}</p> : null}
    </div>
  );
}
```

The click handler calls the SDK action directly, preserving the browser user gesture required by the passkey flow.

***

### Important Rules and Common Pitfalls

| Rule                                                                  | Reason                                                                                                                                                      |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Call `requestCreateAccount()` directly from a user gesture.           | The host opens a WebAuthn passkey prompt. Browsers can block the prompt when the request is triggered on page load or after an unrelated asynchronous step. |
| Use `onWalletChange()` as the source of truth.                        | The listener updates after sign-up and whenever wallet state changes later. The result of one promise should not become your only account state.            |
| Check `isMiniappMode()` before showing an interactive account button. | SDK requests cannot complete when the app is opened outside the host iframe.                                                                                |
| Do not send attribution data from the mini app.                       | The host derives sign-up attribution from the iframe origin. There is no attribution parameter for the mini app to provide.                                 |
| Handle cancelled flows.                                               | A user can dismiss or reject the host flow. Keep the button usable and show a non-blocking error message.                                                   |

#### Avoid breaking the user gesture

Start the host request immediately inside the click handler:

```ts
button.addEventListener('click', async () => {
  await requestCreateAccount();
});
```

Avoid unrelated asynchronous work before calling the SDK:

```ts
button.addEventListener('click', async () => {
  await fetch('/api/prepare-signup'); // Avoid doing this first.
  await requestCreateAccount();
});
```

***

### Use the Connected Account

After the account flow completes, the user has a registered Circles account. The following examples show common next steps.

#### Read a Circles profile

Profile data can be read from the Circles RPC endpoint:

```ts
const CIRCLES_RPC_URL = 'https://rpc.aboutcircles.com/';

type CirclesProfile = {
  name?: string;
  previewImageUrl?: string;
  [key: string]: unknown;
};

export async function fetchCirclesProfile(
  address: string,
): Promise<CirclesProfile | null> {
  const response = await fetch(CIRCLES_RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'circles_getProfileByAddress',
      params: [address],
    }),
  });

  if (!response.ok) {
    throw new Error(`Circles RPC request failed: ${response.status}`);
  }

  const payload = (await response.json()) as {
    result?: CirclesProfile | null;
    error?: { message?: string };
  };

  if (payload.error) {
    throw new Error(payload.error.message ?? 'Circles RPC returned an error.');
  }

  return payload.result ?? null;
}
```

A newly created account may not have a profile yet. In that case, `circles_getProfileByAddress` can return `null` even though the account is registered and usable.

#### Request transaction signing

Use `sendTransactions()` to ask the host to sign and submit one or more transactions with the user's Safe:

```ts
import { sendTransactions } from '@aboutcircles/miniapp-sdk';

const transactionHashes = await sendTransactions([
  {
    to: '0xRecipient',
    value: '0',
    data: '0x...',
  },
]);

console.log(transactionHashes);
```

The promise resolves with an array of transaction hashes and rejects if the user declines the request.

#### Request message signing

Use `signMessage()` when your app needs a user signature:

```ts
import { signMessage } from '@aboutcircles/miniapp-sdk';

const result = await signMessage('hello world');

console.log(result.signature);
console.log(result.verified);
```

The default signature type is `'erc1271'`, which uses EIP-191 prefix hashing. Pass `'raw'` only when your verifier expects raw UTF-8 byte signing:

```ts
const result = await signMessage('hello world', 'raw');
```

The two signature modes are not interchangeable. Match the SDK option to the verification logic in your application.

***

### Test the Integration Inside the Host

Serve your mini app locally:

```bash
npm run dev
```

For the repository demo, the local URL is:

```
http://localhost:5190
```

Open the development host playground and pass your local mini app URL:

```
https://circles-dev.gnosis.io/playground?url=http://localhost:5190
```

The playground embeds your app inside an iframe. Use the account button to run the real host-managed passkey and invitation flow.

#### Local testing checklist

Before debugging application logic, confirm that:

* Your app is loaded through the host playground rather than opened directly.
* The iframe URL matches your local development server.
* The account action starts from a button click.
* `isMiniappMode()` returns `true` inside the embedded app.
* Your `onWalletChange()` listener is registered when the app initializes.

***

### SDK API Reference

The following API snapshot corresponds to `@aboutcircles/miniapp-sdk@0.1.44`.

| Export                 | Signature                                                                                                    | Purpose                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `isMiniappMode`        | `() => boolean`                                                                                              | Returns `true` when the app is running inside the host iframe.                                         |
| `onWalletChange`       | `(callback: (address: string \| null) => void) => () => void`                                                | Emits the current wallet state immediately and emits future changes. Returns an unsubscribe callback.  |
| `requestCreateAccount` | `() => Promise<{ authenticated: boolean; address: string }>`                                                 | Opens the host-managed account creation or login flow. Call it directly from a user gesture.           |
| `sendTransactions`     | `(transactions: Transaction[]) => Promise<string[]>`                                                         | Requests host signing and submission of transactions with the user's Safe. Returns transaction hashes. |
| `signMessage`          | `(message: string, signatureType?: 'erc1271' \| 'raw') => Promise<{ signature: string; verified: boolean }>` | Requests a host-managed signature. Defaults to `'erc1271'`.                                            |
| `onAppData`            | `(callback: (data: string) => void) => void`                                                                 | Receives app-specific data passed by the host through a `?data=` parameter.                            |

#### Type definitions

```ts
interface Transaction {
  to: string;
  data?: string;
  value?: string;
}

interface AuthResult {
  authenticated: boolean;
  address: string;
}

interface SignResult {
  signature: string;
  verified: boolean;
}

type SignatureType = 'erc1271' | 'raw';
```

***

### Troubleshooting

| Symptom                                                         | Likely cause                                                                                                         | Resolution                                                                                                       |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| The passkey prompt does not open or is blocked.                 | `requestCreateAccount()` was not called directly from a user gesture.                                                | Move the SDK call into the button click handler. Avoid unrelated `await` calls before it.                        |
| The sign-up button does nothing.                                | The app was opened as a standalone page, so no host can answer the SDK request.                                      | Load the app through the host playground and check `isMiniappMode()`.                                            |
| `onWalletChange()` remains `null`.                              | The host has not returned a connected wallet state, the app is not embedded correctly, or the host URL is incorrect. | Verify the playground URL, confirm iframe embedding, and render an appropriate disconnected state while waiting. |
| The sign-up request succeeds but profile lookup returns `null`. | Newly registered accounts may not have profile metadata yet.                                                         | Treat the account as usable and render an empty-profile state.                                                   |
| Users can start duplicate requests.                             | The connect button remains enabled during the host flow.                                                             | Disable the button until the promise resolves or rejects.                                                        |

***

### Advanced: Raw `postMessage` Protocol

Prefer the SDK for application code. It centralizes the host protocol and reduces the amount of browser messaging logic you need to maintain.

For debugging or protocol-level understanding, the account exchange uses a request identifier so that the mini app can match the response to the original request:

```
Mini app → host
{ type: 'request_create_account', requestId }

Host → mini app
{ type: 'auth_success', address, requestId }

or

{ type: 'auth_rejected', reason, requestId }
```

A minimal illustrative implementation looks like this:

```ts
function createAccountWithPostMessage(): Promise<string> {
  return new Promise((resolve, reject) => {
    const requestId = `req_${Math.random().toString(36).slice(2)}`;

    function cleanup() {
      window.removeEventListener('message', onMessage);
    }

    function onMessage(event: MessageEvent) {
      const data = event.data;

      if (!data || data.requestId !== requestId) return;

      if (data.type === 'auth_success') {
        cleanup();
        resolve(data.address);
      }

      if (data.type === 'auth_rejected') {
        cleanup();
        reject(new Error(data.reason));
      }
    }

    window.addEventListener('message', onMessage);

    window.parent.postMessage(
      {
        type: 'request_create_account',
        requestId,
      },
      '*',
    );
  });
}
```

This example is intentionally minimal. For production code, use the SDK rather than reimplementing the protocol.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aboutcircles.com/miniapps/create-or-connect-a-circles-account-from-a-mini-app.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
