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

# Passkey

> WebAuthn / secp256r1 signing on a smart contract account - the private key never leaves the authenticator.

The passkey signer lets users authenticate with their device's built-in WebAuthn authenticator
(Touch ID, Face ID, Windows Hello, a hardware security key). The private key never leaves the
authenticator; the Soroban contract's `__check_auth` verifies the secp256r1 signature on-chain.

## When to use

Choose `passkey` for the **hero new-user flow**: no seed phrase, no existing Stellar wallet, no
XLM balance required. The signer must be paired with a contract account (`ozContractAccount`)
because the secp256r1 verification runs inside the contract, not at the network layer.

## Factory

```ts theme={null}
import { passkey } from "@buckspay/signers/passkey";

const signer = passkey({ rpId: "app.example.com", rpName: "My App" });
```

`passkey(opts)` accepts a `PasskeyOptions` object:

| Option   | Type     | Required | Description                                                   |
| -------- | -------- | -------- | ------------------------------------------------------------- |
| `rpId`   | `string` | Yes      | Relying party domain (must match the page's effective domain) |
| `rpName` | `string` | No       | Human-readable name shown in the authenticator prompt         |

<Note>
  `rpId` is bound permanently to the credential at registration time. If you change it, existing
  users cannot authenticate - their credential is tied to the original domain. Use
  `window.location.hostname` for consistency, or a stable parent domain for cross-subdomain apps.
  This is the WebAuthn anti-phishing mechanism: a credential registered at `app.example.com`
  cannot be used on `evil.example.net`.
</Note>

## Required account type

`passkey` must be paired with `ozContractAccount`. The contract's `__check_auth` method
verifies the WebAuthn signature on-chain; a classic `G...` account cannot accept secp256r1
signatures.

```ts theme={null}
import { ozContractAccount } from "@buckspay/accounts/oz-contract";
import { passkey } from "@buckspay/signers/passkey";

const account = ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G });
const signer  = passkey({ rpId: "app.example.com", rpName: "My App" });
```

`sponsorAddress` is the facilitator's public `G...` address, needed to derive the deterministic
`C...` address offline before the contract is deployed.

## Example

The snippet below shows atomic batch transfers using `passkey` and `ozContractAccount`. A batch
of one call is equivalent to `pay([call])`.

```ts theme={null}
// Recipe 10 - ATOMIC BATCH (sendCalls). N USDC transfers settle all-or-nothing in ONE tx via the
// pinned Multicall router's `batch_transfer` - the smart account (or classic wallet) authorizes the
// whole batch with a SINGLE signature. A batch of 1 is exactly pay([call]).
import {
  createBuckspayClient,
  batch,
  MAX_BATCH_CALLS,
  BuckspayError,
  type BuckspayConfig
} from "@buckspay/core";
import { ozContractAccount } from "@buckspay/accounts/oz-contract";
import { passkey } from "@buckspay/signers/passkey";
import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator";

const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const A = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
const B = "GBPYQYRH62E6NLRGXHBT4I3ZPTEHVBQMYTSH44YLOAPTCDYNAXDOLJRY";

export const batchConfig: BuckspayConfig = {
  network: "testnet",
  account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }),
  signer: passkey({ rpId: "localhost", rpName: "buckspay" }),
  relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }),
  gas: { mode: "sponsored" }
};

export const client = createBuckspayClient(batchConfig);

export async function payManyAtomically(): Promise<void> {
  await client.connect();
  // Collect calls with the pure builder (enforces MAX_BATCH_CALLS on build()).
  const calls = batch()
    .add(client.transfer({ token: USDC_SAC, to: A, amount: "1.00" }))
    .add(client.transfer({ token: USDC_SAC, to: B, amount: "2.50" }))
    .build();

  // sendCalls = atomic, all-or-nothing. Either BOTH transfers land or NEITHER does.
  const receipt = await client.sendCalls(calls);
  console.log(receipt.transferTx); // one settlement tx for the whole batch
}

export async function guardOversizeBatch(): Promise<void> {
  const tooMany = Array.from({ length: MAX_BATCH_CALLS + 1 }, () =>
    client.transfer({ token: USDC_SAC, to: A, amount: "0.01" })
  );
  try {
    await client.sendCalls(tooMany);
  } catch (e) {
    if (e instanceof BuckspayError && e.code === "BATCH_TOO_LARGE") {
      console.error(`batch capped at ${MAX_BATCH_CALLS} calls`);
      return;
    }
    throw e;
  }
}
```

## Next

<CardGroup cols={2}>
  <Card title="Native passkey" href="/signers/native-passkey">
    The same secp256r1 crypto on iOS and Android via the secure enclave.
  </Card>

  <Card title="Account models" href="/concepts/account-models">
    When to use ozContractAccount vs. classicAccount.
  </Card>
</CardGroup>
