Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"efgh",
"erigon",
"están",
"EURC",
"Ethereum",
"ethersproject",
"fdir",
Expand Down Expand Up @@ -167,6 +168,7 @@
"unlinkable",
"unpackedProof",
"urlencode",
"USDCE",
"userop",
"UUPS",
"uvwx",
Expand All @@ -176,7 +178,12 @@
"vuni",
"wagmi",
"walletauth",
"WBRL",
"WCLP",
"WCOP",
"wcsep",
"WMXN",
"WPEN",
"webview",
"worldapp",
"worldchain",
Expand Down
23 changes: 18 additions & 5 deletions mini-apps/commands/pay.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ description: "Request a payment from the user"

This command is an abstraction for a simple transfer. This shouldn't be used outside of World App. Pay supports WLD and all local stablecoins.

## Supported Tokens

Import the `Tokens` enum instead of hardcoding symbol strings. Only `Tokens.USDC` has a value that differs from its name (`"USDCE"`):

| `Tokens` member | Enum value | Currency |
| --- | --- | --- |
| `Tokens.WLD` | `"WLD"` | Worldcoin |
| `Tokens.USDC` | `"USDCE"` | USD Coin |
| `Tokens.EURC` | `"EURC"` | Euro Coin |
| `Tokens.WARS` | `"WARS"` | Argentine Peso stablecoin |
| `Tokens.WCOP` | `"WCOP"` | Colombian Peso stablecoin |
| `Tokens.WMXN` | `"WMXN"` | Mexican Peso stablecoin |
| `Tokens.WBRL` | `"WBRL"` | Brazilian Real stablecoin |
| `Tokens.WPEN` | `"WPEN"` | Peruvian Sol stablecoin |
| `Tokens.WCLP` | `"WCLP"` | Chilean Peso stablecoin |

## Basic Usage

<CodeGroup>
Expand All @@ -15,9 +31,6 @@ import { MiniKit } from "@worldcoin/minikit-js";
import {
Tokens,
tokenToDecimals,
type CommandResultByVia,
type MiniKitPayOptions,
type PayResult,
} from "@worldcoin/minikit-js/commands";

export async function sendPayment() {
Expand All @@ -40,8 +53,7 @@ export async function sendPayment() {
},
};

const result: CommandResultByVia<PayResult, PayResult, "minikit"> =
await MiniKit.pay(input);
const result = await MiniKit.pay(input);

await fetch("/api/confirm-payment", {
method: "POST",
Expand All @@ -62,6 +74,7 @@ type MiniKitPayOptions = {
token_amount: string;
}[];
description: string;
network?: Network; // Optional; currently only "worldchain" and defaults to World Chain
fallback?: () => unknown;
};
```
Expand Down
4 changes: 4 additions & 0 deletions mini-apps/commands/request-permission.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Define a custom fallback in the command payload for support outside mini apps.

## Error Codes

The SDK's `RequestPermissionErrorCodes` enum defines these errors:

| Code | Meaning |
| --- | --- |
| `user_rejected` | The user rejected the request |
Expand All @@ -90,6 +92,8 @@ Define a custom fallback in the command payload for support outside mini apps.
| `already_granted` | The permission is already granted |
| `unsupported_permission` | The permission is not supported |

For microphone access, `permission_disabled` or `world_app_permission_not_enabled` means the user must first enable the microphone for World App; see the [microphone guide](/mini-apps/reference/microphone). The SDK types `world_app_permission_not_enabled` and `mini_app_permission_not_enabled` on the separate `MiniAppMicrophone` response event, not in `RequestPermissionErrorCodes`.

## Preview

<div className="grid justify-items-center text-center">
Expand Down
63 changes: 51 additions & 12 deletions mini-apps/commands/send-transaction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ submitted, so the first identifier you receive is `userOpHash`.
You can either use the `@worldcoin/minikit-react` hook or poll the Developer Portal
to check when the user operation is mined and get the final `transaction_hash`.

<Warning>
`poll()` rejects with `Error("Transaction failed")` only when the user operation's status is `failed`. Other rejections don't mean the transaction failed: a status request failed (HTTP or network error), the receipt wait failed or timed out, or polling stopped with an `AbortError` because you called `reset()`, the component unmounted, or a new `poll()` started. Status polling has no overall timeout (an unknown hash stays `pending`); the hook's `timeout` option only bounds the receipt wait once a transaction hash exists. Call `reset()` to stop polling.
</Warning>

<CodeGroup>
```tsx title="React"
import { useUserOperationReceipt } from "@worldcoin/minikit-react";
Expand All @@ -188,28 +192,63 @@ const { poll, isLoading, reset } = useUserOperationReceipt({ client });

const onClick = async () => {
const result = await MiniKit.sendTransaction({...});
const { receipt } = await poll(result.data.userOpHash);
// receipt contains the final transaction receipt

try {
const { receipt } = await poll(result.data.userOpHash);
// receipt contains the final transaction receipt
} catch (error) {
if (error instanceof Error && error.message === "Transaction failed") {
// Only this error means the user operation failed
console.error("User operation failed");
} else if (error instanceof Error && error.name === "AbortError") {
// Stopped by reset(), unmount, or a newer poll(); not a failure
} else {
// A status or receipt lookup failed; the outcome is unknown, so poll again
console.error("Could not confirm the user operation:", error);
}
}
};
```

```ts title="API"
type UserOperationStatusSuccess = {
status: "success";
userOpHash: string;
sender: string;
transaction_hash: string;
nonce: string;
};
type UserOperationStatus =
| {
status: "pending";
userOpHash: string;
sender: null;
transaction_hash: null;
nonce: null;
}
| {
status: "success";
userOpHash: string;
sender: string;
transaction_hash: string;
nonce: string;
}
| {
status: "failed";
userOpHash: string;
sender: string | null;
transaction_hash: string | null;
nonce: string | null;
};
Comment on lines +229 to +235

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the failed response to the documented API schema

For a failed response, the repository's authoritative GetUserOperationResponse schema and example in openapi/developer-portal.json:263-270 return sender, transaction_hash, and nonce, but no error property. Consequently the new failure branch always logs undefined for the diagnostic and the displayed type omits fields callers actually receive. Model the common nullable response fields from the OpenAPI schema instead of inventing error.

Useful? React with 👍 / 👎.


const response = await fetch(
`https://developer.world.org/api/v2/minikit/userop/${userOpHash}`,
);
const status = await response.json();

// The endpoint also returns 400/500 on a bad request or server error.
if (!response.ok) {
throw new Error(`Failed to fetch user operation status: ${response.status}`);
}

const status: UserOperationStatus = await response.json();

if (status.status === "success") {
const success = status as UserOperationStatusSuccess;
console.log(success.transaction_hash);
console.log(status.transaction_hash);
} else if (status.status === "failed") {
console.error("User operation failed:", status.userOpHash);
}
```
</CodeGroup>
Expand Down
2 changes: 1 addition & 1 deletion mini-apps/commands/share-contacts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Open the native contact picker using the unified MiniKit API."
"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
---

Use `MiniKit.shareContacts()` to open the World App contact picker.
Use `MiniKit.shareContacts()` to open the World App contact picker. The `getContacts` export from `@worldcoin/minikit-js/commands` is a legacy alias; call `MiniKit.shareContacts()` instead, since the standalone export never receives World App's response.

## Basic Usage

Expand Down
11 changes: 7 additions & 4 deletions mini-apps/commands/wallet-auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type WalletAuthResponse =
address: string;
message: string;
signature: string;
version?: number; // Command protocol version (present when returned from World App)
};
}
| {
Expand All @@ -91,15 +92,16 @@ type WalletAuthResponse =
"data": {
"address": "0x1234567890123456789012345678901234567890",
"message": "example.com wants you to sign in with your Ethereum account",
"signature": "0xabcdef1234567890"
"signature": "0xabcdef1234567890",
"version": 2
}
}
```
</CodeGroup>

## Backend Verification

Always verify the returned SIWE payload on your backend.
Always verify the returned SIWE payload on your backend. This example targets Next.js 15+, where `cookies()` returns a `Promise`.

```ts
import { cookies } from "next/headers";
Expand All @@ -115,7 +117,8 @@ type RequestBody = {
export async function POST(req: NextRequest) {
const { payload, nonce } = (await req.json()) as RequestBody;

if (nonce !== cookies().get("siwe")?.value) {
const cookieStore = await cookies();
if (nonce !== cookieStore.get("siwe")?.value) {
return NextResponse.json(
{ isValid: false, error: "Invalid nonce" },
{ status: 400 },
Expand Down Expand Up @@ -157,7 +160,7 @@ export async function POST(req: NextRequest) {
| `expirationTime` | `Date` | No | When the SIWE message expires |
| `notBefore` | `Date` | No | SIWE message is not valid before this time |
| `requestId` | `string` | No | Arbitrary ID for correlating the request on your backend |
| `fallback` | `() => Promise<T>` | No | Custom fallback for non-World-App environments |
| `fallback` | `() => unknown` | No | Custom fallback for non-World-App environments (sync or async) |

## Notes

Expand Down
46 changes: 39 additions & 7 deletions mini-apps/growth/invites-viral.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ Referral programs are proven growth drivers because they leverage trust. People

Create server-side invite pages that work across all platforms:

```typescript
```tsx
// pages/invite.tsx
import { useEffect } from "react";

const appId = process.env.NEXT_PUBLIC_APP_ID;

export default function InvitePage({ code }: { code: string }) {
useEffect(() => {
// Redirect to mini app
window.location.href = `https://world.org/mini-app?app_id=${YOUR_APP_ID}&path=/invite?code=${code}`
window.location.href = `https://world.org/mini-app?app_id=${appId}&path=${encodeURIComponent(`/invite?code=${code}`)}`
}, [code])

return <div>Redirecting to mini app...</div>
Expand All @@ -34,7 +38,7 @@ Deep-link (opens World App directly if installed)

`worldapp://mini-app?app_id={app_id}&path={path}`

To force opening in the device browser instead of the native webview, append `open_out_of_window=true` to the URL (works for both universal and deep links).
To open a link from your mini app in the device browser instead of World App, add `open_out_of_window=true` to its URL, e.g. `window.open("https://example.com/?open_out_of_window=true", "_blank")`.

Note: path should be URL encoded.
### 2. Generate Share Links
Expand All @@ -53,7 +57,7 @@ function generateInviteLink(userId: string): string {

### 3. Implement Share Functionality

Add share buttons at key moments in your user journey:
Add share buttons at key moments in your user journey. `trackEvent` below is a placeholder for your own analytics tool (Segment, PostHog, etc.), not a MiniKit command.

```typescript
import { MiniKit } from "@worldcoin/minikit-js";
Expand Down Expand Up @@ -87,7 +91,7 @@ Process referral codes when new users sign up:
// On app initialization
function handleReferral() {
const urlParams = new URLSearchParams(window.location.search);
const refCode = urlParams.get("ref");
const refCode = urlParams.get("code");

if (refCode && !currentUser.referredBy) {
// Credit the referrer
Expand All @@ -107,28 +111,52 @@ function handleReferral() {

Implement two-sided rewards that benefit both parties:

Requires a World-ID-verified user (an [IDKit](/world-id/idkit/integrate) proof, not [Wallet Auth](/mini-apps/commands/wallet-auth) — SIWE only proves wallet control, not personhood, so gating on it alone lets one person collect the bonus from many wallets). Never trust `newUserId`/`referrerCode` from an unauthenticated request, and claim the referral atomically so concurrent requests can't double-credit it:

```typescript
// api/process-referral.ts
export async function processReferral(data: {
newUserId: string;
referrerCode: string;
}) {
// Require a stored World ID nullifier for newUserId, set when they verified
// with IDKit — not a Wallet Auth session, which only proves wallet control.
if (!(await hasVerifiedWorldId(data.newUserId))) {
return { success: false, reason: "unverified_user" };
}

const referrer = await getUserByCode(data.referrerCode);
if (!referrer) {
return { success: false, reason: "invalid_referrer" };
}

// Credit both users
if (referrer.id === data.newUserId) {
return { success: false, reason: "self_referral" };
}

// Atomically claim the referral (e.g. INSERT ... ON CONFLICT DO NOTHING on
// a unique referred_user_id column), then read back the stored referrer. If
// two requests race, only one INSERT succeeds.
const claimedBy = await claimReferral(data.newUserId, referrer.id);
if (claimedBy !== referrer.id) {
return { success: false, reason: "already_referred" };
}

// Credit both users. Each credit has its own idempotency key (unique in your
// credits table; treat a duplicate as success), so retrying processReferral
// after a partial failure writes only the missing credit.
await Promise.all([
creditUser(referrer.id, {
type: "referral_bonus",
amount: 100,
reason: "Friend joined via your invite",
idempotencyKey: `referral:${data.newUserId}:referrer`,
}),
creditUser(data.newUserId, {
type: "signup_bonus",
amount: 50,
reason: "Welcome bonus for joining via invite",
idempotencyKey: `referral:${data.newUserId}:referee`,
}),
]);

Expand Down Expand Up @@ -200,6 +228,10 @@ const events = {
};
```

<Note>
If you use the canonical [Core Event Set](/mini-apps/growth/analytics#2-%C2%B7-core-event-set-6-lines-of-code), `invite_link_created` maps to `invite_sent` and `signup_source_invite` maps to `invite_accepted`; `invite_link_clicked` has no equivalent there. Pick one taxonomy and use it consistently.
</Note>

### Key Metrics Dashboard

- **Invite Conversion Rate**: (Signups from invites) / (Total invite links clicked)
Expand All @@ -225,6 +257,6 @@ Test these variables to optimize your viral loop:

1. Implement universal links for your invite flow
2. Add share buttons after key user achievements
3. Set up two-sided rewards with World ID verification
3. Set up two-sided rewards with World ID verification, self-referral, and idempotency checks
4. Track invite metrics and run small A/B tests
5. Scale successful invite mechanics across more touchpoints
9 changes: 5 additions & 4 deletions mini-apps/growth/notifications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ Thoughtful, behavior‑based notifications keep users engaged long after they cl
| -------------------- | ------------------------------------------------------------------- |
| **Retention boost** | Targeted pushes can 2–3× day‑7 retention. |
| **Free visibility** | ≥ 15 % open rate unlocks a persistent badge on your app icon. |
| **Strict standards** | < 10 % open = delivery paused for 7 days—quality is non‑negotiable. |
| **Strict standards** | < 15 % open = no home‑screen badge—quality is non‑negotiable. |

### 2 · Quality Thresholds

| Open‑Rate (7‑day) | Platform Action | Your Next Step |
| ----------------- | ----------------- | --------------------------------- |
| **< 10%** | Paused for 1 week | Audit triggers & copy immediately |
| **10%+** | Badge displayed | Maintain & iterate |
| **< 15%** | No badge | Audit triggers & copy immediately |
| **15%+** | Badge displayed | Maintain & iterate |
| **25%+** | "Excellent" tier | Scale what works, test new ideas |

### 3 · Core Principles

1. **Trigger‑based > Broadcasts** – React to _user actions_ (wins, risks) instead of fixed schedules.
2. **Personalize** – Use `${username}` placeholder to personalize notifications with usernames.
3. **Copy rules** – ≤ 30‑char title, 1–2 emojis, clear value + curiosity gap.
3. **Copy rules** – ≤ 30‑char title, ≤ 200‑char message, 1–2 emojis, clear value + curiosity gap.

### 4 · Trigger Library

Expand All @@ -45,6 +45,7 @@ Thoughtful, behavior‑based notifications keep users engaged long after they cl

### 6 · Copy Cheatsheet

- **Limits**: title ≤ 30 characters (≤ 16 plus `${username}` if you use it), message ≤ 200 characters (API-enforced).
- **Lead with benefit**: "Earn 50 coins" beats "Check the app".
- **Curiosity**: "Something new awaits …".
- **Concrete numbers**: "30 s left" > "Hurry up".
Expand Down
Loading
Loading