> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.coinflow.cash/guides/checkout/settlement-locations/settlement-to-contracts/settle-to-evm-contract/tempo-signature-integration-secp-256-k-1-p-256-web-authn-keychain-v-2/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 `` 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 `` 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 ``. 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 `` 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 ``. 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 `` 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 ( {/* ...QueryClientProvider, your app tree... */} {children} ); } ``` ### 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[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[0]; connect(variables); }; if (!tempoAccount) { return (
{error &&

{error.message}

}
); } return ( <>
Signed in as {tempoAccount.accounts[0]}
{children(tempoAccount)} ); } ``` ### 3. Adapt the wagmi session to an `EthWallet` for `` 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; types: Record; primaryType: string; message: Record; }; 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; 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 => { 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[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 ``: ```tsx function TempoPurchaseContent() { const wallet = useTempoPasskeyWallet(); if (!wallet) return null; return ( ); } function TempoPurchase() { return {() => }; } ``` ## 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.