> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.coinflow.cash/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.coinflow.cash/_mcp/server.

# Tempo Signature Integration (secp256k1 / P-256 / WebAuthn / Keychain V2)

> **Warning**
>
> **This page is for advanced / cryptocurrency-native companies.** If that's not you, head back to the [Quickstart](/guides/getting-started/quickstart) for the standard flows.

## Overview

Coinflow's Tempo credits-redemption path accepts four signature shapes
through the same `<CoinflowPurchase>` integration:

* **secp256k1** — the standard EVM curve used by MetaMask and any EIP-1193
  wallet.
* **P-256 (raw)** — secp256r1 / NIST-P-256 keys signing a digest directly,
  without the WebAuthn envelope. Useful for non-browser signers.
* **WebAuthn** — P-256 keys held in a passkey authenticator (browser, OS
  keychain, hardware key). Coinflow accepts root WebAuthn passkey signatures
  for credits-only redemptions, so users do not need a separate
  MetaMask-style secp256k1 wallet.
* **Keychain V2 access keys (`0x04`)** — an authorized access key signing on
  behalf of a root Tempo account, wrapped in a canonical Keychain V2
  envelope. The wallet must register the access key on-chain via Tempo's
  AccountKeychain precompile before signing — see [Tempo access-key wallets (Keychain V2)](#tempo-access-key-wallets-keychain-v2) below.

All four resolve through the same Coinflow checkout API. The merchant's
`<CoinflowPurchase>` integration stays the same regardless of which signer
the user holds — only the bytes inside the `permitCredits` field differ.

Your app owns the registration / sign-in UI and passes a compatible
`EthWallet` adapter into `<CoinflowPurchase>`. Coinflow does not create or
store keys or passkeys for the merchant.

> **Info**
>
> If you are also settling to a merchant contract on Tempo, your contract must
> be whitelisted first. See [Whitelist Your Contracts](/guides/checkout/settlement-locations/settlement-to-contracts/whitelist-your-contracts).

## What's supported

| Flow                                   | Supported on Tempo | Notes                                                                                                                                           |
| -------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| secp256k1 wallet redemption            | ✅                  | Works with any EIP-1193 wallet the same as on other EVM chains.                                                                                 |
| Raw P-256 (P256) wallet redemption     | ✅                  | Sign with a raw P-256 key via `viem/tempo` `Account.fromP256`.                                                                                  |
| WebAuthn / passkey wallet redemption   | ✅                  | Integrated through Tempo's `webAuthn` connector and a standard Coinflow `EthWallet` adapter.                                                    |
| Tempo access-key wallets (Keychain V2) | ✅                  | Inner signature wrapped in a canonical `0x04` envelope. Requires prior on-chain access-key registration via AccountKeychain. See section below. |
| Tempo access-key wallets (Keychain V1) | ❌                  | The `0x03` envelope was deprecated by the Tempo protocol post-T1C. Wallets must emit V2 (`0x04`).                                               |

## Tempo access-key wallets (Keychain V2)

Tempo lets a root account authorize a separate access key to sign on its
behalf, sparing users a root-passkey prompt on every transaction. Coinflow
accepts these access-key signatures when the wallet emits a canonical
Keychain V2 envelope:

```
0x04 || rootAccount(20 bytes) || innerSig(secp256k1 65B | P-256 130B | WebAuthn 129–2049B)
```

### Prerequisites

The wallet must complete the following BEFORE the customer reaches
checkout — Coinflow performs only the verification step:

1. **Authorize the access key on-chain.** The wallet submits a Tempo AA
   transaction (type `0x76`) carrying a `key_authorization` field, signed by
   the root account's secp256k1 / P-256 / WebAuthn key. The Tempo node
   writes the authorization into the AccountKeychain precompile at
   `0xaAAA…0000`. See Tempo's
   [AccountKeychain spec](https://docs.tempo.xyz/protocol/transactions/AccountKeychain)
   for the canonical registration flow.
2. **Sign with the access key.** The wallet signs the credits-auth digest
   with the registered access key, wraps the inner signature in the V2
   envelope, and returns the resulting bytes through `EthWallet.signMessage`
   as usual.
3. **Match the registered signature type.** The on-chain `signatureType`
   recorded for the access key (`0`=Secp256k1, `1`=P-256, `2`=WebAuthn) must
   match the inner signature's scheme. Mismatched types reject on-chain per
   Tempo's `validate_keychain_authorization` rule.

### Common failure: unregistered access key

If the wallet emits a V2 envelope without registering the access key
first, Coinflow's on-chain verification surfaces *"Invalid credits auth
signature"* to the user. The wallet team must either:

* Implement the `authorizeKey` AA-transaction registration flow above, or
* Downgrade to raw secp256k1 / `0x01` P-256 / `0x02` WebAuthn signatures,
  which require no AccountKeychain registration.

### Merchant integration impact

None. The merchant's `<CoinflowPurchase>` integration code stays
identical — the customer's wallet emits the V2 envelope, your `EthWallet`
adapter forwards the opaque bytes unchanged, and Coinflow's contract
performs the keychain composition (TIP-1020 inner-signature recovery
followed by AccountKeychain authorization lookup) on-chain. No new SDK
parameters and no new API fields.

## How it works

* **Your app** — hosts the registration / sign-in UI, manages the signer
  session, and exposes a standard `EthWallet` adapter to
  `<CoinflowPurchase>`. The adapter routes Coinflow's `signMessage` request
  to whichever signer the user chose (secp256k1, raw P-256, or WebAuthn).
  Nothing else in your Coinflow integration changes.
* **Coinflow** — forwards the opaque signature bytes to Tempo's on-chain
  signature-verification precompile, which accepts any of the three types.
  Coinflow does not need to know which type the user used.
* **Networks** — Coinflow supports Tempo mainnet and the Moderato testnet.
  Switch between them by changing the wagmi chain id — no other
  configuration.

## Constructing each signer with viem

The snippets below show the minimum viem / `viem/tempo` calls to construct
each signer type. In a real merchant integration, wrap the resulting
`Account` (or wagmi connection) inside an `EthWallet` adapter — see
[Adapt the wagmi session to an `EthWallet`](#3-adapt-the-wagmi-session-to-an-ethwallet-for-coinflowpurchase)
below for the WebAuthn path; the secp256k1 / P-256 paths follow the same
shape.

> **Warning**
>
> The snippets below generate ephemeral keys at runtime for illustration only.
> In production, derive keys from your secure key-management system and
> never commit private keys to source control.

### secp256k1

```ts
import {generatePrivateKey, privateKeyToAccount} from 'viem/accounts';
import {hashMessage, type Hex} from 'viem';

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);

const message = 'coinflow login challenge';
const digest = hashMessage(message) as Hex;
const signature = (await account.sign({hash: digest})) as Hex;
// 65-byte r||s||v signature ready to forward as `permitCredits`.
```

### Raw P-256

```ts
import {Account as TempoAccount, P256 as TempoP256} from 'viem/tempo';
import {hashMessage, type Hex} from 'viem';

const privateKey = TempoP256.randomPrivateKey();
const account = TempoAccount.fromP256(privateKey);

const message = 'coinflow login challenge';
const digest = hashMessage(message) as Hex;
const signature = (await account.sign({hash: digest})) as Hex;
// Raw P-256 envelope (typeId 0x01) ready to forward as `permitCredits`.
```

### WebAuthn (passkey)

For production browser flows, use Tempo's wagmi `webAuthn` connector with a
remote `KeyManager` (covered in detail below). For headless / non-browser
contexts and tests, `Account.fromHeadlessWebAuthn` constructs a WebAuthn
signer from a P-256 key:

```ts
import {Account as TempoAccount, P256 as TempoP256} from 'viem/tempo';
import {hashMessage, type Hex} from 'viem';

const privateKey = TempoP256.randomPrivateKey();
const account = TempoAccount.fromHeadlessWebAuthn(privateKey, {
  origin: 'https://example.com',
  rpId: 'example.com',
});

const message = 'coinflow login challenge';
const digest = hashMessage(message) as Hex;
const signature = (await account.sign({hash: digest})) as Hex;
// WebAuthn envelope (typeId 0x02) ready to forward as `permitCredits`.
```

## Merchant integration (WebAuthn passkey reference flow)

The walkthrough below targets the WebAuthn / passkey case, which has the
most setup. The secp256k1 and raw P-256 paths reuse the same
`<CoinflowPurchase>` integration; only the signer backing
`EthWallet.signMessage` differs. For secp256k1 or P-256, use any wagmi
connector (`injected`, `walletConnect`, or a custom one wrapping a
`viem/tempo` `Account`) and skip to
[Adapt the wagmi session to an `EthWallet`](#3-adapt-the-wagmi-session-to-an-ethwallet-for-coinflowpurchase).

### 1. Configure the Tempo `webAuthn` connector in your wagmi config

Register Tempo's WebAuthn connector alongside your existing EVM
connectors. Use a remote key manager in production. Use
`KeyManager.localStorage()` for demos only: it stores the credential /
public-key mapping in the browser, so clearing storage or switching
devices breaks lookup. (The passkey key material itself stays in the
authenticator.)

> **Warning**
>
> `KeyManager.localStorage()` is demo-only — ship a server-backed
> `KeyManager.http(url)` before production. The snippet below disables the
> connector entirely in production builds when `VITE_TEMPO_KEY_MANAGER_URL`
> is missing, preventing silent fallback to browser storage.

```tsx
// ContextWrapper.tsx
import {KeyManager, webAuthn} from '@wagmi/core/tempo';
import {createConfig, http, WagmiProvider} from 'wagmi';
import {
  tempo,
  tempoModerato /* ...plus your other chains */,
} from 'wagmi/chains';
import {injected, walletConnect} from 'wagmi/connectors';

const tempoKeyManagerUrl = import.meta.env.VITE_TEMPO_KEY_MANAGER_URL?.trim();

function createTempoWebAuthnConnector() {
  if (tempoKeyManagerUrl) {
    return webAuthn({
      keyManager: KeyManager.http(tempoKeyManagerUrl),
      createOptions: {label: 'My Merchant - Tempo passkey'},
    });
  }

  if (import.meta.env.PROD) {
    // Do not silently fall back to browser-local key lookup in production.
    // Disable passkeys or fail startup until VITE_TEMPO_KEY_MANAGER_URL is set.
    return null;
  }

  return webAuthn({
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    keyManager: KeyManager.localStorage(),
    createOptions: {label: 'My Merchant - Tempo passkey'},
  });
}

const tempoWebAuthnConnector = createTempoWebAuthnConnector();

const config = createConfig({
  chains: [tempo, tempoModerato /* ...plus your other chains */],
  connectors: tempoWebAuthnConnector
    ? [tempoWebAuthnConnector, injected(), walletConnect({projectId})]
    : [injected(), walletConnect({projectId})],
  transports: {
    [tempo.id]: http(),
    [tempoModerato.id]: http(),
    // ...other chains
  },
});

export const TEMPO_WEBAUTHN_CONNECTOR_TYPE = webAuthn.type; // 'webAuthn'

export function ContextWrapper({children}) {
  return (
    <WagmiProvider config={config}>
      {/* ...QueryClientProvider, your app tree... */}
      {children}
    </WagmiProvider>
  );
}
```

### 2. Drive sign-up / sign-in through wagmi

The connector accepts a `capabilities` argument on `connect()` that
selects between registering a new passkey (`sign-up`) and authenticating
with an existing one (`sign-in`). Pass the Tempo chain id when connecting
so the connector session targets the same Tempo network Coinflow uses.

```tsx
import {tempo, tempoModerato} from 'wagmi/chains';
import {useConnect, useConnections, useDisconnect} from 'wagmi';
import {TEMPO_WEBAUTHN_CONNECTOR_TYPE} from './ContextWrapper';

const tempoChainId = import.meta.env.PROD ? tempo.id : tempoModerato.id;

function TempoPasskeyGate({children}) {
  const connections = useConnections();
  const {connect, connectors, isPending, error} = useConnect();
  const {disconnect} = useDisconnect();

  const tempoAccount = connections.find(
    c => c.connector.type === TEMPO_WEBAUTHN_CONNECTOR_TYPE
  );
  const tempoConnector = connectors.find(
    c => c.type === TEMPO_WEBAUTHN_CONNECTOR_TYPE
  );

  // wagmi's public `ConnectVariables` type does not expose `capabilities`,
  // but the Tempo `webAuthn` connector consumes it at runtime. Scope the
  // cast to `Parameters<typeof connect>[0]` so a future wagmi rename still
  // fails type-checking at this call site.
  const connectWith = (type: 'sign-up' | 'sign-in') => {
    if (!tempoConnector) return;
    const variables = {
      connector: tempoConnector,
      chainId: tempoChainId,
      capabilities: {type},
    } as Parameters<typeof connect>[0];
    connect(variables);
  };

  if (!tempoAccount) {
    return (
      <div>
        <button disabled={isPending} onClick={() => connectWith('sign-up')}>
          Register new passkey
        </button>
        <button disabled={isPending} onClick={() => connectWith('sign-in')}>
          Sign in with existing passkey
        </button>
        {error && <p>{error.message}</p>}
      </div>
    );
  }

  return (
    <>
      <div>
        Signed in as <code>{tempoAccount.accounts[0]}</code>
        <button onClick={() => disconnect({connector: tempoAccount.connector})}>
          Sign out
        </button>
      </div>
      {children(tempoAccount)}
    </>
  );
}
```

### 3. Adapt the wagmi session to an `EthWallet` for `<CoinflowPurchase>`

Coinflow's iframe passes EIP-712 typed data into `signMessage` as a
JSON-stringified object — the same shape secp256k1 merchants already
handle. Parse the string, detect the EIP-712 shape, and route typed data
through wagmi's `useSignTypedData` (which invokes the chosen connector).
Let typed-data errors surface — do not retry as `personal_sign`.

```tsx
import {useCallback} from 'react';
import {useConnections, useSignMessage, useSignTypedData} from 'wagmi';
import type {Hex} from 'viem';
import type {EthWallet} from '@coinflowlabs/react';
import {TEMPO_WEBAUTHN_CONNECTOR_TYPE} from './ContextWrapper';

type Eip712Payload = {
  domain: Record<string, unknown>;
  types: Record<string, readonly {name: string; type: string}[]>;
  primaryType: string;
  message: Record<string, unknown>;
};

function parseJsonMessage(message: string): unknown {
  try {
    return JSON.parse(message) as unknown;
  } catch {
    return undefined;
  }
}

function isEip712Payload(value: unknown): value is Eip712Payload {
  if (value === null || typeof value !== 'object') return false;
  const v = value as Record<string, unknown>;
  return (
    typeof v.domain === 'object' &&
    v.domain !== null &&
    typeof v.types === 'object' &&
    v.types !== null &&
    typeof v.primaryType === 'string' &&
    typeof v.message === 'object' &&
    v.message !== null
  );
}

export function useTempoPasskeyWallet(): EthWallet | undefined {
  const connections = useConnections();
  const {signMessageAsync} = useSignMessage();
  const {signTypedDataAsync} = useSignTypedData();

  const tempoAccount = connections.find(
    c => c.connector.type === TEMPO_WEBAUTHN_CONNECTOR_TYPE
  );
  const address = tempoAccount?.accounts[0];

  const signMessage = useCallback(
    async (message: string): Promise<Hex> => {
      if (!tempoAccount || !address) {
        throw new Error('Tempo passkey wallet not connected - sign in first.');
      }

      const parsed = parseJsonMessage(message);
      if (isEip712Payload(parsed)) {
        return signTypedDataAsync({
          ...parsed,
          connector: tempoAccount.connector,
        } as Parameters<typeof signTypedDataAsync>[0]);
      }

      return signMessageAsync({
        message,
        connector: tempoAccount.connector,
      });
    },
    [tempoAccount, address, signMessageAsync, signTypedDataAsync]
  );

  if (!tempoAccount || !address) return undefined;

  return {
    address,
    signMessage,
    sendTransaction: () =>
      Promise.reject(
        new Error(
          'Tempo passkey redemption does not require sendTransaction — Coinflow submits the redemption on your behalf.'
        )
      ),
  };
}
```

Compose the gate, adapter hook, and `<CoinflowPurchase>`:

```tsx
function TempoPurchaseContent() {
  const wallet = useTempoPasskeyWallet();
  if (!wallet) return null;

  return (
    <CoinflowPurchase
      wallet={wallet}
      blockchain={'tempo'}
      /* ...other props... */
    />
  );
}

function TempoPurchase() {
  return <TempoPasskeyGate>{() => <TempoPurchaseContent />}</TempoPasskeyGate>;
}
```

## Login signatures

The `signMessage` adapter from step 3 also handles Coinflow's login
challenge. Users sign in with their Tempo signer (secp256k1, P-256, or
WebAuthn) with no extra client-side code.

## Errors your users may see

The Coinflow iframe surfaces this message to the user during the Tempo
signature flow:

| When it happens                           | What the user sees                |
| ----------------------------------------- | --------------------------------- |
| The signature cannot be verified on-chain | *Invalid credits auth signature.* |

## Operational notes

* **Production key storage (WebAuthn).** Point `KeyManager.http(url)` at a
  server-side key store. `KeyManager.localStorage()` is demo-only — it
  stores the credential / public-key mapping in the browser, so the app
  loses the mapping when browser storage is cleared or the user switches
  devices.
* **Same OpenAPI shape across all three signer types.** Coinflow adds no
  new request / response fields. The `permitCredits` field carries the
  opaque signature bytes for whichever signer the user used.

## Related documentation

* [Implement Settlement to EVM Contract](/guides/checkout/settlement-locations/settlement-to-contracts/settle-to-evm-contract/implement-settlement-to-evm-contract) — baseline EVM settlement guide.
* [How EVM Transactions Work](/guides/checkout/settlement-locations/settlement-to-contracts/settle-to-evm-contract/evm-transactions-in-depth) — lifecycle of a Coinflow EVM transaction.
* [Whitelist Your Contracts](/guides/checkout/settlement-locations/settlement-to-contracts/whitelist-your-contracts) — whitelisting prerequisite for merchants settling to a contract.