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

# Sign in with Starknet

> Let users sign in with the wallet they already have (Braavos, Ready, Bramble): a SNIP-12 message, a one-time nonce, and a check by the account itself. No email, no password.

Starknet users already hold a key they trust. **Sign in with Starknet** (SIWS) turns that key into a login: your server issues a one-time nonce, the user's wallet signs a SNIP-12 message containing it, and your server asks the account contract whether the signature is valid.

`@chipi-stack/backend` (14.16.0+) ships both halves:

* `createStarknetSignInTypedData` builds the message.
* `verifyStarknetSignIn` checks it: chain, domain, URI, nonce, expiry, and the signature.

It works with deployed accounts (any wallet, through the account's own `is_valid_signature`) and with **accounts that are not deployed yet**, which is how new Bramble and Ready accounts start out.

## 1. Issue the message (server)

Store the nonce with a short expiry, then return the typed data to the browser.

```ts theme={null}
import { createStarknetSignInTypedData } from "@chipi-stack/backend";

const nonce = crypto.randomUUID().replaceAll("-", "");
const now = Math.floor(Date.now() / 1000);

const typedData = createStarknetSignInTypedData({
  domain: "app.example.com",
  uri: "https://app.example.com",
  address: "0x0123", // the address the wallet reported
  nonce,
  issuedAt: now,
  expirationTime: now + 600,
});
```

The builder sticks to what every wallet accepts: SNIP-12 revision 1, `SN_MAIN` in the domain, and single-line printable ASCII strings. It throws on anything else, because some wallets (Bramble among them) refuse to sign it.

## 2. Sign it (browser)

Any Starknet wallet signs it through the standard wallet API:

```ts theme={null}
declare const starknetWallet: {
  request(call: { type: string; params?: unknown }): Promise<unknown>;
};
declare const typedData: unknown;

const signature = (await starknetWallet.request({
  type: "wallet_signTypedData",
  params: typedData,
})) as string[];
```

If the server answers `ACCOUNT_NOT_DEPLOYED`, also send the wallet's deployment data:

```ts theme={null}
declare const starknetWallet: {
  request(call: { type: string; params?: unknown }): Promise<unknown>;
};

const deploymentData = await starknetWallet.request({ type: "wallet_deploymentData" });
```

## 3. Verify (server)

Consume the nonce (once, atomically), then verify:

```ts theme={null}
import { verifyStarknetSignIn, type StarknetDeploymentData } from "@chipi-stack/backend";
import { RpcProvider, type TypedData } from "starknet";

declare const typedData: TypedData;
declare const signature: string[];
declare const nonce: string;
declare const deploymentData: StarknetDeploymentData | undefined;

const provider = new RpcProvider({ nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" });

const result = await verifyStarknetSignIn({
  typedData,
  signature,
  provider,
  expected: { domain: "app.example.com", uri: "https://app.example.com", nonce },
  deploymentData,
});

if (result.ok) {
  // result.address: 0x + 64 hex. Sign the user in, or link the address to the current user.
} else {
  // result.reason: WRONG_NONCE, EXPIRED, INVALID_SIGNATURE, ACCOUNT_NOT_DEPLOYED, ...
}
```

What it checks:

| Check | Fails with |
| - | - |
| Domain chain is `SN_MAIN` | `WRONG_CHAIN` |
| `domain`, `uri`, `nonce` are the ones you issued | `WRONG_DOMAIN`, `WRONG_URI`, `WRONG_NONCE` |
| Not expired, not issued in the future (60 s skew) | `EXPIRED`, `NOT_YET_VALID` |
| Deployed account: `is_valid_signature` returns `'VALID'` | `INVALID_SIGNATURE` |
| Undeployed account with no `deploymentData` | `ACCOUNT_NOT_DEPLOYED` |
| Undeployed account: known class, address matches its constructor, owner key signed | `UNSUPPORTED_ACCOUNT_CLASS`, `ADDRESS_MISMATCH`, `INVALID_SIGNATURE` |

The typed data is rebuilt from its message before hashing, so a client can't swap in extra types or a testnet domain. Only `'VALID'` counts as valid: accounts answer `0` or revert on a bad signature, and a looser "anything non-zero" check would accept garbage.

Undeployed accounts are supported for **OpenZeppelin 3.0** (Bramble's default account) and **Argent 0.4 without a guardian** (Ready). Other undeployed accounts get `UNSUPPORTED_ACCOUNT_CLASS`: ask the user to make one transaction first, which deploys the account.

## Lower-level helpers

* `isValidStarknetSignature(provider, address, hash, signature)` asks any deployed account about any message hash.
* `verifyUndeployedStarknetSignature(deploymentData, hash, signature)` does the off-chain check on its own.

## Related

* [Passkeys](/sdk/guides/passkeys)
* [Connect with Chipi](/sdk/connector/overview)

> ✅ Verified against the SDK source on **2026-10-10**.


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