> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Invite Codes

> Handle Mainnet invite mode in your frontend: sign users in with an invite code and let them share their own.

Mainnet runs in **invite mode**: a wallet signs in only once it has Mainnet access, and a wallet without access gets it by redeeming an invite code at sign-in. Every wallet with access can mint its own codes to share. This page shows how to build both halves into your frontend: the code-entry step at sign-in, and an "invite friends" panel.

Invite mode is a per-deployment switch. Do not hard-code whether it is on: sign in as usual and branch on the refusal code, so the same frontend works whether invite mode is on or off.

## Surface Map

| Surface | Use |
| - | - |
| REST | `POST /api/v1/auth/verify` (`inviteCode` in the body), `POST /api/v1/invite-codes/requests`, `GET /api/v1/invite-codes` |
| TypeScript SDK | `sdk.login(clientId, { inviteCode })`, `sdk.auth.verifySignature(..., inviteCode)`, `sdk.auth.requestInviteCodes()`, `sdk.auth.listInviteCodes()`, `INVITE_ERROR_CODES` from `@0xmonaco/types` |
| React SDK | `useAuth()`: `login({ inviteCode })`, `requestInviteCodes()`, `listInviteCodes()` |
| WebSocket | None |
| gRPC | `AuthService.Verify` (`invite_code`), `AuthService.RequestInviteCodes`, `AuthService.ListInviteCodes` |

## Access Rules

* **A wallet with Mainnet access never needs a code.** Access is lasting: once a wallet has redeemed a code it keeps signing in without one, and only deactivating the wallet blocks it. A wallet that signed in while invite mode was off also has access.
* **A wallet without access needs a code, existing accounts included.** An account that never received access is refused like a new wallet.
* **A code is only redeemed when it is needed.** For a wallet that already has access, a code passed to sign-in is neither checked nor redeemed. For a wallet without access while invite mode is off, the code is not redeemed (the sign-in grants access on its own), but it still goes through the invite-attempt checks: it can be refused with `429` while this wallet or client address is in its cooldown, or `503` when codes cannot be validated right now.
* **A code redeems once.** It is a bearer secret: whoever signs in with it first gets access, and it is then spent.

## Sign-In With a Code

Sign in without a code first. If the wallet has no access, the sign-in is refused with `403` and the stable code `INVITE_CODE_REQUIRED`, read from `APIError.serverCode`. Show a code-entry step and sign in again with the code.

```typescript theme={null}
import { APIError } from "@0xmonaco/core";
import { INVITE_ERROR_CODES } from "@0xmonaco/types";

const INVITE_CODE_FORMAT = /^[A-Za-z0-9]{10}$/;

async function signIn(inviteCode?: string) {
  try {
    return { status: "signed-in", auth: await sdk.login(clientId, inviteCode ? { inviteCode } : undefined) } as const;
  } catch (error) {
    if (!(error instanceof APIError)) throw error;
    switch (error.serverCode) {
      case INVITE_ERROR_CODES.INVITE_CODE_REQUIRED:
        return { status: "needs-code" } as const;
      case INVITE_ERROR_CODES.INVITE_CODE_INVALID:
        return { status: "code-rejected" } as const;
      case INVITE_ERROR_CODES.ACCOUNT_DEACTIVATED:
        return { status: "deactivated" } as const;
    }
    if (error.statusCode === 429) return { status: "cooldown", retryAfter: error.retryAfter } as const;
    if (error.statusCode === 503) return { status: "retry-later" } as const;
    throw error;
  }
}

// After "needs-code": validate locally before spending an attempt
if (!INVITE_CODE_FORMAT.test(codeInput)) {
  // show "Invite codes are 10 letters or digits"
} else {
  const result = await signIn(codeInput);
}
```

Each `login` call creates a new challenge and asks the wallet to sign it again, so a code retry is a second wallet prompt. If the user arrived with a code (for example from a `?invite=` link your app defines), you can pass it on the first sign-in to save that prompt; a wallet that already has access skips the code entirely. A wallet without access sends the code through the invite-attempt checks even while invite mode is off, so on a `429` or `503` from that first attempt, retry without the code before showing the refusal.

### Refusals to Handle

| Outcome | Meaning | What to show |
| - | - | - |
| `403` `INVITE_CODE_REQUIRED` | No access and no code was passed | The code-entry step |
| `403` `INVITE_CODE_INVALID` | The code is wrong, malformed, already redeemed or revoked, or unused and its issuer is deactivated. The reasons are deliberately indistinguishable | "That code didn't work"; let the user try another |
| `429` | Repeated failed attempts from this wallet or client address are in a cooldown | Wait `APIError.retryAfter` seconds (also `details.retryAfter` in the response body) before allowing another attempt |
| `503` | The code could not be validated right now. Not a verdict on the code | Retry shortly |
| `403` `ACCOUNT_DEACTIVATED` | The signing account is deactivated. A code does not help | A blocked-account message; do not offer code entry |

Codes are exactly 10 case-sensitive letters or digits. Check the format before submitting, and never change the case of what the user typed: a malformed code is refused as `INVITE_CODE_INVALID` and counts toward the cooldown like a wrong one.

### React

`useAuth().login` takes the same `inviteCode` option, reading `clientId` from the provider. The refusal is the same `APIError`:

```tsx theme={null}
import { APIError } from "@0xmonaco/core";
import { useAuth } from "@0xmonaco/react";
import { INVITE_ERROR_CODES } from "@0xmonaco/types";
import { useState } from "react";

function SignIn() {
  const { login, authenticationStatus } = useAuth();
  const [needsCode, setNeedsCode] = useState(false);
  const [code, setCode] = useState("");
  const [message, setMessage] = useState<string>();

  const submit = async () => {
    try {
      await login(needsCode ? { inviteCode: code } : undefined);
    } catch (error) {
      if (error instanceof APIError && error.serverCode === INVITE_ERROR_CODES.INVITE_CODE_REQUIRED) {
        setNeedsCode(true);
      } else if (error instanceof APIError && error.serverCode === INVITE_ERROR_CODES.INVITE_CODE_INVALID) {
        setMessage("That invite code didn't work.");
      } else if (error instanceof APIError && error.statusCode === 429) {
        setMessage(`Too many attempts. Try again in ${error.retryAfter ?? 60}s.`);
      } else {
        throw error;
      }
    }
  };

  return (
    <div>
      {needsCode && <input value={code} onChange={(e) => setCode(e.target.value.trim())} maxLength={10} placeholder="Invite code" />}
      <button onClick={submit} disabled={authenticationStatus === "authenticating"}>
        {needsCode ? "Sign in with code" : "Sign in"}
      </button>
      {message && <p>{message}</p>}
    </div>
  );
}
```

## Share Invite Codes

A signed-in wallet with Mainnet access has an allowance of codes: one initial code, plus one more per 250,000 USDC of Mainnet volume. Two signed calls back an "invite friends" panel:

* **`listInviteCodes()`** reads the allowance and the codes the wallet minted, newest first. It never mints a code or uses a slot, so call it on page load and poll it to show progress toward the next code. It also recovers a code whose mint response was lost.
* **`requestInviteCodes()`** mints one code and uses up a slot. Call it only when the user asks for a new code, never on page load. When the allowance is used up it returns no `code`.

```typescript theme={null}
const { availableCodes, totalAllowance, mainnetVolumeUsd, codes } = await sdk.auth.listInviteCodes();

// Codes ready to share: status "issued" still carries the code
const shareable = codes.filter((entry) => entry.status === "issued" && entry.code);
const redeemed = codes.filter((entry) => entry.status === "redeemed");

async function onMintClick() {
  if (availableCodes === 0) return;
  const minted = await sdk.auth.requestInviteCodes();
  if (minted.code) {
    // show minted.code with a copy button
  }
}
```

In React, `useAuth()` returns the same `listInviteCodes` and `requestInviteCodes`.

* An entry's `status` is `issued`, `redeemed` or `revoked`. Only an `issued` entry carries its `code`; a redeemed or revoked one shows its `milestone` and dates only.
* `milestone` `0` is the initial code; milestone N is the Nth 250,000 USDC of volume.
* A revoked code never refunds its slot.
* `mainnetVolumeUsd` is a decimal string. Format it for display; do not parse it as a float for comparisons.

Both calls refuse a delegated agent's session with `403`: they act for the owner wallet only. `requestInviteCodes` also needs the wallet to have active Mainnet access. Both return `503` when invite codes are temporarily unavailable; retry. See [Mainnet Invite Codes](https://docs.0xmonaco.com/sdk/typescript/auth.md#mainnet-invite-codes) for the full response shapes and error list.

## Handling Codes Safely

* A code is a bearer secret until it is redeemed. Never log it, send it to analytics, or include it in error reports, and do not store the codes from `listInviteCodes` beyond the session: read them again when you need them.
* Do not put a code in a URL your app sends to third parties (referrer headers, link previews). If you support invite links, read the code from the link on arrival and drop it from the address bar.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.