---
name: oviato
description: Build on Oviato — passkey auth, multi-chain wallet (Bitcoin + EVM), popup/iframe signing flows. Use this skill when the user asks to integrate Oviato, sign transactions, connect a wallet, or migrate from viem/ethers.
---

# Oviato SDK Skill

You are an expert in building on **Oviato** — a multi-chain wallet SDK with
passkey (WebAuthn) authentication. Oviato handles key custody via device
passkeys and signs transactions inside a popup/iframe bridge, so dapps never
touch private keys.

Authoritative docs live at **https://docs.oviato.com**. Full text:
**https://docs.oviato.com/llms-full.txt**. Page index:
**https://docs.oviato.com/llms.txt**. Always prefer fetching the canonical
docs when you need API details this skill doesn't cover.

## Scope: when this skill applies

Use this skill when the user mentions any of:

- Oviato, `@oviato/sdk`, `@oviato/connect`
- Passkey / WebAuthn wallet authentication
- Signing messages, sending crypto, contract calls on an Oviato wallet
- OVI.ID usernames
- Specifically asks about bridge popups, nonce tracking, or Oviato docs

Do **not** apply to unrelated wallet SDKs (Privy, Dynamic, Turnkey, Web3Auth,
Magic, viem, ethers) — those have their own docs.

---

## Mental model — load this first

Oviato exposes **three API surfaces**:

| Surface | Location | Works on |
|---|---|---|
| Chain-agnostic | top level (`sendTransfer`, `signMessage`, `switchNetwork`, `getBalance`, auth, session) | Bitcoin + EVM |
| EVM-specific | `evm.*` namespace (reads, writes, tokens, NFTs, fees, EIP-712) | EVM only |
| UTXO-specific | `utxo.*` namespace (`signPsbt`) | Bitcoin only |

The namespaces **throw** on the wrong session type — calling `evm.readContract`
on a Bitcoin session returns an error, not silent garbage.

Oviato ships as **two packages, same API**:

- **`@oviato/connect`** — React/Next.js. Use this for any React app.
  Two entry points:
  - `@oviato/connect` — re-exports 100% of `@oviato/sdk` (actions, namespaces)
  - `@oviato/connect/client` — React provider, hooks, components
- **`@oviato/sdk`** — framework-agnostic. Vue, Svelte, Angular, Solid,
  vanilla JS, Node, Deno, Bun.

**Never install both.** `@oviato/connect` already depends on `@oviato/sdk`.
Two installs = duplicate Zustand stores = session state splits.

---

## Decision trees

### Which package?

```
React or Next.js?
├── Yes → @oviato/connect
│         import actions:    from '@oviato/connect'
│         import React parts: from '@oviato/connect/client'
└── No  → @oviato/sdk
          import everything:  from '@oviato/sdk'
```

### Which API surface for a given task?

```
What's the task?
├── Log in / register / refresh session
│     → login() / register() / authenticate()          (top level)
│
├── Show the connected user's address + balance
│     → getSession() / rpc.getBalance()                (top level)
│
├── Send native coin (BTC / ETH / Base ETH)
│     → sendTransfer({ recipient: { address, amount } }) (top level)
│
├── Sign a plain message
│     → signMessage('text')                            (top level)
│
├── Switch chains
│     → switchNetwork(Network.X)                       (top level)
│
├── Call a smart contract (read)
│     → evm.readContract / evm.multicall               (EVM)
│
├── Read a token/NFT balance or metadata
│     → evm.getTokenBalance / evm.getNftOwner / ...    (EVM)
│
├── Send an ERC-20 / ERC-721 / any contract tx
│     → evm.writeContract({ abi, functionName, args }) (EVM)
│
├── Generic EVM transaction (pre-encoded data)
│     → evm.sendTransaction({ to, value, data })       (EVM)
│
├── Sign EIP-712 typed data
│     → evm.signTypedData({ domain, types, ... })      (EVM)
│
├── Wait for a tx to mine
│     → evm.waitForTransactionReceipt({ hash })        (EVM)
│
├── Sign a Bitcoin PSBT (coin-selection done server-side)
│     → utxo.signPsbt({ psbt, broadcast: true })       (Bitcoin)
│
└── Anything niche not here? Fetch llms-full.txt.
```

### Popup vs iframe?

Writes require user approval via the bridge UI. **Default is iframe** (better
UX, stays in-page, works on mobile). The SDK only falls back to popup in two
cases:

1. **Localhost / non-HTTPS** — iframes have security restrictions in insecure
   contexts, so dev environments auto-switch to popup.
2. **Safari / iOS + WebAuthn passkey ops** (register/authenticate) — Safari has
   long-standing bugs around WebAuthn in iframes, so those specific flows
   force popup.

In production, expect iframe everywhere. Override with `method: 'popup'` or
`method: 'iframe'` if you genuinely need to force one.

---

## Initialization

### React (preferred)

```tsx
// app/providers.tsx
'use client';
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <OviConnectProvider appId="your-app-id" theme="auto">
      {children}
    </OviConnectProvider>
  );
}
```

**Do NOT pass `network:` to the provider.** It was removed. Specify network
on `<ConnectButton>` / `<Connect>` / `login({ network })` instead.

### Non-React

```ts
import { initialize } from '@oviato/sdk';
await initialize({ appId: 'your-app-id' });
```

---

## Response envelope pattern (critical)

Every write method resolves to a **discriminated envelope**, never a raw value:

```ts
type Result =
  | { status: 'success'; type: string; data: unknown }
  | { status: 'error'; message: string };
```

**Always narrow on `status` before accessing `.data`:**

```ts
const r = await evm.sendTransaction({ to, value });

if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
  console.log(r.data.hash, r.data.explorerUrl);
} else if (r.status === 'error') {
  console.error(r.message);
}
```

**Why:** "user closed the popup" arrives as `status: 'error'` — *not a thrown
exception*. Don't wrap writes in try/catch expecting to catch cancellation.

Migrators from viem/ethers who want the raw-hash style can use a tiny adapter:

```ts
async function unwrap(p) {
  const r = await p;
  if (r.status === 'success') return r.data;
  throw new Error(r.message ?? 'Unknown error');
}
const { hash } = await unwrap(evm.sendTransaction({ to, value }));
```

---

## Recipes

### 1. Connect + show session (React)

```tsx
'use client';
import { ConnectButton, useOviConnect } from '@oviato/connect/client';
import { Network } from '@oviato/connect';

export default function Page() {
  const { session, disconnect } = useOviConnect();
  if (!session) return <ConnectButton text="Connect" network={Network.BASEMAINNET} />;
  return (
    <div>
      <p>{session.username} — {session.address}</p>
      <button onClick={disconnect}>Disconnect</button>
    </div>
  );
}
```

Note: the hook exposes `disconnect`, not `logout`.

### 2. Send native coin (any chain)

```ts
import { sendTransfer } from '@oviato/connect';

// Bitcoin: amount is sats (number)
const r1 = await sendTransfer({ recipient: { address: 'bc1q...', amount: 10000 } });

// EVM: amount is wei (bigint recommended)
const r2 = await sendTransfer({ recipient: { address: '0x...', amount: 10n ** 15n } });

if (r2.status === 'success' && r2.type === 'sendTransferSuccess') {
  console.log(r2.data.txid, r2.data.explorerUrl);
}
```

### 3. Transfer an ERC-20

```ts
import { evm } from '@oviato/connect';

const ERC20_ABI = [
  {
    type: 'function', name: 'transfer',
    inputs: [
      { name: 'to', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ type: 'bool' }], stateMutability: 'nonpayable',
  },
] as const;

const r = await evm.writeContract({
  address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC mainnet
  abi: ERC20_ABI,
  functionName: 'transfer',
  args: [recipient, 1_000_000n], // 1 USDC (6 decimals)
});
```

### 4. Approve + use (check allowance first, wait for approval)

```ts
import { evm } from '@oviato/connect';

const { raw: current } = await evm.getTokenAllowance({
  tokenAddress: USDC,
  owner: session.address,
  spender: ROUTER,
});

if (current < amount) {
  const approve = await evm.writeContract({
    address: USDC, abi: ERC20_ABI, functionName: 'approve',
    args: [ROUTER, amount],
  });
  if (approve.status !== 'success') throw new Error('approve failed');

  // MUST wait for the approve to mine before calling the router
  await evm.waitForTransactionReceipt({ hash: approve.data.hash });
}

// now call the router
```

### 5. Read token balance + metadata in one call

```ts
const { raw, formatted, symbol, decimals } = await evm.getTokenBalance({
  tokenAddress: USDC,
  address: session.address,
});
// raw: bigint, formatted: '1234.56'
```

Never parse `formatted` for math — it's display-only. Use `raw` and
`formatUnits`/`parseUnits` for arithmetic.

### 6. Sign EIP-712 typed data

```ts
const r = await evm.signTypedData({
  domain: { name: 'MyApp', version: '1', chainId: 8453 },
  types: {
    // DO NOT include EIP712Domain here — it breaks the signature
    Greeting: [
      { name: 'content', type: 'string' },
      { name: 'from', type: 'address' },
    ],
  },
  primaryType: 'Greeting',
  message: { content: 'Hi', from: session.address },
});
```

### 7. Sign a Bitcoin PSBT (server-built)

```ts
import { utxo } from '@oviato/connect';

// Backend builds the PSBT with coin selection + outputs
const { psbt } = await fetch('/api/tx/prepare').then(r => r.json());

const r = await utxo.signPsbt({ psbt, broadcast: true });
if (r.status === 'success' && r.type === 'signPsbtBroadcastSuccess') {
  console.log(r.data.txid);
}
```

### 8. Batch reads with multicall

```ts
const [name, symbol, decimals, totalSupply] = await evm.multicall({
  contracts: [
    { address: USDC, abi: ERC20_ABI, functionName: 'name' },
    { address: USDC, abi: ERC20_ABI, functionName: 'symbol' },
    { address: USDC, abi: ERC20_ABI, functionName: 'decimals' },
    { address: USDC, abi: ERC20_ABI, functionName: 'totalSupply' },
  ],
});
// Each: { status: 'success', result } | { status: 'failure', error }
```

`allowFailure` defaults to `true` — a single revert doesn't poison the batch.

### 9. Switch networks

```ts
import { switchNetwork, Network } from '@oviato/connect';

await switchNetwork(Network.BASEMAINNET);
// Session re-issued with a new chain-scoped JWT.
// User taps passkey once — this is a security requirement, not a bug.
```

Ethers-style alias: `evm.switchChain(Network.BASEMAINNET)`.

---

## Networks

```ts
enum Network {
  BTCMAINNET   = 'bitcoin',
  BTCTESTNET   = 'bitcointestnet',
  ETHMAINNET   = 'eth',
  ETHSEPOLIA   = 'ethsepolia',
  BASEMAINNET  = 'base',
  BASESEPOLIA  = 'basesepolia',
}
```

Sessions are **chain-scoped**. A JWT for `base` can't be used for `eth`
without a `switchNetwork` re-issue.

---

## bigint conventions (EVM)

EVM wei values can exceed `Number.MAX_SAFE_INTEGER`. The SDK uses `bigint`
throughout:

```ts
import { formatUnits, parseUnits } from '@oviato/connect';

const wei = parseUnits('1.5', 18);         // → 1500000000000000000n
const usd = formatUnits(1_234_500n, 6);     // → '1.2345'

// Literals
const oneEth = 10n ** 18n;
const oneUsdc = 1_000_000n;  // 1 USDC has 6 decimals
```

UTXO amounts stay `number` (satoshis max out at ~2.1×10¹⁵, well under 2⁵³).

---

## Nonce tracker (EVM writes)

Oviato runs a server-side per-`(address, chain)` nonce tracker. **You never
manage nonces.** Rapid-fire sends serialize correctly:

```ts
await Promise.all([
  evm.sendTransaction({ to, value: 1n }),
  evm.sendTransaction({ to, value: 2n }),
  evm.sendTransaction({ to, value: 3n }),
]); // all three get unique, sequential nonces
```

On pre-mempool errors (`replacement underpriced`, `nonce too low`, `already
known`, `underpriced`) the tracker **auto-releases** the reservation. The next
send picks up the right nonce. No manual recovery required.

If you genuinely need to override (migrating from a legacy nonce store), pass
`nonce:` explicitly to `sendTransaction`/`writeContract`.

---

## Error-keyed troubleshooting

### "User closed popup"
Not an exception — `status: 'error'`. Check and re-prompt if user retries.

### "Replacement transaction underpriced"
A previous tx at the same nonce is pending with higher gas. Retry with
`fee: 'fast'` — the tracker auto-releases on this error.

### "Nonce too low"
Nonce already used on-chain. Tracker resyncs automatically on next call.
Just retry.

### "Insufficient funds"
Not just `value > balance`, but `value + gas × maxFeePerGas > balance`.
Check `evm.getGasPrice()` + `evm.estimateGas(tx)` to size.

### "Missing revert data" (EVM read)
Almost always: the contract reverted silently. Common cause — ERC-20 transfer
with 0 balance. Check `evm.getTokenBalance` first.

### "Popup blocked by browser"
`sendTransaction` was called outside a user gesture. Always invoke from a
click handler. Fall back to `method: 'iframe'` if you can't.

### "Not connected: please login first"
No session. Require `useOviConnect().session` before rendering write UI.

---

## Anti-patterns — DO NOT

- **Do NOT install both packages.** `@oviato/connect` re-exports `@oviato/sdk`. Installing both duplicates module state.
- **Do NOT pass `network:` to `<OviConnectProvider>`.** Removed. Pass it per-action (button, login call).
- **Do NOT include `EIP712Domain` in `types`.** The SDK computes the domain hash from `domain` directly — adding it changes the hash and signatures won't recover.
- **Do NOT parse `formatted` for math.** It's display-only; use `raw` (bigint) + `formatUnits`/`parseUnits`.
- **Do NOT call `evm.*` on a Bitcoin session** or `utxo.*` on EVM. Use `switchNetwork` first, or use chain-agnostic methods.
- **Do NOT retry write errors blindly.** If `status === 'error'`, read `message` — surface it to the user or branch on specific errors (see troubleshooting above).
- **Do NOT write multi-chain code that branches on `session.network === 'eth'` vs `'bitcoin'`.** That's what chain-agnostic methods are for.
- **Do NOT use `logout` on the hook** — it's `disconnect`.
- **Do NOT wrap writes in try/catch expecting to catch user cancellation.** Cancellation is an envelope `error`, not a throw.

---

## When the user asks about ordinals / inscriptions / runes

**Out of scope for v1.** The SDK ships `utxo.signPsbt` which can sign any PSBT
the dapp builds, but we don't provide ordinals-aware PSBT construction.
Suggest the user build PSBTs using an ordinals library (out of our scope)
and then sign them with `utxo.signPsbt`.

## When the user asks about SOL / XRP / Solana / Ripple

**Not supported yet.** Our surface is Bitcoin + EVM (eth, base + their
testnets). Don't invent APIs for other chain families.

## When the user asks about `watchAsset` / EIP-747

**Not implemented.** Use `evm.getTokens()` — we maintain a curated per-chain
registry (USDC, USDT, WETH, etc.) that devs can fetch. Custom app-pinned
tokens are a v2 feature.

## When the user asks about `simulateContract`

**Not implemented.** Call `evm.readContract` with the same ABI to preview
state, or pair with `evm.estimateGas` to check revert.

---

## What to do if this skill is out of date

If the user asks about something not covered here, **fetch the canonical
docs** rather than guessing. The LLM-optimized file lives at
**https://docs.oviato.com/llms-full.txt** and is regenerated on every
deploy. The page index is at **https://docs.oviato.com/llms.txt**.

Don't invent methods, parameters, or return shapes — if you're unsure,
look them up.
