# Oviato — Complete Documentation for LLMs
Oviato is a multi-chain wallet SDK with passkey authentication. This file
concatenates every docs page in sidebar order. If you only need a pointer
map, fetch `https://docs.oviato.com/llms.txt` instead — it's the llmstxt.org-spec index.
## Mental model (read this first)
Oviato exposes three API surfaces:
1. **Chain-agnostic (top level)** — `signMessage`, `sendTransfer`,
`switchNetwork`, `getBalance`, plus auth (`login`, `register`,
`authenticate`). These dispatch by session network and work on
Bitcoin + EVM with identical signatures.
2. **`evm.*` namespace** — EVM-specific: `readContract`, `multicall`,
`sendTransaction`, `writeContract`, `signTypedData`,
`waitForTransactionReceipt`, token + NFT helpers, fee estimates.
Throws on non-EVM sessions.
3. **`utxo.*` namespace** — Bitcoin-specific: `signPsbt`.
Throws on non-UTXO sessions.
Two packages, same API:
- **`@oviato/sdk`** — framework-agnostic. Vue, Svelte, Angular, vanilla JS,
Node, Deno, Bun.
- **`@oviato/connect`** — React. Re-exports 100% of `@oviato/sdk` plus
adds `OviConnectProvider`, `useOviConnect`, `ConnectButton`,
`Connect` at `@oviato/connect/client`.
Never install both. `@oviato/connect` already depends on `@oviato/sdk`.
## Response envelope pattern
Writes return a discriminated envelope, never a raw value:
```ts
const r = await evm.sendTransaction({ to, value });
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log(r.data.hash);
} else if (r.status === 'error') {
console.error(r.message);
}
```
Narrow on `status` + `type` before accessing `data`. "User cancelled
popup" arrives as `status: 'error'` — not a thrown exception.
## Nonce tracker (EVM writes)
Rapid-fire EVM sends never collide — the server serializes nonces per
`(address, chain)`. You never manage nonces. On pre-mempool errors
(`replacement underpriced`, `nonce too low`, `already known`,
`underpriced`) the tracker auto-releases and the next send picks the
right nonce.
## Supported networks
EVM: `eth`, `ethsepolia`, `base`, `basesepolia`
UTXO: `bitcoin`, `bitcointestnet`
---
# Oviato Documentation
> Passkey-based authentication + multi-chain wallet SDK for Bitcoin and EVM chains.
Canonical URL: https://docs.oviato.com
File: index.mdx
# Welcome to Oviato
Oviato is a **passkey-based wallet SDK** for multi-chain applications. Users sign in with Face ID / Touch ID / security key — no passwords, no seed phrases. One API works across Bitcoin and EVM.
Ship a working sign-in + transfer in 10 minutes.
Mental model — passkeys, sessions, popup flows, nonce tracker.
`@oviato/sdk` vs `@oviato/connect` — same API, pick by framework.
Create your app, configure domains, themes, networks.
## Explore the SDK
Works on Bitcoin + EVM with one API — `signMessage`, `sendTransfer`, `switchNetwork`, `getBalance`.
`evm.*` namespace — `readContract`, `multicall`, `sendTransaction`, `writeContract`, token + NFT helpers, fees, receipts.
`utxo.*` namespace — PSBT signing for Bitcoin, including server-built transactions and counterparty signing.
`OviConnectProvider`, `useOviConnect`, ``, ``.
## Why Oviato
- **Passkey authentication** — Face ID, Touch ID, or FIDO2 security keys. No passwords, no seed phrases.
- **Multi-chain with one API** — identical surface for Bitcoin + EVM. Switching chains is a one-liner.
- **Server-side nonce tracker** — rapid-fire EVM sends don't collide. You don't manage nonces.
- **Specialized confirmation sheets** — `writeContract` with an ERC-20 ABI shows a branded "Transfer Token" sheet, not raw hex.
- **Tiny bundle** — ~11 KB gzipped core, ~14 KB with React bindings.
- **TypeScript-native** — full type safety with `bigint` for EVM, discriminated unions for chain-agnostic returns.
## Three-step start
1. **Create your app** at [dashboard.oviato.com](https://dashboard.oviato.com) → copy your `appId`.
2. **Install** `@oviato/connect` (React) or `@oviato/sdk` (any framework).
3. **Drop in the provider + button**:
```tsx
import { OviConnectProvider, ConnectButton } from '@oviato/connect/client';
import { Network } from '@oviato/connect';
```
See the [Quickstart](/getting-started/quickstart) for the full walk-through including sends and contract calls.
## Community + Resources
- [GitHub](https://github.com/oviato-hq) — report issues, follow releases
- [Developer Dashboard](https://dashboard.oviato.com) — create apps, view activity logs, manage branding
- [Changelog](/changelog/sdk) — version history
---
# Section: Getting Started
# Quick Start
> Get started with Oviato in minutes
Canonical URL: https://docs.oviato.com/getting-started/overview
File: getting-started/overview.mdx
Welcome to Oviato! This guide will walk you through integrating Oviato authentication and wallet functionality into your application.
## What is Oviato?
Oviato provides WebAuthn-based passkey authentication for multi-chain blockchain applications. Users can create accounts and sign transactions using biometrics (Face ID, Touch ID) or security keys—no passwords or seed phrases required.
## Getting Started in 3 Steps
### 1. Create Your App
First, create an application in the [Oviato Developer Dashboard](https://dashboard.oviato.com):
1. Sign up or log in at [dashboard.oviato.com](https://dashboard.oviato.com)
2. Create a new application
3. Configure your app settings (name, logo, domains)
4. Copy your **App ID** - you'll need this for the SDK
See the [Developer Dashboard](/developer-dashboard/setting-up-app) section for detailed instructions.
### 2. Choose Your SDK
Oviato offers two SDK options depending on your framework:
#### For React/Next.js Apps: **@oviato/connect**
Best for React applications. Includes hooks, providers, and pre-built UI components.
```bash
npm install @oviato/connect
```
[→ @oviato/connect Documentation](/react-components/overview)
#### For Other Frameworks: **@oviato/sdk**
Framework-agnostic core SDK. Works with Vue, Svelte, Angular, or vanilla JavaScript.
```bash
npm install @oviato/sdk
```
[→ @oviato/sdk Documentation](/packages/choosing)
### 3. Integrate Authentication
#### Next.js Example
```tsx
// app/layout.tsx
import { OviConnectProvider } from "@oviato/connect/client";
export default function RootLayout({ children }) {
return (
);
}
```
#### Vanilla JavaScript Example
```javascript
import { initialize, authenticate, getSession } from "@oviato/sdk";
// Initialize
await initialize({
appId: "your-app-id",
network: "bitcoinmainnet",
});
// Authenticate
const result = await authenticate();
if (result.status === "success") {
const session = getSession();
console.log("Connected:", session.address);
}
```
## What's Next?
- **[Developer Dashboard Guide](/developer-dashboard/setting-up-app)** - Set up your first app
- **[@oviato/connect Docs](/react-components/overview)** - React SDK with hooks and components
- **[@oviato/sdk Docs](/packages/choosing)** - Framework-agnostic core SDK
## Key Features
- **Passkey Authentication** - Secure WebAuthn-based login with biometrics
- **Multi-Chain Support** - Bitcoin, Ethereum, Base (Solana, XRP, and more coming soon)
- **SSR Compatible** - Works with Next.js App Router and all SSR frameworks
- **TypeScript First** - Full type safety and IntelliSense
- **Pre-built UI** - Drop-in components for rapid development
- **Tiny Bundle** - Optimized for performance
## Need Help?
- Drop us an email to: [support[at]oviato.com](mailto:support@oviato.com)
---
# How Oviato Works?
> An overview of the Oviato architecture and data flow.
Canonical URL: https://docs.oviato.com/getting-started/how-oviato-works
File: getting-started/how-oviato-works.mdx
Understand the high-level architecture and request flow within Oviato.
---
# Quickstart
> Ship a working sign-in + multi-chain wallet in 10 minutes with @oviato/connect.
Canonical URL: https://docs.oviato.com/getting-started/quickstart
File: getting-started/quickstart.mdx
Build a working React app with passkey sign-in, native sends (BTC + ETH), and a token transfer — all in one file.
This guide uses `@oviato/connect`. Not on React? See
[Chain-Agnostic → Overview](/chain-agnostic/overview) for the
`@oviato/sdk` version.
## Prerequisites
- An `appId` from the [Developer Dashboard](/developer-dashboard/setting-up-app) (free to create)
- Node 18+ and React 18+
- A device with a passkey — iPhone/iPad with Face ID, Mac with Touch ID, Android with fingerprint, or a FIDO2 security key
## 1. Install
```bash
pnpm add @oviato/connect
```
```bash
npm install @oviato/connect
```
```bash
yarn add @oviato/connect
```
## 2. Wrap your app in the provider
```tsx title="app/providers.tsx"
'use client';
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```tsx title="app/layout.tsx"
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
{children}
);
}
```
```tsx title="src/main.tsx"
import React from 'react';
import ReactDOM from 'react-dom/client';
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
,
);
```
Replace `your-app-id-here` with the `appId` from your dashboard. The
provider's `appId` is non-optional — the SDK refuses to initialize without
it.
## 3. Add a Connect button
The fastest way — drop in our pre-built button, pick a network per button.
```tsx title="app/page.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 (
);
}
return (
Connected as {session.username}
Address: {session.address}
Network: {session.network}
);
}
```
Click one, approve the passkey, done. The `session` object is now populated with the user's address, network, public keys, and auth token.
## 4. Send your first transaction
Every chain supports `sendTransfer` — pass the recipient + amount in the chain's smallest unit.
```tsx
import { sendTransfer } from '@oviato/connect';
async function sendBitcoin() {
const r = await sendTransfer({
recipient: {
address: 'bc1q...',
amount: 10000, // satoshis (0.0001 BTC)
},
fee: 'auto',
});
if (r.status === 'success' && r.type === 'sendTransferSuccess') {
console.log('TxID:', r.data.txid);
console.log('Explorer:', r.data.explorerUrl);
} else if (r.status === 'error') {
console.error(r.message);
}
}
```
```tsx
import { sendTransfer } from '@oviato/connect';
async function sendEth() {
const r = await sendTransfer({
recipient: {
address: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6',
amount: 10n ** 15n, // wei (0.001 ETH) — bigint for overflow safety
},
fee: 'auto',
});
if (r.status === 'success' && r.type === 'sendTransferSuccess') {
console.log('Hash:', r.data.txid);
console.log('Explorer:', r.data.explorerUrl);
}
}
```
A popup opens with the full transaction details. The user approves with their passkey, the bridge signs server-side, and the tx broadcasts. You get back a hash + explorer URL.
## 5. Transfer a token (EVM only)
Use `evm.writeContract` to call any contract method. Here's USDC transfer:
```tsx
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;
async function transferUSDC() {
const r = await evm.writeContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
abi: ERC20_ABI,
functionName: 'transfer',
args: ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', 1_000_000n], // 1 USDC (6 decimals)
});
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log('Hash:', r.data.hash);
}
}
```
The bridge auto-detects this is an ERC-20 transfer (via the ABI + known-contracts registry) and renders a dedicated "Transfer Token" confirmation sheet — no raw hex shown to the user.
## 6. Wait for confirmation
```tsx
import { evm } from '@oviato/connect';
const receipt = await evm.waitForTransactionReceipt({ hash });
if (receipt?.status === 'success') {
console.log('Mined in block', receipt.blockNumber);
}
```
Defaults adapt to the chain: 1 confirmation + 2s polling on L2s, 3 confirmations + 4s polling on Ethereum mainnet.
## That's it
You now have:
- Passkey auth
- Multi-chain connect
- Native + ERC-20 sends
- Receipt polling
Not bad for 20 lines of code.
## What to learn next
- [Core Concepts](/getting-started/core-concepts) — the mental model behind the SDK
- [EVM → Overview](/evm/overview) — the full EVM surface (`readContract`, `multicall`, NFTs, fees, EIP-712)
- [UTXO → Overview](/utxo/overview) — PSBT signing, multi-recipient sends
- [React Components → Provider](/react-components/provider) — deeper config (themes, DNS override, custom domains)
- [Developer Dashboard](/developer-dashboard/setting-up-app) — configure your app (domains, branding, supported networks)
---
# Core Concepts
> The mental model behind Oviato — passkeys, sessions, popup flows, chain scoping, the nonce tracker.
Canonical URL: https://docs.oviato.com/getting-started/core-concepts
File: getting-started/core-concepts.mdx
You can use Oviato without reading this page — the SDK handles everything. But when something surprises you later, come back here: this is where the moving parts are named.
## The data model
Oviato has four things a developer interacts with.
**App.** Your product. Created in the [Developer Dashboard](/developer-dashboard/setting-up-app) — gives you an `appId`, domains, theme, supported networks. The SDK reads your app's config at `initialize()` time.
**User.** A human with a [passkey](https://fidoalliance.org/passkeys/). Identified by a username (which maps to an [OVI.ID](/getting-started/how-oviato-works)). A user can connect to many apps; their wallet addresses are deterministic per (user, app, network).
**Session.** A signed JWT + the user's public state (address, network, hdkeys). Created on login and kept in-memory + localStorage by the SDK. Scoped to one network at a time — switching networks re-issues the JWT.
**Bridge.** The popup/iframe UI at `connect.oviato.com` where the user approves sign requests. Your dapp never touches private keys; signatures happen inside the bridge via WebAuthn.
## The passkey flow
WebAuthn passkeys are how users sign in and sign transactions — no passwords, no seed phrases.
**First-time registration.** User types a username, the SDK calls `register()`. The browser prompts for a passkey (Face ID / Touch ID / security key). A cryptographic keypair is generated + stored in the user's device keychain. The public key is uploaded to Oviato; the private half never leaves the device.
**Login on the same device.** `login()` opens a WebAuthn prompt. The device proves possession of the private key by signing a server-issued challenge. SDK receives a JWT + hd-keys, session starts.
**Login on a new device.** Passkeys sync across a user's Apple/Google/Microsoft ecosystem automatically. A user who registered on iPhone can log in on their Mac without extra setup.
**Signing a transaction.** The dapp calls `sendTransaction` / `writeContract` / etc. A popup opens showing the full transaction details. User approves with their passkey — WebAuthn signs on the device, the bridge completes the on-chain signature server-side, and the tx is broadcast.
The dapp never holds a private key or sees the passkey. All signing flows
through the bridge UI, gated by a biometric/security-key tap.
## Sessions are chain-scoped
A session's JWT is issued for **one network** — `eth`, `base`, `bitcoin`, etc. When a user switches networks you must re-issue the JWT.
```ts
import { switchNetwork, Network } from '@oviato/connect';
const r = await switchNetwork(Network.BASEMAINNET);
if (r.status === 'success') {
// SDK's session is now Base-scoped. Every subsequent call
// (reads, writes, balance) targets Base until you switch again.
}
```
Switching networks requires a WebAuthn assertion — the user will see one
passkey prompt to re-sign the session. This is deliberate: it prevents a
stolen JWT from being silently repointed to a different chain.
Under the hood, `switchNetwork` calls `refreshSession` + re-opens the bridge for a one-tap signature.
## Popup vs iframe
Write operations (sign a message, send a tx, switch a network) always show an approval UI. The SDK picks between two modes:
- **Iframe** (default) — overlays a modal in your page. Better UX, works on mobile, same-domain-feel. Used on production HTTPS.
- **Popup** — opens a new browser window. Only used when iframe can't work:
- **Localhost / non-HTTPS** — iframes have security restrictions in insecure contexts, so the SDK auto-falls-back to popup.
- **Safari / iOS with WebAuthn** (passkey create/login) — Safari has long-standing bugs around WebAuthn in iframes; forced to popup for those ops only.
```ts
await evm.sendTransaction({ to, value, method: 'iframe' }); // force iframe
await evm.sendTransaction({ to, value, method: 'popup' }); // force popup
```
In production, expect iframe everywhere. Popup is a localhost-dev
concession and a Safari-WebAuthn concession — you typically don't need
to think about it.
## The response envelope
Every write method resolves to a discriminated envelope, never a raw value.
```ts
type Result =
| { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } }
| { status: 'error'; message: string };
```
Narrow on `status` + `type` before reading `.data`:
```ts
const r = await evm.sendTransaction({ to, value });
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log('Hash:', r.data.hash);
} else if (r.status === 'error') {
console.error(r.message);
}
```
This is different from ethers/viem, which return a raw hash and throw on error. See [Migrating from viem](/evm/migrating-from-viem) for a one-line adapter if you prefer that style.
## The nonce tracker
EVM transactions need a monotonically-increasing nonce. A dapp that fires three `sendTransaction` calls in 100 ms would collide if they all read `eth_getTransactionCount('pending')` at the same time — the chain hasn't caught up yet.
Oviato runs a server-side nonce tracker per (address, chain). Rapid-fire calls serialize correctly without you having to manage anything.
```ts
// These three calls all get unique, sequential nonces automatically.
const [r1, r2, r3] = await Promise.all([
evm.sendTransaction({ to, value: 10n ** 15n }),
evm.sendTransaction({ to, value: 2n * 10n ** 15n }),
evm.sendTransaction({ to, value: 3n * 10n ** 15n }),
]);
```
If a broadcast fails pre-mempool (`replacement underpriced`, `nonce too
low`), the tracker automatically releases the nonce for reuse. You get a
structured error with a `retryAfter` hint — no manual recovery needed.
## Protected vs read-only methods
Some methods require a valid Bearer JWT; others are public reads.
**Protected (JWT required):**
- `connect_prepare` + `connect_preparePsbt` (UTXO writes)
- `connect_prepareEvm` + `connect_broadcastEvm` (EVM writes)
- `refreshSession`
**Read-only (no JWT needed):**
- All `evm.*` reads (contract, chain, fees, tokens, NFTs)
- `rpc.getBalance()`, balance history, username resolution
- `x402` payment flows
Rate limits apply to protected methods per `(app, address)`. Reads have looser app-level limits.
## Server-side components you don't see
The SDK looks simple because most of the complexity lives behind it:
- **Bridge worker** (`connect.oviato.com`) — hosts the popup/iframe UI, holds the WebAuthn flow, signs transactions server-side.
- **RPC API** (`rpc.oviato.com`) — JSON-RPC router. Handles auth, rate limits, nonce tracking, activity logging. Routes on-chain reads to Chainstack.
- **Chainstack** — our RPC provider for EVM chains. Behind the API; you never talk to it directly.
- **D1 + KV** — Cloudflare storage for users, app config, nonce state, token cache, activity logs.
You don't need to understand these to ship. They matter when you hit an edge case (rate-limit error, wedged nonce, cache miss) and want to know what's going on.
## Next steps
- [Quickstart](/getting-started/quickstart) — build a working sign-in + transfer in 10 minutes
- [Chain-Agnostic → Overview](/chain-agnostic/overview) — methods that work across BTC + EVM
- [EVM → Overview](/evm/overview) — the `evm.*` namespace, popup flow, envelope pattern
---
# Section: Developer Dashboard
# Signing Up / Logging In
> Create an account and sign in to the Developer Dashboard.
Canonical URL: https://docs.oviato.com/developer-dashboard/signing-up-logging-in
File: developer-dashboard/signing-up-logging-in.mdx
## Overview
Use this guide to create an account and access the Developer Dashboard. It covers sign up, email verification, logging in, and password reset.
## Sign up
1. Key in your work email Address
2. Press Continue
3. Check your inbox for one time password to activate your account.

You would recieve the one time password in few minutes.
- Check spam/junk
- Key in the code in the Dashboard Signup

## Setting up an Org
Key in your Org name, Org URL and Size.

## Log in
1. Key in the email you signed up with.
2. Check your inbox for one time password
3. Click “Continue” to the dashboard.
## Troubleshooting
- Make sure your email address is typed correctly and has been verified.
- If you run into repeated failures, try another browser or disable extensions temporarily.
- Still stuck? Contact support@oviato.com.
---
# Setting Up Your App
> Create and configure your first application in the Oviato Developer Dashboard
Canonical URL: https://docs.oviato.com/developer-dashboard/setting-up-app
File: developer-dashboard/setting-up-app.mdx
This guide will walk you through creating and configuring your first application in the Oviato Developer Dashboard.
## Prerequisites
Before you begin, you'll need:
- An Oviato account (sign up at [dashboard.oviato.com](https://dashboard.oviato.com))
- Your application's domain(s) where you'll integrate Oviato
## Step 1: Access the Dashboard
1. Go to [dashboard.oviato.com](https://dashboard.oviato.com)
2. Sign in with your Oviato account
3. You'll land on the dashboard home page
## Step 2: Create a New Application
1. Click **"Create New App"** or **"New Application"** button
2. Fill in the required information:
- **App Name**: A friendly name for your application (e.g., "My DeFi App"). This is an internal name and won't be visible to your users.
- **Select Network**: We support Bitcoin, Ethereum, and Base networks.

## Step 3: Configure App Settings
After creating your app, you'll need to configure the following settings:
### Basic Information
- **App Name**: Update anytime if needed
- **App ID**: Auto Generated ID for your App. You will be using this ID in the SDK
```tsx
// You'll use this App ID in your code
```
### Allowed Domains
Add the domains where your app will be hosted:
1. Click **"Add Domain"**
2. Enter your domain (e.g., `https://myapp.com` or `http://localhost:3000` for development)
3. Click **"Add"**
**Important**:
- Include both production and development domains
- Localhost addresses are supported for testing
- Wildcards are not supported for security reasons
### Branding (Optional)
Customize how Oviato authentication appears to your users:
- **Logo**: Upload your app logo (recommended: 200x200px PNG or SVG)
- **Brand Color**: Choose your primary brand color
- **Theme**: Select light or dark theme
These settings affect the authentication modal and wallet UI shown to your users.
## Step 4: Copy Your App ID
1. In your app settings, locate the **App ID** field
2. Click the copy icon or select and copy the ID
3. Save this somewhere secure - you'll need it for SDK integration
## Step 5: Environment Selection
Oviato supports multiple environments:
- **Production**: For live applications
- **Staging**: For testing before deployment
You can switch between environments in the dashboard sidebar.
## Step 6: SDK Integration
Now that your app is configured, integrate the Oviato SDK:
### For React/Next.js:
```bash
npm install @oviato/connect
```
```tsx
import { OviConnectProvider } from "@oviato/connect/client";
{children}
;
```
[→ Full @oviato/connect Documentation](/react-components/overview)
### For Other Frameworks:
```bash
npm install @oviato/sdk
```
```javascript
import { initialize } from "@oviato/sdk";
await initialize({
appId: "your-app-id",
network: "bitcoinmainnet",
});
```
[→ Full @oviato/sdk Documentation](/packages/choosing)
## Managing Your App
### View App Analytics
The dashboard provides insights into:
- Total users
- Authentication events
- Transaction volume
- Network usage
### Update App Settings
You can update your app settings anytime:
1. Select your app from the sidebar
2. Navigate to the settings you want to change
3. Make your updates
4. Changes take effect immediately
### Multiple Apps
You can create and manage multiple applications:
- Each app has its own App ID
- Apps can have different configurations
- Switch between apps using the sidebar dropdown
## Security Best Practices
- **Never expose your App ID in public repositories** (if it includes sensitive configuration)
- **Use environment variables** to store your App ID
```bash
# .env.local
NEXT_PUBLIC_OVIATO_APP_ID=your-app-id
```
- **Regularly review** allowed domains to ensure only authorized domains can use your app
- **Monitor** your app's usage in the analytics dashboard
## Troubleshooting
### "Domain not allowed" error
- Verify the domain is added to your allowed domains list
- Check for typos in the domain URL
- Ensure you're using the correct protocol (http/https)
### App ID not working
- Confirm you copied the entire App ID
- Check that you're using the correct environment (staging vs production)
- Verify the SDK is properly initialized
## Next Steps
- **[Integrate @oviato/connect](/getting-started/quickstart)** - Install and setup the React SDK
- **[Integrate @oviato/sdk](/getting-started/quickstart)** - Install the framework-agnostic SDK
---
# Branding and Colors
> Customize brand assets, theme, and colors.
Canonical URL: https://docs.oviato.com/developer-dashboard/branding-and-colors
File: developer-dashboard/branding-and-colors.mdx
You can customize the Branding, logo and colors of the secure iframe. When users are prompted to login, sign a message, approve transfer, Your branding and colors will be visible.

---
# Wallets using your dApp
> View the Wallets/Users using your dApp
Canonical URL: https://docs.oviato.com/developer-dashboard/wallets
File: developer-dashboard/wallets.mdx
This feature lets you see the users and their wallet address using your dApp.
## Prerequisites
For the users information to show up here your app must have users.

---
# Section: AI Integration
# AI Integration Overview
> Use AI tools to accelerate your Oviato development workflow
Canonical URL: https://docs.oviato.com/ai-integration/overview
File: ai-integration/overview.mdx
Modern AI coding assistants can dramatically speed up your Oviato integration. This section covers how to configure the most popular tools for optimal Oviato development.
## Quick Comparison
| Tool | Best For | Setup Complexity |
|------|----------|------------------|
| Claude Code | Deep reasoning, complex integrations, security reviews | Low |
| Cursor | Fast iteration, multi-file editing | Low |
| Antigravity | Agentic workflows, multi-file refactors | Low |
| ChatGPT | Quick questions, code review, learning | Low |
| Codex | Automated tasks, background code generation | Low |
## The LLM Documentation File
All these tools work best when given context about Oviato. We provide an optimized documentation file:
```
https://docs.oviato.com/llms-full.txt
```
This file contains:
- Complete SDK documentation (@oviato/connect and @oviato/sdk)
- Authentication and wallet operation guides
- Code examples for all frameworks
- Changelog and version history
It's automatically regenerated on each deployment to stay current. Learn more about setting it up in the [LLM Documentation File](/ai-integration/using-llms) guide.
## Choosing the Right Tool
### Use Claude Code when:
- Building complex authentication flows
- Reviewing security of wallet integrations
- Debugging tricky PSBT signing issues
- You need deep reasoning about your code
- You want to use [Oviato Skills](/ai-integration/skills) for guided development
### Use Cursor when:
- Rapidly iterating on UI components
- Making changes across multiple files
- You want inline code suggestions
- Working in a VS Code-style environment
### Use Antigravity when:
- You prefer agentic AI that takes action autonomously
- Working on large refactors across many files
- You want a Cursor-like experience with a different workflow
### Use ChatGPT when:
- Learning Oviato concepts
- Getting quick answers about the SDK
- Reviewing code snippets
- Creating Custom GPTs for your team
### Use Codex when:
- You want AI to run tasks in the background
- Automating repetitive integration work
- Running code generation from GitHub issues
---
# LLM Documentation Files
> Point Claude Code, Cursor, Codex, and other AI coding tools at Oviato's LLM-optimized docs.
Canonical URL: https://docs.oviato.com/ai-integration/using-llms
File: ai-integration/using-llms.mdx
Oviato ships two LLM-facing documentation artifacts. They're regenerated on every docs deploy, so anything you point your AI tooling at stays in sync with the codebase.
## The files
### `llms.txt` — index (llmstxt.org spec)
```
https://docs.oviato.com/llms.txt
```
Follows the [llmstxt.org](https://llmstxt.org) standard — a concise markdown file with one line per docs page (title + description + URL), organized by sidebar section. Agents that support the spec (Cursor, Claude Code, newer Copilot modes) fetch this first to see what's available, then pull individual pages on demand.
This saves context window: instead of ingesting ~200 KB of full documentation on every query, the agent scans the 6 KB index and fetches only the pages that match the task.
### `llms-full.txt` — full concatenation
```
https://docs.oviato.com/llms-full.txt
```
Every docs page concatenated into one file, with a preamble that front-loads the mental model (three API surfaces, two-package story, response envelope, nonce tracker) so any LLM reading has the scope before the method-level reference. Use this when your tool doesn't understand `llms.txt` or you want one-shot context.
## What's included
Both files cover:
- **Getting started** — install, quickstart, core concepts
- **Packages** — how to pick between `@oviato/sdk` and `@oviato/connect`
- **Chain-agnostic** — multi-chain actions (`sendTransfer`, `signMessage`, `switchNetwork`, `getBalance`, auth, session)
- **EVM** — the `evm.*` namespace (reads, writes, recipes, troubleshooting, migration from viem/ethers)
- **UTXO** — the `utxo.*` namespace (PSBT signing)
- **React components** — provider, hooks, ``, ``
- **x402 payments**, **RPC reference**, **developer dashboard**
- **Full changelog**
Both are regenerated from the sidebar's `meta.json` on every deploy — drift between sidebar and LLM export is structurally impossible.
## Setup in Claude Code
Add to your project's `CLAUDE.md`:
```markdown
## Documentation
- Oviato LLM index: https://docs.oviato.com/llms.txt
- Oviato full docs: https://docs.oviato.com/llms-full.txt
- Oviato skill (deeper patterns): https://docs.oviato.com/skills/oviato.md
```
Claude Code will consult these when you ask Oviato-related questions.
Install the Oviato skill for richer guidance (decision trees, recipes, anti-patterns):
```bash
curl -o .claude/skills/oviato.md https://docs.oviato.com/skills/oviato.md
```
See [Skills](/ai-integration/skills) for details.
## Setup in Cursor
1. Open **Cursor Settings** → **Features** → **Docs**
2. Click **Add new doc**
3. Paste: `https://docs.oviato.com/llms-full.txt`
4. Name it `Oviato`
Use `@docs Oviato` in chat to reference.
Cursor also supports `.cursorrules` at your project root. Grab our template:
```bash
curl -o .cursorrules https://docs.oviato.com/templates/cursorrules.txt
```
## Setup in Antigravity / Gemini CLI
Add to your project's `GEMINI.md` or `AGENTS.md`:
```markdown
## Documentation
- Oviato LLM index: https://docs.oviato.com/llms.txt
- Oviato full docs: https://docs.oviato.com/llms-full.txt
```
The skill also works under `.agents/skills/` — copy `oviato.md` in.
## Setup in Codex
Paste the `llms-full.txt` URL into your repo's setup instructions or system prompt. For CLI agents, you can pre-fetch:
```bash
curl https://docs.oviato.com/llms-full.txt > .oviato-docs.txt
```
Then reference `.oviato-docs.txt` in your agent's context.
## Setup in ChatGPT / other web UIs
Download and attach:
```bash
curl -O https://docs.oviato.com/llms-full.txt
```
Upload to a conversation or add to a Custom GPT's knowledge base.
## How fresh are these files?
Regenerated on every docs deploy. The generator walks the `meta.json` sidebar — if a page ships in the docs, it's in the LLM files within minutes of deploy.
If your agent ever gives you stale API details (e.g. an old import path),
ask it to re-fetch `https://docs.oviato.com/llms-full.txt`. The skill file
explicitly instructs agents to do this.
## Related
- [Skills](/ai-integration/skills) — install the Oviato skill for decision trees + recipes
- [Overview](/ai-integration/overview) — AI workflow overview
---
# Skills
> Install the Oviato skill for AI-assisted development in Claude Code, Cursor, Antigravity, and Codex
Canonical URL: https://docs.oviato.com/ai-integration/skills
File: ai-integration/skills.mdx
Skills are markdown files that give AI coding tools deep knowledge about the Oviato SDK. Instead of configuring each tool separately, you install one skill and your AI assistant understands authentication, wallets, transactions, RPC, and all Oviato patterns.
## Oviato Skill
The Oviato skill teaches your AI assistant the SDK's mental model, decision trees, recipes, and anti-patterns:
- **Three API surfaces** — chain-agnostic (`sendTransfer`, `signMessage`, `switchNetwork`, `getBalance`, auth), `evm.*` (reads, writes, tokens, NFTs, EIP-712, fees, receipts), `utxo.*` (PSBT signing)
- **Two packages, one API** — when to pick `@oviato/sdk` vs `@oviato/connect`, the exact import rules, why installing both is a foot-gun
- **Decision trees** — "which package?", "which surface for this task?", "popup or iframe?"
- **Response envelope pattern** — discriminated `{ status, type, data }` returns, how to narrow, why cancellation isn't a thrown exception
- **Nonce tracker** — what it is, why you never manage nonces, how auto-release works on pre-mempool errors
- **Recipes** — connect + session, native send, ERC-20 transfer, approve + use, typed-data signing, PSBT signing, multicall
- **Error-keyed troubleshooting** — replacement underpriced, nonce too low, missing revert data, popup blocked, etc.
- **Anti-patterns** — explicit "don't do this" list (install both packages, `EIP712Domain` in types, parsing `formatted` for math, etc.)
- **Scope limits** — explicitly tells the agent what's NOT supported (ordinals/inscriptions, Solana, `watchAsset`, `simulateContract`) so it won't invent APIs
The skill also instructs the agent to fetch `https://docs.oviato.com/llms-full.txt` for anything not covered — so it stays accurate even after the SDK evolves.
### View the Skill
You can view and download the full skill file here:
```
https://docs.oviato.com/skills/oviato.md
```
## How to Use
Once installed, your AI assistant will automatically apply Oviato knowledge when you:
- Ask it to implement authentication, wallet features, or transactions
- Reference `@oviato/connect` / `@oviato/sdk` / `evm.*` / `utxo.*` in your code
- Ask about PSBT signing, contract calls, EIP-712, gas estimation, or RPC calls
- Migrate code from viem or ethers
- Need help debugging Oviato-related errors
You don't need to prompt it with any special commands — the skill is always active in your project.
### Example Prompts
After installing the skill, try:
```
Set up Oviato authentication in my Next.js app with OviConnectProvider
and a connect button.
```
```
Add a "Send Bitcoin" button that calls sendTransfer when the user
clicks it. Show the txid on success.
```
```
I need to sign a PSBT hex string and broadcast it using the Oviato SDK.
Show me how to call signPsbt with broadcast enabled.
```
```
Fetch and display the user's wallet balance using the Oviato RPC client.
```
## How to Install
### Claude Code
Create the skill file in your project's `.claude/skills/` directory:
```bash
mkdir -p .claude/skills/oviato
curl -o .claude/skills/oviato/SKILL.md https://docs.oviato.com/skills/oviato.md
```
Claude Code automatically reads skills from `.claude/skills/` in your project. No additional configuration needed.
### Cursor
Create a `.cursor/rules/` directory and add the skill as a rule file:
```bash
mkdir -p .cursor/rules
curl -o .cursor/rules/oviato.mdc https://docs.oviato.com/skills/oviato.md
```
Cursor will automatically pick up rules from `.cursor/rules/` and apply them as context.
### Antigravity
Add the skill to your project's `.agents/skills/` directory:
```bash
mkdir -p .agents/skills
curl -o .agents/skills/oviato.md https://docs.oviato.com/skills/oviato.md
```
Antigravity reads skills from `.agents/skills/` at the workspace level. It also supports `GEMINI.md` and `AGENTS.md` at the project root for rules.
### Codex
Add the skill to your repository's `AGENTS.md` or Codex setup instructions. Download the skill file and include its contents:
```bash
curl -o AGENTS.md https://docs.oviato.com/skills/oviato.md
```
Codex will read `AGENTS.md` from your repository root as project context.
### Other Tools
For any AI coding tool that supports project-level context files:
1. Download the skill: [oviato.md](https://docs.oviato.com/skills/oviato.md)
2. Place it in your tool's context/rules directory
3. The AI will use it as persistent knowledge for your project
## Keeping the Skill Updated
The skill file at `https://docs.oviato.com/skills/oviato.md` is kept in sync with the latest SDK releases. To update:
```bash
# Re-download to overwrite your local copy
curl -o .claude/skills/oviato/SKILL.md https://docs.oviato.com/skills/oviato.md
```
Replace the path with wherever you installed the skill for your tool.
---
# Section: Packages
# Choosing a Package
> Which Oviato package to install — @oviato/sdk or @oviato/connect — and which imports to use.
Canonical URL: https://docs.oviato.com/packages/choosing
File: packages/choosing.mdx
Oviato ships **one SDK, two packages.** Pick based on your framework; the API surface is identical under the hood.
## TL;DR
Install **`@oviato/connect`**.
```bash
pnpm add @oviato/connect
```
Import everything from `@oviato/connect` (for actions) or `@oviato/connect/client` (for React hooks + components).
```tsx
// Actions — re-exports all of @oviato/sdk
import { evm, utxo, sendTransfer, signMessage } from '@oviato/connect';
// React hooks + components
import { OviConnectProvider, useOviConnect, ConnectButton } from '@oviato/connect/client';
```
Install **`@oviato/sdk`**.
```bash
pnpm add @oviato/sdk
```
Import directly.
```ts
import { evm, utxo, sendTransfer, signMessage, initialize, login } from '@oviato/sdk';
```
Works with Vue, Svelte, Angular, Solid, vanilla JS, Node, Deno, Bun.
## What's in each package
**`@oviato/sdk`** — the framework-agnostic core. Contains everything:
- Authentication (`login`, `register`, `authenticate`)
- Chain-agnostic actions (`signMessage`, `sendTransfer`, `switchNetwork`, `getBalance`)
- EVM-specific surface (`evm.*` — reads, writes, typed data, receipts)
- UTXO-specific surface (`utxo.*` — PSBT signing)
- RPC client (`rpc`, `rpcRequest`)
- x402 payments (`x402`)
- Types + bigint helpers
Runtime dep: `zustand` only. ~11 KB gzipped.
**`@oviato/connect`** — `@oviato/sdk` + React bindings on top. Two entry points:
- `@oviato/connect` — **re-exports 100% of `@oviato/sdk`.** Nothing new.
- `@oviato/connect/client` — adds `OviConnectProvider`, `useOviConnect`, `useOviStore`, `ConnectButton`, `Connect` (React 18+).
Because `@oviato/connect` re-exports everything from `@oviato/sdk`, a React
dev never needs to install both. One package, everything on hand.
## The one confusion we want to squash
You will see both `@oviato/sdk` and `@oviato/connect` in code samples on other sites and in GitHub issues. **They're the same API.** The function names, parameters, and return shapes are identical — `@oviato/connect` is a pass-through.
In this documentation we use `@oviato/connect` in examples by default (React is our primary audience). Swap to `@oviato/sdk` if you're using a different framework. Nothing else changes.
**Do not install both packages.** `@oviato/connect` already depends on
`@oviato/sdk` and re-exports everything. Installing both leads to duplicate
module instances and subtle state-sharing bugs — the Zustand store lives in
`@oviato/sdk`'s module scope, and two copies mean two stores.
## Import cheat sheet
Use this as a reminder when copy-pasting from mixed sources:
| You need… | `@oviato/connect` (React) | `@oviato/sdk` (any framework) |
|---|---|---|
| Auth (`login`, `register`, `authenticate`) | `@oviato/connect` | `@oviato/sdk` |
| Session (`getSession`, `clearSession`) | `@oviato/connect` | `@oviato/sdk` |
| Chain-agnostic actions (`sendTransfer`, `signMessage`) | `@oviato/connect` | `@oviato/sdk` |
| EVM namespace (`evm.*`) | `@oviato/connect` | `@oviato/sdk` |
| UTXO namespace (`utxo.*`) | `@oviato/connect` | `@oviato/sdk` |
| RPC client (`rpc`, `rpcRequest`) | `@oviato/connect` | `@oviato/sdk` |
| x402 payments (`x402`) | `@oviato/connect` | `@oviato/sdk` |
| Types, bigint helpers | `@oviato/connect` | `@oviato/sdk` |
| **React provider, hooks, components** | **`@oviato/connect/client`** | not available |
## Bundle sizes
- `@oviato/sdk` — ~11 KB gzipped (ESM)
- `@oviato/connect` (re-export entry) — thin layer over `@oviato/sdk`, ~12 KB gzipped
- `@oviato/connect/client` — adds React components, ~14 KB gzipped
All entry points are tree-shakeable — only imported functions ship.
## CDN usage (SDK only)
For quick prototypes without a build step, load `@oviato/sdk` from a CDN:
```html
```
The CDN bundle is UMD — everything is under the global `OviatoSDK`. Not recommended for production.
## Next steps
- [Quickstart](/getting-started/quickstart) — ship a working sign-in in 10 minutes
- [Core Concepts](/getting-started/core-concepts) — passkey flow, session scope, popup-vs-iframe, nonce tracker
- [Chain-Agnostic → Overview](/chain-agnostic/overview) — methods that work across BTC + EVM
---
# Section: Chain-Agnostic
# Chain-Agnostic Overview
> Methods that work identically across Bitcoin + EVM with one API. Oviato's unique value proposition.
Canonical URL: https://docs.oviato.com/chain-agnostic/overview
File: chain-agnostic/overview.mdx
This is what Oviato does that no other wallet SDK does cleanly: **one API that works across chain families.** Same function names, same return shapes — the SDK routes by the session's active network.
```ts
import { signMessage, sendTransfer, switchNetwork, getBalance } from '@oviato/connect';
// Same code, different chain each run
const bal = await getBalance({ address: session.address });
await signMessage('Hello from Oviato');
await sendTransfer({ recipient: { address, amount } });
```
No branches on `network === 'eth'` vs `network === 'bitcoin'`. The SDK dispatches internally.
## What's chain-agnostic
| Method | Works on | Dispatches by |
|---|---|---|
| [`signMessage`](/chain-agnostic/sign-messages) | BTC + EVM | session network |
| [`sendTransfer`](/chain-agnostic/send-transfers) | BTC + EVM (native only) | session network |
| [`switchNetwork`](/chain-agnostic/session-and-network) | any → any | target network |
| [`getBalance`](/chain-agnostic/get-balance) | BTC + EVM (via `rpc`) | session network |
| Auth: [`login`](/chain-agnostic/authentication), `register`, `authenticate` | any | n/a — WebAuthn is universal |
| Session: `getSession`, `clearSession`, `isAuthenticated` | any | n/a |
## What's chain-specific (lives elsewhere)
When you're doing something that genuinely only works on one chain family, use the dedicated namespace:
- **EVM:** contract calls, token reads, NFTs, typed data, gas fees → [`evm.*`](/evm/overview)
- **UTXO:** PSBT signing → [`utxo.*`](/utxo/overview)
The namespaces exist precisely so you don't accidentally call an EVM method on a Bitcoin session. Runtime errors become impossible compile-time mistakes.
## The dispatch model
Every chain-agnostic method inspects the session's network and chooses the right backend path:
```
sendTransfer({ recipient })
↓
session.network
├─ 'bitcoin' / 'bitcointestnet' → UTXO prepare path (PSBT, fee_rate, multi-recipient supported)
└─ 'eth' / 'base' / 'basesepolia' / 'ethsepolia' → EVM prepare path (nonce, gas, value as wei)
```
Return shapes are aligned where possible:
- `sendTransfer` → `{ txid, network, explorerUrl }` — same across both chain families
- `getBalance` → discriminated union: `{ chainType: 'utxo', raw: number, confirmed, unconfirmed, ... }` OR `{ chainType: 'evm', raw: bigint, ... }` so your code narrows explicitly
## Amount unit conventions
Different chains have different native units. The SDK uses whatever's overflow-safe for the chain:
| Chain family | Unit | Type |
|---|---|---|
| Bitcoin + other UTXO | satoshis (1 BTC = 10⁸ sats) | `number` |
| Ethereum + EVM | wei (1 ETH = 10¹⁸ wei) | `bigint` |
`number` overflows past 2⁵³−1 (~9×10¹⁵). UTXO amounts never get that
big (max BTC supply × 10⁸ ≈ 2.1×10¹⁵). EVM amounts easily exceed it, so
we use `bigint` there. `sendTransfer` accepts both — pass `number` or
`bigint` based on the active chain.
```ts
// Bitcoin — satoshis as number
await sendTransfer({
recipient: { address: 'bc1q...', amount: 10000 }, // 0.0001 BTC
});
// Ethereum — wei as bigint
await sendTransfer({
recipient: { address: '0x...', amount: 10n ** 15n }, // 0.001 ETH
});
```
## When "chain-agnostic" isn't quite true
A few caveats worth calling out:
- **`sendTransfer` on EVM** sends *native* coin (ETH/Base ETH/etc.) only. For ERC-20 / NFT / contract calls, use [`evm.writeContract`](/evm/writes).
- **`sendTransfer` with multiple recipients** works only on UTXO (Bitcoin supports multi-output natively; EVM doesn't in one tx).
- **`signMessage`** produces different signature formats: EIP-191 on EVM, Bitcoin message signing on UTXO. Verification needs to know which.
## When to use chain-agnostic vs chain-specific
**Chain-agnostic** when you're writing code that should work across chain families — a generic "connect + send" UI, a faucet, a multi-chain dashboard.
**Chain-specific** when you're doing something that genuinely only makes sense for one chain — a DeFi app (EVM), a PSBT-heavy Bitcoin flow, a DEX swap.
Mixing is fine:
```ts
import { getBalance, signMessage, evm } from '@oviato/connect';
async function sendUsdcWithAuthProof(amount: bigint, recipient: `0x${string}`) {
// Chain-agnostic: fetch balance for a pre-check
const { balance } = await getBalance({ address: session.address });
// Chain-agnostic: sign an auth message for off-chain logging
await signMessage(`Authorize transfer of ${amount} at ${Date.now()}`);
// Chain-specific: the actual USDC transfer (EVM-only)
return evm.writeContract({
address: USDC,
abi: ERC20_ABI,
functionName: 'transfer',
args: [recipient, amount],
});
}
```
## Explore
---
# Authentication
> Learn how to authenticate users with passkeys using @oviato/connect
Canonical URL: https://docs.oviato.com/chain-agnostic/authentication
File: chain-agnostic/authentication.mdx
@oviato/connect provides three authentication methods: smart authentication, explicit login, and explicit registration. This guide covers all authentication flows.
## Authentication Methods
### Smart Authentication (Recommended)
The `authenticate()` function automatically detects whether the user is new or returning:
```javascript
import { authenticate } from "@oviato/connect";
const result = await authenticate({
username: "alice", // optional: pre-fill username
});
if (result.status === "success") {
console.log("Connected:", result.data.address);
console.log("Username:", result.data.username);
console.log("Network:", result.data.network);
}
```
**When to use**: Most of the time. This provides the best user experience.
### Explicit Login
For returning users who already have a passkey:
```javascript
import { login } from "@oviato/connect";
const result = await login({
username: "alice", // optional: pre-fill username
});
if (result.status === "success") {
console.log("Logged in:", result.data.address);
}
```
**When to use**: When you know the user already has an account (e.g., they clicked "Login" instead of "Sign Up").
### Explicit Registration
For new users creating an account:
```javascript
import { register } from "@oviato/connect";
const result = await register({
username: "bob", // required for registration
});
if (result.status === "success") {
console.log("Registered:", result.data.address);
}
```
**When to use**: When you have separate sign-up and login flows.
## Authentication Options
### Pre-filling Username
You can pre-fill the username field:
```javascript
await authenticate({
username: "alice@ovi.id",
});
```
### Without Pre-filled Username
Let users enter their own username:
```javascript
await authenticate();
// Opens modal with empty username field
```
## Response Handling
All authentication methods return a `BridgeResponse` object:
```typescript
type BridgeResponse = {
status: "success" | "error";
data?: T;
message?: string;
};
```
### Success Response
```javascript
const result = await authenticate();
if (result.status === "success") {
console.log("Session data:", result.data);
// result.data contains UserSession
}
```
### Error Handling
```javascript
const result = await authenticate();
if (result.status === "error") {
console.error("Authentication failed:", result.message);
// Handle specific errors
if (result.message.includes("cancelled")) {
console.log("User cancelled authentication");
} else if (result.message.includes("timeout")) {
console.log("Authentication timed out");
}
}
```
## Session Data
After successful authentication, access the session:
```javascript
import { getSession } from "@oviato/connect";
const session = getSession();
if (session) {
console.log("Username:", session.username);
console.log("Address:", session.address);
console.log("Public Key:", session.publicKey);
console.log("Network:", session.network);
console.log("HD Keys:", session.hdKeys);
}
```
### Session Type
```typescript
type UserSession = {
username: string; // OVI.ID username
address: string; // Current network address
publicKey: string; // Current network public key
network: string; // Active network
networkType: string; // Network type (e.g., 'utxo', 'evm')
networkEnvironment: string; // Environment ('mainnet', 'testnet')
hdKeys: {
mainnet: string; // Mainnet HD public key
testnet: string; // Testnet HD public key
};
auth?: {
token: string; // RPC auth token
expires: string; // Token expiration (ISO 8601)
};
};
```
## Checking Authentication Status
```javascript
import { isAuthenticated, getSession } from "@oviato/connect";
// Quick check
if (isAuthenticated()) {
console.log("User is authenticated");
}
// With session data
const session = getSession();
if (session) {
console.log("User is authenticated as:", session.username);
}
```
## Logout
Clear the user session:
```javascript
import { clearSession } from "@oviato/connect";
// Logout user
clearSession();
// Verify logout
console.log("Authenticated:", isAuthenticated()); // false
```
## Complete Authentication Example
Here's a complete example with all authentication flows:
```javascript
import {
initialize,
authenticate,
login,
register,
getSession,
isAuthenticated,
clearSession,
} from "@oviato/connect";
// Initialize SDK
await initialize({
appId: "your-app-id",
network: "bitcoinmainnet",
});
// Check if already authenticated
if (isAuthenticated()) {
const session = getSession();
console.log("Already logged in as:", session.username);
} else {
// Authenticate user
const result = await authenticate();
if (result.status === "success") {
const session = getSession();
console.log("Successfully authenticated!");
console.log("Username:", session.username);
console.log("Address:", session.address);
} else {
console.error("Authentication failed:", result.message);
}
}
// Later: logout
function handleLogout() {
clearSession();
console.log("Logged out");
}
```
## Framework-Specific Examples
### Vue 3
```vue
{{ error }}
Welcome, {{ session.username }}!
Address: {{ session.address }}
```
### Svelte
```svelte
{#if error}
{error}
{/if}
{#if session}
Welcome, {session.username}!
Address: {session.address}
{:else}
{/if}
```
## WebAuthn / Passkeys
Oviato uses WebAuthn for secure, passwordless authentication:
### How It Works
1. **Registration**: Creates a new passkey tied to the user's device
2. **Authentication**: Uses the passkey to verify the user's identity
3. **Signing**: Uses the passkey to sign messages and transactions
### Browser Support
WebAuthn is supported in:
- Chrome 67+
- Safari 14+
- Firefox 60+
- Edge 18+
### Requirements
- **HTTPS**: Required in production (localhost works for development)
- **Biometrics**: Touch ID, Face ID, or security key
- **User Gesture**: Must be triggered by user interaction (button click)
## Troubleshooting
### "WebAuthn not supported"
Ensure:
- Using HTTPS (required in production)
- Browser supports WebAuthn
- Device has biometric hardware or security key
### "User cancelled"
User closed the authentication dialog or cancelled biometric prompt.
### "Username already exists"
When using `register()`, the username is already taken. Try `login()` instead or use `authenticate()` for auto-detection.
### "No passkey found"
When using `login()`, no passkey exists for this username. User should `register()` first or use `authenticate()`.
### Session not persisting
Check:
- Cookies are enabled
- LocalStorage is accessible
- Domain is in allowed domains list (dashboard)
- Using HTTPS in production
## Security Best Practices
### 1. Verify Session on Server
Never trust client-side session data alone. Use the `auth.token` for server-side verification:
```javascript
const session = getSession();
// Send token to your backend
fetch("/api/verify", {
headers: {
Authorization: `Bearer ${session.auth.token}`,
},
});
```
### 2. Check Token Expiration
```javascript
const session = getSession();
if (session?.auth) {
const expiresAt = new Date(session.auth.expires);
const now = new Date();
if (now > expiresAt) {
console.warn("Auth token expired, re-authenticate");
clearSession();
}
}
```
### 3. Handle Network Changes
When users switch networks, re-authenticate if needed:
```javascript
import { switchNetwork } from "@oviato/connect";
const result = await switchNetwork("ethmainnet");
if (result.status === "success") {
// Session automatically updated
const session = getSession();
console.log("New network:", session.network);
console.log("New address:", session.address);
}
```
## Next Steps
- **[Wallet Operations](/chain-agnostic/overview)** - Learn about signing and transactions
---
# Session & Network
> Read the active session, switch networks, and refresh the JWT. Works identically across Bitcoin + EVM.
Canonical URL: https://docs.oviato.com/chain-agnostic/session-and-network
File: chain-agnostic/session-and-network.mdx
Every SDK method that touches a wallet reads from the session — a small in-memory object that holds the user's address, network, hdkeys, and auth token. This page covers reading, updating, and clearing it.
## Read the session
```ts
import { getSession } from '@oviato/connect';
const session = getSession();
if (!session) {
// User isn't logged in
} else {
console.log(session.username); // 'alice'
console.log(session.address); // 'bc1q...' or '0x...'
console.log(session.network); // 'bitcoin' | 'eth' | 'base' | ...
console.log(session.networkType); // 'utxo' | 'evm'
}
```
### React hook equivalent
```tsx
import { useOviConnect } from '@oviato/connect/client';
function Profile() {
const { session, isLoading, disconnect } = useOviConnect();
if (isLoading) return
Loading…
;
if (!session) return
Please connect
;
return (
{session.username}
);
}
```
## Check whether authenticated
```ts
import { isAuthenticated } from '@oviato/connect';
if (isAuthenticated()) {
// Session exists AND JWT hasn't expired
}
```
This is stricter than `!!getSession()` — it also checks that the bearer token is still valid (not expired).
## Clear the session
```ts
import { clearSession } from '@oviato/connect';
clearSession();
```
Wipes the in-memory store + the localStorage cache. The user's passkey isn't affected — they can `login()` again and pick up where they left off.
Server-side sessions aren't revoked — only the client-side copy is dropped.
If you need to revoke the JWT server-side (account takeover, compromised
device), call `auth.revokeToken()` via the dashboard.
## Switch network
Changes the active chain. Requires a passkey tap because the SDK re-signs the session JWT for the new chain — this prevents a stolen JWT from being silently repointed.
```ts
import { switchNetwork, Network } from '@oviato/connect';
const r = await switchNetwork(Network.BASEMAINNET);
if (r.status === 'success' && r.type === 'switchNetworkSuccess') {
console.log('Now on', r.data.network);
console.log('New address:', r.data.session.address);
}
```
### What changes
Switching networks re-derives the user's address (addresses are deterministic per (user, app, network)) and re-issues the JWT with a chain-scoped claim. After a successful switch:
- `session.network` → the new network
- `session.networkType` → `'utxo'` or `'evm'` based on the new chain
- `session.address` → the deterministic address on the new chain
- The JWT's `network` claim matches — required for write methods
### Supported networks
- `Network.BTCMAINNET` (`'bitcoin'`)
- `Network.BTCTESTNET` (`'bitcointestnet'`)
- `Network.ETHMAINNET` (`'eth'`)
- `Network.ETHSEPOLIA` (`'ethsepolia'`)
- `Network.BASEMAINNET` (`'base'`)
- `Network.BASESEPOLIA` (`'basesepolia'`)
More come online via the dashboard. If you try to switch to an unsupported network for your app, the call fails with a clear error.
### Ethers-style alias
```ts
import { evm } from '@oviato/connect';
await evm.switchChain(Network.BASEMAINNET);
```
Identical to `switchNetwork` — kept for ethers-ecosystem familiarity.
## The session shape
```ts
type Session = {
username: string;
address: string;
publicKey: string;
network: Network;
networkType: 'utxo' | 'evm';
hdKeys?: { mainnet: string; testnet: string; solana: string };
pk?: string;
auth?: {
token: string; // Bearer JWT
expires: string; // ISO 8601
};
};
```
## Subscribe to session changes (React)
```tsx
import { useOviConnect } from '@oviato/connect/client';
function App() {
const { session } = useOviConnect();
useEffect(() => {
if (session) {
console.log('User logged in:', session.address);
} else {
console.log('User logged out');
}
}, [session]);
}
```
For vanilla JS, the underlying store is Zustand — subscribe directly:
```ts
import { oviStore } from '@oviato/connect';
const unsub = oviStore.subscribe((state) => {
console.log('Session changed:', state.session);
});
```
## Related
- [Authentication](/chain-agnostic/authentication) — login/register/authenticate flows
- [Core Concepts → Sessions are chain-scoped](/getting-started/core-concepts#sessions-are-chain-scoped)
---
# Sign Messages
> Sign arbitrary messages with the user's passkey. Same API across Bitcoin + EVM, different wire format per chain.
Canonical URL: https://docs.oviato.com/chain-agnostic/sign-messages
File: chain-agnostic/sign-messages.mdx
`signMessage` produces a cryptographic signature over an arbitrary string, authenticated by the user's passkey. Works on every chain family — the signature format is dispatched by the session's network type.
```ts
import { signMessage } from '@oviato/connect';
const r = await signMessage('Hello, Oviato!');
if (r.status === 'success' && r.type === 'signMessageSuccess') {
console.log(r.data.signature);
}
```
## What you get back
```ts
type Result =
| { status: 'success'; type: 'signMessageSuccess'; data: { signature: string; message: string; publicKey: string } }
| { status: 'error'; message: string };
```
Narrow on `status`:
```ts
const r = await signMessage('Verify my ownership');
if (r.status === 'success' && r.type === 'signMessageSuccess') {
await sendToBackend({
message: r.data.message,
signature: r.data.signature,
publicKey: r.data.publicKey,
address: session.address,
});
}
```
## How the signature is formatted
Different chains, different wire formats — same API.
On EVM networks, messages are signed per [EIP-191](https://eips.ethereum.org/EIPS/eip-191) — the message is prefixed with `\x19Ethereum Signed Message:\n` before signing, so you can't trick a user into signing a raw transaction hash.
```ts
// Signed message: '\x19Ethereum Signed Message:\n13Hello, world!'
const r = await signMessage('Hello, world!');
// r.data.signature → '0x…' (65 bytes: r + s + v)
```
Verify with [`evm.verifyMessage`](/evm/reads#eip-191-plain-messages):
```ts
import { evm } from '@oviato/connect';
const valid = await evm.verifyMessage({
address: session.address,
message: r.data.message,
signature: r.data.signature,
});
```
On UTXO networks, messages are signed using the [Bitcoin Signed Message](https://en.bitcoin.it/wiki/Message_signing) standard — a prefix of `\x18Bitcoin Signed Message:\n` wrapped in a double-sha256, signed with the address's private key.
```ts
const r = await signMessage('Hello, world!');
// r.data.signature → base64 string (65 bytes raw → base64)
```
Verification uses standard Bitcoin libraries (bitcoinjs, bitcore, etc.). The SDK doesn't ship a built-in verifier today — pass the signature to your backend for verification.
## Common use cases
### Prove wallet ownership
Before a backend grants a user access to a premium feature, have them sign a challenge that includes a nonce + timestamp.
```ts
// 1. Fetch a challenge from your backend
const { challenge } = await fetch('/api/auth/challenge').then((r) => r.json());
// challenge example: 'Sign to prove ownership: nonce=abc123 ts=1700000000'
// 2. Have the user sign it
const r = await signMessage(challenge);
if (r.status !== 'success' || r.type !== 'signMessageSuccess') return;
// 3. Send back for verification
const { granted } = await fetch('/api/auth/verify', {
method: 'POST',
body: JSON.stringify({
message: r.data.message,
signature: r.data.signature,
address: session.address,
}),
}).then((r) => r.json());
```
### Off-chain consent
Get a signed consent to avoid an on-chain gas cost:
```ts
const intent = `I authorize a transfer of 100 USDC to 0x... at ${Date.now()}`;
const r = await signMessage(intent);
// Store r.data.signature for later — execute on-chain if/when needed
```
### Sign-in with [SIWE](https://eips.ethereum.org/EIPS/eip-4361) (EVM)
SIWE is the canonical "sign in with Ethereum" flow. Build the message per spec, sign it:
```ts
const domain = 'example.com';
const statement = 'Sign in to Example';
const nonce = crypto.randomUUID();
const issuedAt = new Date().toISOString();
const message = `${domain} wants you to sign in with your Ethereum account:
${session.address}
${statement}
URI: https://${domain}
Version: 1
Chain ID: 1
Nonce: ${nonce}
Issued At: ${issuedAt}`;
const r = await signMessage(message);
// Send r.data.signature + message to your backend for verification
```
## Security notes
**Never have users sign an untrusted message.** Messages can be structured
to look like plain text but actually encode a transaction. Always show
users the full message content before signing, and treat any signing
request as deliberate on your part.
- Signatures produced by `signMessage` authenticate the user's passkey, not their on-chain wallet directly. Verification recovers the *public key*, which you then match against the expected session address.
- Nonces + timestamps in signed messages prevent replay attacks.
- Oviato's bridge UI displays the full message to the user before they approve. For ASCII messages this is straightforward; for binary payloads users see a hex dump — avoid binary content in user-facing flows.
## Options
```ts
await signMessage('...', {
method: 'popup', // or 'iframe' — how the approval UI opens
});
```
Defaults to **iframe** — the SDK only falls back to popup on localhost / non-HTTPS or on Safari/iOS for passkey operations. In production you get iframe unless you override.
## Related
- [EVM Writes → signTypedData](/evm/writes#sign-eip-712-typed-data) — structured EIP-712 data
- [EVM Reads → Verify](/evm/reads#signature-verification-offchain) — recover + check a signature
---
# Send Transfers
> Send native coin to an address. One API for Bitcoin (satoshis) and EVM chains (wei). Single recipient or batch on UTXO.
Canonical URL: https://docs.oviato.com/chain-agnostic/send-transfers
File: chain-agnostic/send-transfers.mdx
`sendTransfer` moves native coin (BTC, ETH, Base ETH, etc.) from the session address to one or more recipients. Dispatched by session network.
```ts
import { sendTransfer } from '@oviato/connect';
const r = await sendTransfer({
recipient: { address: '0x742d35Cc...', amount: 10n ** 15n },
fee: 'auto',
});
if (r.status === 'success' && r.type === 'sendTransferSuccess') {
console.log('TxID:', r.data.txid);
console.log('Explorer:', r.data.explorerUrl);
}
```
For ERC-20 / NFT / contract calls on EVM, use
[`evm.writeContract`](/evm/writes) instead — `sendTransfer` is
*native coin only*.
## Amount units per chain
| Chain | Unit | Type |
|---|---|---|
| Bitcoin | satoshis (1 BTC = 100 000 000 sats) | `number` |
| Ethereum, Base | wei (1 ETH = 10¹⁸ wei) | `bigint` (preferred), `string`, or `number` for small values |
```ts
// Bitcoin
await sendTransfer({
recipient: { address: 'bc1q...', amount: 10_000 }, // 0.0001 BTC
});
// EVM — use bigint for overflow safety
await sendTransfer({
recipient: { address: '0x...', amount: 10n ** 15n }, // 0.001 ETH
});
```
## Single vs batch recipients
```ts
await sendTransfer({
recipient: { address, amount },
fee: 'auto',
memo: 'Payment for services', // optional
});
```
Works on Bitcoin + EVM.
Bitcoin natively supports multiple outputs in one transaction. Save on fees vs sending N separate txs.
```ts
await sendTransfer({
recipients: [
{ address: 'bc1q...', amount: 10000 },
{ address: 'bc1p...', amount: 5000 },
{ address: 'bc1q...', amount: 7500 },
],
fee: 'auto',
memo: 'Splitting a payment',
});
```
Attempting `recipients: []` on an EVM chain returns an error — multi-recipient doesn't exist in EVM.
## Fee handling
Fees are chain-specific, but the API is unified via semantic tiers:
```ts
await sendTransfer({
recipient: { address, amount },
fee: 'fast', // or 'medium' | 'slow' | 'auto' |
});
```
- `auto` (default) — sensible default, ≈ `medium`
- `fast`, `medium`, `slow` — explicit tiers. Map to fee multipliers per chain.
- `` — raw fee rate. On Bitcoin it's sat/vB; on EVM it's wei gas-price (not recommended — prefer tiers).
The bridge UI shows three tiers with current cost estimates before the user confirms, and lets them pick inline.
## The full response shape
```ts
type Result =
| { status: 'success'; type: 'sendTransferSuccess'; data: { txid, network, explorerUrl } }
| { status: 'error'; message: string };
```
Always narrow on `status`:
```ts
const r = await sendTransfer({ recipient: { address, amount } });
if (r.status === 'success' && r.type === 'sendTransferSuccess') {
console.log(r.data.txid);
} else if (r.status === 'error') {
console.error(r.message);
}
```
EVM writes return `data.hash` via `evm.sendTransaction` /
`evm.writeContract`. `sendTransfer` uses `data.txid` universally. Both
are the same 0x-prefixed hex — the field name is different for
historical reasons.
## Memo / OP_RETURN
Bitcoin supports a small arbitrary payload per transaction via `OP_RETURN`:
```ts
await sendTransfer({
recipient: { address, amount },
memo: 'Invoice #1234',
});
```
Max size: ~80 bytes. Larger memos are rejected.
On EVM, `memo` is ignored — `sendTransaction`'s `data` field serves the same purpose but has different semantics (it's calldata, not a comment).
## Full parameter list
| Field | Type | Notes |
|---|---|---|
| `recipient` | `{ address, amount }` | For single-recipient sends. XOR with `recipients`. |
| `recipients` | `[{ address, amount }, ...]` | UTXO only. XOR with `recipient`. |
| `fee` | `'auto' \| 'fast' \| 'medium' \| 'slow' \| number` | Default `'auto'`. |
| `memo` | `string` | UTXO only. Max ~80 bytes. |
| `method` | `'popup' \| 'iframe'` | How the approval UI opens. |
## Gotchas
**Amount is smallest unit, always.** Bitcoin gives you satoshis, EVM gives
you wei. `amount: 1` on Bitcoin is 1 satoshi (0.00000001 BTC), not 1 BTC.
Use `parseUnits('1', 18)` for ETH or `10000` for 0.0001 BTC.
- **Dust outputs.** On Bitcoin, amounts below the dust threshold (≈546 sats) are rejected.
- **Insufficient funds.** Not just `amount > balance`, but `amount + fee > balance`. The SDK fetches a fee estimate before confirming.
- **Self-sends.** Sending to your own address works — useful for testing. The fee still applies.
## Preview before sending
To show a transaction summary before the user confirms (cost, USD value, decoded recipient), call `rpc.prepareTx` with the same params:
```ts
import { rpc } from '@oviato/connect';
const preview = await rpc.prepareTx({
recipient: { address, amount },
fee: 'auto',
publicKey: session.publicKey,
address: session.address,
});
console.log(preview.result.feeTotalUsd);
```
## Related
- [EVM Writes](/evm/writes) — contract calls, token transfers, EIP-712
- [UTXO → Sign PSBT](/utxo/sign-psbt) — advanced Bitcoin signing via PSBTs
---
# Get Balance
> Fetch native coin balance for any address on any chain. Discriminated return — EVM gives bigint wei, UTXO gives confirmed + unconfirmed satoshis.
Canonical URL: https://docs.oviato.com/chain-agnostic/get-balance
File: chain-agnostic/get-balance.mdx
Get balance uses the RPC client to dispatch to the right backend based on the session's active network. The return shape differs per chain family so you can access chain-specific fields (like UTXO's confirmed/unconfirmed split) when needed.
```ts
import { rpc, getSession } from '@oviato/connect';
const session = getSession();
const r = await rpc.getBalance({ address: session.address });
if (r.result) {
console.log(r.result.balance.formatted); // '1.2345'
console.log(r.result.symbol); // 'ETH' | 'BTC' | 'USDC' etc.
}
```
`getBalance` lives under the `rpc` client today. A flat top-level
`getBalance({ address })` with the spec-flattened return shape is on the
roadmap — the current form works and is stable for use.
## Return shape
The response is a discriminated union keyed on `chainType`:
### EVM
```ts
{
address: string;
network: string; // 'eth' | 'base' | ...
networkName: string; // 'Ethereum' | 'Base' | ...
chainType: 'evm';
balance: {
raw: string; // wei as decimal string (avoids bigint-over-JSON)
formatted: string; // '1.2345' (native units)
symbol: string; // 'ETH' | 'BASE' | ...
decimals: number; // 18
};
symbol: string;
decimals: number;
}
```
### UTXO (Bitcoin)
```ts
{
address: string;
network: string; // 'bitcoin' | 'bitcointestnet' | ...
networkName: string;
chainType: 'utxo';
balance: {
raw: number; // satoshis
formatted: string; // '0.0125'
symbol: string; // 'BTC' | 'tBTC'
decimals: number; // 8
confirmed: { // mined-only portion
raw: number; formatted: string; symbol: string; decimals: number;
};
unconfirmed: { // pending mempool portion
raw: number; formatted: string; symbol: string; decimals: number;
};
};
symbol: string;
decimals: number;
}
```
EVM `raw` is a *decimal string* (e.g. `"1234000000000000"`), not a bigint —
because bigint can't JSON-serialize. Parse with `BigInt(raw)` or use
`fromWire()` to get back a bigint. UTXO `raw` is a number (safe since
satoshi amounts don't overflow 2⁵³).
## Typical usage
### Display a user's balance
```tsx
import { rpc, useOviConnect } from '@oviato/connect/client';
import { useEffect, useState } from 'react';
function BalanceView() {
const { session } = useOviConnect();
const [balance, setBalance] = useState(null);
useEffect(() => {
if (!session) return;
rpc.getBalance({ address: session.address }).then((r) => {
if (r.result) setBalance(r.result.balance.formatted);
});
}, [session]);
if (!session) return null;
return
{balance ?? '…'} {/* symbol */}
;
}
```
### Pre-check before sending
```ts
import { rpc, getSession, fromWire } from '@oviato/connect';
async function canSend(amountWei: bigint) {
const session = getSession();
const r = await rpc.getBalance({ address: session!.address });
if (!r.result || r.result.chainType !== 'evm') return false;
const available = fromWire(r.result.balance.raw); // string → bigint
return available >= amountWei;
}
```
### Narrow by chain type
```ts
const r = await rpc.getBalance({ address });
if (!r.result) return;
if (r.result.chainType === 'evm') {
// r.result.balance is { raw: string, formatted, symbol, decimals }
console.log('ETH balance:', r.result.balance.formatted);
} else {
// r.result.balance has confirmed + unconfirmed
console.log('Confirmed BTC:', r.result.balance.confirmed.formatted);
console.log('Pending BTC:', r.result.balance.unconfirmed.formatted);
}
```
## How it's fetched
- **EVM** — Chainstack RPC `eth_getBalance(address, 'latest')`. Returns wei, SDK formats with chain decimals.
- **UTXO** — Our Bitcoin indexer (built on mempool.space). Returns confirmed + unconfirmed separately since the UTXO model distinguishes them on-chain.
Both paths go through Oviato's RPC API — no direct Chainstack / indexer calls from your dapp.
## What this isn't
`getBalance` is for the *native coin* only. For ERC-20 balances, use [`evm.getTokenBalance`](/evm/reads#token-balance). For multiple tokens in one call, use [`evm.multicall`](/evm/reads#batch-reads-with-multicall).
## Related
- [EVM → Token Balance](/evm/reads#token-balance) — ERC-20 balances
- [Send Transfers](/chain-agnostic/send-transfers) — once you know the balance, send some
---
# Section: EVM
# EVM Overview
> The evm namespace — reads, writes, popup flow, nonce tracker, envelope pattern. Everything you need to know before writing EVM code with Oviato.
Canonical URL: https://docs.oviato.com/evm/overview
File: evm/overview.mdx
Every EVM-specific method in Oviato lives under the `evm` namespace — contract calls, block reads, fee estimates, transaction sending, typed-data signing, receipt polling. Chain-agnostic actions (sign message, send native transfer) stay at the top level.
```ts
import { evm } from '@oviato/connect';
// Reads (no popup)
const block = await evm.getBlockNumber();
const bal = await evm.getTokenBalance({ tokenAddress });
// Writes (popup, user approves with passkey)
const r = await evm.sendTransaction({ to, value });
const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash });
```
New to the SDK? Start with the [Quickstart](/getting-started/quickstart)
to get a working app, then come back here for the EVM surface.
## Why a namespace?
An early version of the SDK exported `readContract` at the top level. A Bitcoin-session dev would import it, call it, and get a runtime error — *"readContract is EVM-only"*.
The namespace fixes that: `evm.readContract` is unambiguous at the type level. You can't call it on a Bitcoin session without knowing you're doing something wrong. The `utxo.*` namespace mirrors this for Bitcoin-specific methods.
Chain-agnostic actions (`signMessage`, `sendTransfer`, `switchNetwork`, `getBalance`) stay at the top level — they're the multi-chain DX.
## What's in the namespace
**Reads** ([full reference](/evm/reads)) — no popup, no user interaction.
- Contract reads: `readContract`, `multicall`
- Token: `getTokenBalance`, `getTokenAllowance`, `getTokens`
- NFT: `getNftOwner`, `getNftBalance`, `getNftMetadata`
- Chain: `getBlockNumber`, `getBlock`, `getTransaction`, `getTransactionReceipt`, `getTransactionCount`, `getCode`, `getStorageAt`
- Fees: `getGasPrice`, `estimateGas`, `estimateFeesPerGas`, `estimateMaxPriorityFeePerGas`, `getFeeHistory`, `getFeeData`
- Verify: `verifyMessage`, `verifyTypedData`
- Wait: `waitForTransactionReceipt`, `waitForTransaction`
**Writes** ([full reference](/evm/writes)) — opens popup/iframe, requires passkey.
- `sendTransaction` — generic EVM transaction
- `writeContract` — ABI-driven contract call (specialized confirmation sheets)
- `signTypedData` — EIP-712 structured data signing
- `switchChain` — ethers-style alias for `switchNetwork`
## The write flow
Here's what happens when you call `evm.sendTransaction` or `evm.writeContract`.
**Prepare.** SDK calls `connect_prepareEvm` on our API. Server reserves a nonce from the tracker, estimates gas, computes three fee tiers (fast/medium/slow), and — for ABI calls — decodes the function into a category (`erc20_transfer`, `erc721_transfer`, `erc20_approve`, `contract_call`).
**Confirm.** SDK opens a popup or iframe to `connect.oviato.com` with the prepared transaction. The user sees a specialized sheet based on the decoded category — "Transfer Token", "Transfer NFT", "Approve", or the generic call UI with decoded args.
**Sign.** User taps "Approve", the bridge prompts for the passkey (WebAuthn), and signs the transaction server-side. The raw signed hex never reaches your dapp.
**Broadcast.** Bridge calls `connect_broadcastEvm`, which submits via Chainstack and commits the nonce tracker.
**Resolve.** SDK returns `{ status: 'success', type: '...', data: { hash, network, explorerUrl } }`.
You don't handle any of this. The SDK orchestrates the full flow — you
await one function and get back a tx hash.
## The response envelope
Every write resolves to a discriminated envelope, never a raw value.
```ts
type SendTransactionResult =
| { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } }
| { status: 'error'; message: string };
```
Narrow on `status` before reading `.data`:
```ts
const r = await evm.sendTransaction({ to, value });
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log(r.data.hash); // ← safely typed
} else {
console.error(r.message);
}
```
This differs from ethers/viem which return a raw hash and throw on error. The envelope makes cancellation ("user closed the popup") non-exceptional — you get a normal error result, not a thrown exception. See [Migrating from viem](/evm/migrating-from-viem) for a thin adapter if you prefer the raw-hash style.
## The nonce tracker
Rapid-fire EVM sends are a pain on raw JSON-RPC — `eth_getTransactionCount('pending')` often lags what's actually in the mempool, so firing three transactions in 100 ms can hand out the same nonce three times.
Oviato's API maintains a server-side nonce counter per `(address, chain)`. You call `evm.sendTransaction` three times in a row; each call reserves a unique sequential nonce atomically in D1.
```ts
// All three get unique, sequential nonces automatically.
await Promise.all([
evm.sendTransaction({ to, value: 1n }),
evm.sendTransaction({ to, value: 2n }),
evm.sendTransaction({ to, value: 3n }),
]);
```
When a broadcast fails pre-mempool (`replacement underpriced`, `nonce too
low`, `already known`, `underpriced`), the tracker automatically releases
the nonce. Your next send picks up cleanly with the right number. No
manual recovery required.
If you need to override (advanced — e.g. migrating a legacy system with its own nonce state), pass `nonce:` explicitly:
```ts
await evm.sendTransaction({ to, value, nonce: 42 });
```
## Bigint everywhere (for overflow-safe math)
EVM units can exceed `Number.MAX_SAFE_INTEGER` (2⁵³ − 1). We use `bigint` for anything that can overflow: balances, gas, fees, values.
```ts
// bigint literals — note the `n` suffix
const value = 10n ** 18n; // 1 ETH in wei
const amount = 1_000_000n; // 1 USDC (6 decimals)
// For display — never parse `formatted` back for math
const { raw, formatted } = await evm.getTokenBalance({ tokenAddress });
console.log(formatted); // '1234.56'
// For parsing user input into bigint
import { parseUnits, formatUnits } from '@oviato/connect';
const wei = parseUnits('0.5', 18); // 500000000000000000n
const display = formatUnits(1234500n, 6); // '1.2345'
```
## Supported EVM networks
- `ethmainnet` (Ethereum)
- `ethsepolia` (Ethereum Sepolia testnet)
- `base` (Base mainnet)
- `basesepolia` (Base Sepolia testnet)
More chains come online via the dashboard. If your app's supported networks don't include an EVM chain, SDK calls will reject early — no silent misroutes.
## Session scope
A session is scoped to one network. When a user is on Bitcoin, `evm.*` methods throw — EVM is not the active network. Use `switchNetwork` to re-scope the session.
```ts
import { switchNetwork, Network } from '@oviato/connect';
await switchNetwork(Network.ETHMAINNET);
// Session now Ethereum-scoped. evm.* works.
```
Switching networks requires the user to tap their passkey once — the server
re-issues the JWT with the new chain's claims. This is a security measure
(a stolen JWT can't be silently repointed).
## Where to go next
---
# EVM Reads
> Read contract state, token balances, NFT metadata, chain state, and fees on EVM networks. No popup, no passkey.
Canonical URL: https://docs.oviato.com/evm/reads
File: evm/reads.mdx
Every read below is a plain JSON-RPC call routed through our backend. No popup, no WebAuthn, no user interaction. The active network is picked up from the session.
```ts
import { evm } from '@oviato/connect';
const block = await evm.getBlockNumber();
const { formatted, symbol } = await evm.getTokenBalance({ tokenAddress: USDC });
const [name, sym, dec] = await evm.multicall({ contracts: [...] });
```
All overflow-safe values come back as `bigint`. Use `formatUnits` /
`parseUnits` for display and user input. See
[Overview → Bigint everywhere](/evm/overview#bigint-everywhere-for-overflow-safe-math).
## Contract reads
### Read a single value
```ts
import { evm } from '@oviato/connect';
const ERC20_ABI = [
{
type: 'function',
name: 'balanceOf',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
},
] as const;
const raw = await evm.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: ERC20_ABI,
functionName: 'balanceOf',
args: ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6'],
});
```
The generic `` declares your expected return type. The SDK can't infer from the ABI without dependencies, so cast manually.
### Batch reads with multicall
Any time you'd make more than one contract read per render, reach for multicall instead. One request, one on-chain call, N decoded results.
```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 entry: { status: 'success', result: unknown } | { status: 'failure', error: string }
```
`allowFailure` defaults to `true` — a single revert in the batch doesn't
poison the rest. Set `allowFailure: false` if you want the whole call to
throw on any revert.
Multicall uses the [Multicall3](https://github.com/mds1/multicall) contract at `0xcA11bde05977b3631167028862bE2a173976CA11` — the same deterministic address on every EVM chain.
## Token helpers
Thin conveniences that bundle the common reads for ERC-20 interactions.
### Token balance
```ts
const {
raw, // bigint — smallest unit
formatted, // '1234.56'
symbol, // 'USDC'
decimals, // 6
tokenAddress,
} = await evm.getTokenBalance({
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
address: session.address, // optional — defaults to session
});
```
`formatted` is for display only. Never parse it back for arithmetic — the
decimal precision is tuned for human readability, not computation. Use
`raw` (bigint) and `formatUnits`/`parseUnits` for math.
### Allowance
How much a spender is allowed to pull from an owner's balance. Useful before calling `approve`.
```ts
const { raw, formatted } = await evm.getTokenAllowance({
tokenAddress: USDC,
owner: session.address,
spender: UNISWAP_ROUTER,
});
if (raw < BigInt(needed)) {
// Approve first — see /evm/recipes
}
```
### Curated token list
Oviato maintains a registry of well-known ERC-20 tokens per chain (USDC, USDT, WETH, wrapped BTC variants). Use it to populate dropdowns or wallet UIs without hardcoding addresses.
```ts
const { network, tokens } = await evm.getTokens();
// tokens: [{ address, symbol, name, decimals, logoUrl? }, ...]
```
New tokens are added server-side — no SDK release needed to pick them up.
## NFT helpers
### Owner, balance, metadata
```ts
// Who owns tokenId 1?
const { owner } = await evm.getNftOwner({ contractAddress, tokenId: 1n });
// How many NFTs in this collection does the user hold?
const { raw, formatted } = await evm.getNftBalance({
contractAddress,
address: session.address,
});
// Resolved metadata JSON — image normalized to a gateway URL
const meta = await evm.getNftMetadata({ contractAddress, tokenId: 1n });
```
The metadata call resolves the on-chain `tokenURI` transparently:
| Scheme | Behaviour |
|---|---|
| `data:application/json;base64,…` | Decoded inline |
| `ipfs://` | Raced across three public IPFS gateways |
| `ar://` | Resolved to `arweave.net` |
| `https://…` | Direct fetch with 5 s timeout |
| `http://…` | **Rejected** (mixed-content security) |
```ts
// Returned shape
{
tokenURI: 'ipfs://...', // original on-chain URI
name: 'Collection Name', // from `name()`
symbol: 'COLL', // from `symbol()`
metadata: { // resolved JSON (or absent on failure)
name: 'Token #42',
description: '...',
image: 'https://ipfs.io/ipfs/...', // normalized to gateway URL
attributes: [...],
},
resolverError: undefined, // populated when resolution failed
}
```
Resolution failures are non-fatal — `metadata` is absent and
`resolverError` carries a one-line reason. The tokenURI + collection name
still come back so you have something to show.
## Chain reads
### Block
```ts
const n = await evm.getBlockNumber(); // bigint
const latest = await evm.getBlock({}); // latest block
const past = await evm.getBlock({ blockNumber: 12345678n });
const byHash = await evm.getBlock({ blockHash: '0x...' });
const full = await evm.getBlock({ includeTransactions: true });
```
### Transaction + receipt
```ts
const tx = await evm.getTransaction({ hash: '0x...' }); // Transaction | null
const rx = await evm.getTransactionReceipt({ hash: '0x...' }); // TransactionReceipt | null
```
Both return `null` when the tx doesn't exist on-chain yet. A transaction you just broadcast may not have a receipt until the next block — see `waitForTransactionReceipt` for polling.
### Nonce
```ts
const nonce = await evm.getTransactionCount({
address: session.address,
blockTag: 'pending', // 'pending' reflects our nonce tracker
});
```
The `pending` count is tracker-adjusted so rapid-fire sends don't collide.
Pass `blockTag: 'latest'` for raw on-chain state.
### Contract code + storage
```ts
const code = await evm.getCode({ address });
const isContract = code !== '0x';
const slot0 = await evm.getStorageAt({ address, slot: '0x0' });
```
## Fees
### Single values
```ts
const gp = await evm.getGasPrice(); // bigint wei
const pri = await evm.estimateMaxPriorityFeePerGas(); // bigint wei
const units = await evm.estimateGas({ to, data, value }); // bigint gas units
```
### Combined snapshot
For gas-picker UIs, grab everything in one call:
```ts
const {
gasPrice, // bigint — legacy
maxFeePerGas, // bigint — EIP-1559
maxPriorityFeePerGas,
baseFeePerGas, // null on legacy chains
} = await evm.estimateFeesPerGas();
// Ethers-style alias
const fd = await evm.getFeeData();
// same + maxFeePerBlobGas stubbed to null
```
### Historical
```ts
const h = await evm.getFeeHistory({
blockCount: 20,
newestBlock: 'latest',
rewardPercentiles: [10, 50, 90],
});
```
## Signature verification (offchain)
Pure `ecrecover` — no chain call, no popup. Useful for backend verification of signatures your frontend produced.
### EIP-191 (plain messages)
```ts
const valid = await evm.verifyMessage({
address: session.address,
message: 'Hello, Oviato!',
signature: '0x...',
});
```
### EIP-712 (typed data)
```ts
const valid = await evm.verifyTypedData({
address: session.address,
domain: { name: 'MyApp', version: '1', chainId: 8453 },
types: { Greeting: [{ name: 'content', type: 'string' }] },
primaryType: 'Greeting',
message: { content: 'Hi' },
signature: '0x...',
});
```
`types` must NOT include an `EIP712Domain` entry — the domain separator is
computed from the `domain` object directly. Adding `EIP712Domain` to
`types` produces a different hash and the signature won't match.
## Waiting for receipts
### `evm.waitForTransactionReceipt`
Polls `eth_getTransactionReceipt` until the tx is mined + N confirmations deep. Returns `null` on timeout (default 180 s) — callers distinguish "not yet" from "RPC error" without try/catch.
```ts
const rx = await evm.waitForTransactionReceipt({ hash });
if (rx?.status === 'success') {
console.log('Mined in block', rx.blockNumber);
} else if (rx === null) {
console.log('Timed out — try again later');
} else {
console.log('Reverted:', rx);
}
```
Defaults adapt to the chain:
| Chain | Confirmations | Polling interval |
|---|---|---|
| Base, Base Sepolia, other L2s | 1 | 2 s |
| Ethereum mainnet | 3 | 4 s |
| Ethereum Sepolia | 1 | 2 s |
Override via `{ confirmations, pollingInterval, timeout }`.
### Cancel via AbortSignal
```ts
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);
try {
const rx = await evm.waitForTransactionReceipt({ hash, signal: ctrl.signal });
} catch (err: any) {
if (err.name === 'AbortError') { /* handled */ }
}
```
## Factory (client object style)
If you prefer a single `client` object to pass around (viem-style), `createEvmClient()` bundles all the methods:
```ts
import { createEvmClient } from '@oviato/connect';
const client = createEvmClient();
const block = await client.getBlockNumber();
const bal = await client.getTokenBalance({ tokenAddress });
```
The `evm` namespace and `createEvmClient()` share identical internals — pick whichever reads nicer in your codebase.
## Common questions
`getBalance` is chain-agnostic (works on Bitcoin + EVM) so it lives at the
top level. See
[Chain-Agnostic → getBalance](/chain-agnostic/get-balance).
Yes. The SDK's ABI type (`AbiItem[]`) is structurally compatible with
viem's `Abi`. Pass viem ABIs verbatim — `as const` still gives you the
strongest TS narrowing.
Pass `blockTag` to any read that supports it — `'latest'` | `'pending'` |
`'earliest'` | `'safe'` | `'finalized'` | a hex block number.
`readContract`, `getCode`, `getStorageAt`, `getTransactionCount` all accept it.
Reads throw with a descriptive error. You should retry with exponential
backoff client-side or show a "try again" UI — the SDK doesn't retry
automatically to avoid amplifying outages.
## Related
- [Writes](/evm/writes) — send transactions, call contracts, sign typed data
- [Recipes](/evm/recipes) — common flows (approve + transfer, batch balance, NFT mint)
- [Troubleshooting](/evm/troubleshooting) — error-keyed lookup
---
# EVM Writes
> Send transactions, call contracts, sign EIP-712 data, and switch chains on EVM networks.
Canonical URL: https://docs.oviato.com/evm/writes
File: evm/writes.mdx
All EVM writes open a popup or iframe where the user approves with their passkey. Signing happens server-side inside the bridge — your dapp never touches private keys.
```ts
import { evm } from '@oviato/connect';
const r = await evm.sendTransaction({ to, value: 10n ** 15n });
// → popup → passkey → broadcast → { hash, network, explorerUrl }
```
New to Oviato writes? [Core Concepts → The write flow](/getting-started/core-concepts#the-write-flow)
walks through the full popup → sign → broadcast chain.
## Response envelope
Every write returns a discriminated envelope:
```ts
type Result =
| { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } }
| { status: 'error'; message: string };
```
Always narrow on `status` before reading `.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);
}
```
Prefer the raw-hash style from ethers/viem? See [Migrating from viem](/evm/migrating-from-viem) for a one-line unwrap helper.
## Send ETH / native
The simplest write — just move native value from the session address to a recipient.
```ts
import { evm } from '@oviato/connect';
const r = await evm.sendTransaction({
to: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6',
value: 10n ** 15n, // 0.001 ETH in wei (bigint)
fee: 'auto',
});
```
### Parameters
| Field | Type | Notes |
|---|---|---|
| `to` | `Address` | Required. |
| `value` | `bigint \| string \| number` | Wei amount. Default `0n`. |
| `data` | `Hex` | Optional calldata (`0x…`). Default `0x`. |
| `gas` | `bigint \| string \| number` | Override gas limit. Usually not needed. |
| `maxFeePerGas` | `bigint \| string \| number` | EIP-1559 override. |
| `maxPriorityFeePerGas` | `bigint \| string \| number` | EIP-1559 override. |
| `gasPrice` | `bigint \| string \| number` | Legacy override (mutually exclusive with 1559). |
| `nonce` | `number` | Power-user override — bypasses the tracker. |
| `fee` | `'auto' \| 'fast' \| 'medium' \| 'slow'` | Semantic tier. Default `auto` (≈ medium). |
| `method` | `'popup' \| 'iframe'` | How the approval UI opens. |
`fee: 'auto'` is the right default. Tiers (`fast`/`medium`/`slow`) map to
priority-fee multipliers per chain; explicit values (`maxFeePerGas` etc.)
override them. Don't set both.
## Transfer a token (ERC-20)
`evm.writeContract` takes an ABI + function name + args. The backend encodes the calldata and the bridge renders a specialized "Transfer Token" sheet — no raw hex shown to the user.
```ts
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 on Ethereum
abi: ERC20_ABI,
functionName: 'transfer',
args: [recipient, 1_000_000n], // 1 USDC (6 decimals)
});
```
`1_000_000n` is 1 USDC because USDC has 6 decimals. Always remember —
token amounts are in smallest units (wei-equivalent). Use `parseUnits('1',
6)` if you want to be explicit about the conversion.
### Approve a spender
```ts
const ERC20_APPROVE_ABI = [
{
type: 'function',
name: 'approve',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
],
outputs: [{ type: 'bool' }],
stateMutability: 'nonpayable',
},
] as const;
// Approve exactly 100 USDC
await evm.writeContract({
address: USDC,
abi: ERC20_APPROVE_ABI,
functionName: 'approve',
args: [spender, 100_000_000n],
});
// Max uint256 — "infinite" approval (saves gas on repeat use)
await evm.writeContract({
address: USDC,
abi: ERC20_APPROVE_ABI,
functionName: 'approve',
args: [spender, 2n ** 256n - 1n],
});
```
The bridge detects this is an ERC-20 approval (via the selector + known-contracts registry) and renders a dedicated "Approve" sheet with clear messaging about unlimited approvals when the value is max-uint256.
## Transfer an NFT (ERC-721)
```ts
const ERC721_ABI = [
{
type: 'function',
name: 'safeTransferFrom',
inputs: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
],
outputs: [],
stateMutability: 'nonpayable',
},
] as const;
await evm.writeContract({
address: nftContract,
abi: ERC721_ABI,
functionName: 'safeTransferFrom',
args: [session.address, recipient, tokenId],
});
```
The bridge fetches the NFT's image via [`evm.getNftMetadata`](/evm/reads#owner-balance-metadata) and renders it in a "Transfer NFT" sheet. If the image can't be resolved, the sheet shows a placeholder — the user still sees the collection name + token ID.
## Call any contract (generic)
For contract calls that aren't erc20/erc721 standards — governance, DeFi, custom contracts — `writeContract` takes any ABI + function.
```ts
const ROUTER_ABI = [
{
type: 'function',
name: 'swapExactTokensForTokens',
inputs: [
{ name: 'amountIn', type: 'uint256' },
{ name: 'amountOutMin', type: 'uint256' },
{ name: 'path', type: 'address[]' },
{ name: 'to', type: 'address' },
{ name: 'deadline', type: 'uint256' },
],
outputs: [{ type: 'uint256[]' }],
stateMutability: 'nonpayable',
},
] as const;
await evm.writeContract({
address: UNISWAP_V2_ROUTER,
abi: ROUTER_ABI,
functionName: 'swapExactTokensForTokens',
args: [amountIn, 0n, [TOKEN_A, TOKEN_B], recipient, BigInt(deadline)],
});
```
The bridge shows a generic "Contract Call" sheet with decoded args. Consider adding your contract to our curated registry (via the dashboard, coming in a later release) so users see a branded sheet instead.
## Sign EIP-712 typed data
```ts
const r = await evm.signTypedData({
domain: {
name: 'MyApp',
version: '1',
chainId: 8453, // Base
},
types: {
Greeting: [
{ name: 'content', type: 'string' },
{ name: 'from', type: 'address' },
],
},
primaryType: 'Greeting',
message: {
content: 'Hi from Oviato',
from: session.address,
},
});
if (r.status === 'success' && r.type === 'signTypedDataSuccess') {
console.log('Signature:', r.data.signature);
}
```
`types` must NOT include an `EIP712Domain` entry. The SDK computes the
domain separator from `domain` directly — adding it manually produces a
different hash and signatures won't recover to the expected address.
Verify the signature offchain with [`evm.verifyTypedData`](/evm/reads#eip-712-typed-data).
## Switch chains
Ethers-style alias for [`switchNetwork`](/chain-agnostic/session-and-network#switch-network).
```ts
import { evm } from '@oviato/connect';
import { Network } from '@oviato/connect';
const r = await evm.switchChain(Network.BASEMAINNET);
if (r.status === 'success') {
console.log('Now on', r.data.network);
}
```
Chain switching requires a passkey tap — the server re-issues the session JWT scoped to the new network.
## Wait for confirmations
After any write, pass the hash to [`evm.waitForTransactionReceipt`](/evm/reads#waiting-for-receipts) to block until it mines.
```ts
const r = await evm.sendTransaction({ to, value });
if (r.status !== 'success') return;
const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash });
if (rx?.status === 'success') {
console.log('Mined in block', rx.blockNumber);
}
```
## Fee tiers
Instead of setting `maxFeePerGas` manually, pass a semantic tier:
- `fee: 'fast'` — high priority, 1–2 block inclusion
- `fee: 'medium'` — sensible default, 2–5 block inclusion
- `fee: 'slow'` — cheapest, may take 5–20 blocks
- `fee: 'auto'` — same as `'medium'`
The bridge UI's gas picker lets the user override inline — they can still bump to fast if they're in a hurry.
## Rate limits
Per `(app, address)` pair:
- `sendTransaction` / `writeContract` — 60 prepares/min, 30 broadcasts/min
- `signTypedData` — no explicit limit (purely client-side, no chain state)
- `switchChain` / `switchNetwork` — 10/min
Exceeding any of these returns an error with a `retryAfter` hint. Your normal UI flow should be nowhere near these — they exist to bound runaway scripts.
## Common questions
Both produce a signed EVM tx. Use `sendTransaction` when you have
pre-encoded calldata (or none — native sends). Use `writeContract` when
you have an ABI + function name + args — the bridge can then show a
specialized confirmation sheet (Transfer Token, Transfer NFT, Approve).
Yes — call `rpc.prepareEvmTx({ to, value, ... })` to get the fully
prepared tx (hex, fee tiers, decoded category) without opening a popup.
Useful for showing a summary UI before the user commits.
You get `{ status: 'error', message: 'User closed popup' }` (or similar).
It's non-exceptional — no try/catch needed, just check `status`.
Yes, pass `gas:`. The default comes from `eth_estimateGas` on our prep
step, which is accurate within ~10% for most calls. Override only if you
have specific reason.
A previous tx at the same nonce is already in the mempool with higher
gas. Either wait for it to clear, or retry with `fee: 'fast'`. The
nonce tracker auto-releases your reservation — no manual cleanup
needed. See [Troubleshooting](/evm/troubleshooting#replacement-transaction-underpriced).
## Related
- [Reads](/evm/reads) — every method that doesn't need a popup
- [Recipes](/evm/recipes) — end-to-end flows (approve + transfer, mint, sign permit)
- [Troubleshooting](/evm/troubleshooting) — error-keyed lookup
- [Migrating from viem](/evm/migrating-from-viem) — side-by-side
---
# EVM Recipes
> End-to-end copy-paste flows for the common EVM tasks — approve-and-transfer, batch balances, NFT mint, EIP-2612 permit, and more.
Canonical URL: https://docs.oviato.com/evm/recipes
File: evm/recipes.mdx
Each recipe below is complete — imports, session setup, error handling, explorer URL. Paste, swap the addresses, and ship.
All recipes use `@oviato/connect` (React). Swap for `@oviato/sdk` if you're
using a different framework — the function signatures are identical.
## Approve-and-transfer (ERC-20)
Check allowance, approve if needed, then transfer through a contract (e.g. a DEX router).
```ts
import { evm } from '@oviato/connect';
import { parseUnits } from '@oviato/connect';
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const ROUTER = '0x...';
const amount = parseUnits('100', 6); // 100 USDC
const ERC20_ABI = [
{
type: 'function', name: 'approve',
inputs: [{ name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }],
outputs: [{ type: 'bool' }], stateMutability: 'nonpayable',
},
] as const;
async function approveAndTransfer(session: { address: `0x${string}` }) {
// 1. Check current allowance
const { raw: current } = await evm.getTokenAllowance({
tokenAddress: USDC,
owner: session.address,
spender: ROUTER,
});
// 2. Approve if short
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.status === 'error' ? approve.message : 'Approve failed');
}
// Wait for approval to land before using it
await evm.waitForTransactionReceipt({ hash: approve.data.hash });
}
// 3. Now call the router
// ... evm.writeContract({ address: ROUTER, ... })
}
```
Always `waitForTransactionReceipt` on the approve before using the
allowance. A pending approve tx doesn't count — the router will still see
the old allowance until it mines.
## Dashboard-style balance fetch (multicall)
Fetch balances for multiple tokens in one round-trip.
```ts
import { evm, formatUnits } from '@oviato/connect';
const ERC20_BALANCE_OF_ABI = [
{
type: 'function', name: 'balanceOf',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }], stateMutability: 'view',
},
] as const;
async function getMultiBalance(owner: `0x${string}`) {
// Get the curated token list
const { tokens } = await evm.getTokens();
// Batch their balances in one call
const results = await evm.multicall({
contracts: tokens.map((t) => ({
address: t.address,
abi: ERC20_BALANCE_OF_ABI,
functionName: 'balanceOf',
args: [owner],
})),
});
return tokens.map((t, i) => {
const r = results[i];
if (r.status !== 'success') {
return { ...t, balance: '0', raw: 0n, error: r.error };
}
const raw = r.result as bigint;
return {
...t,
raw,
balance: formatUnits(raw, t.decimals),
};
});
}
// Output: [{ symbol: 'USDC', address, balance: '1234.56', raw: 1234560000n, ... }, ...]
```
## Mint an NFT (no-args mint)
Many test/community NFTs expose a parameterless `mintNft()` function.
```ts
const MINTABLE_NFT_ABI = [
{ type: 'function', name: 'mintNft', inputs: [], outputs: [], stateMutability: 'nonpayable' },
] as const;
async function mintNft(contract: `0x${string}`) {
const r = await evm.writeContract({
address: contract,
abi: MINTABLE_NFT_ABI,
functionName: 'mintNft',
args: [],
});
if (r.status !== 'success') {
throw new Error(r.status === 'error' ? r.message : 'Mint failed');
}
const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash });
// Your newly-minted token id often comes from a Transfer event in the receipt.
// For simple contracts, the token id is 1 + totalSupply before mint.
return { hash: r.data.hash, receipt: rx };
}
```
## Sign an EIP-2612 Permit (gasless approval)
Permit lets a user approve a spender without an on-chain transaction — the signature is presented later by the spender alongside `permit()` + the intended action. USDC on Ethereum supports this natively.
```ts
import { evm } from '@oviato/connect';
async function signPermit(session: { address: `0x${string}` }) {
const chainId = 1; // mainnet
const deadline = Math.floor(Date.now() / 1000) + 3600; // +1h
// Read the current nonce from the token (USDC permit nonces are token-side, not eth nonce)
const USDC_PERMIT_ABI = [
{
type: 'function', name: 'nonces',
inputs: [{ name: 'owner', type: 'address' }],
outputs: [{ type: 'uint256' }], stateMutability: 'view',
},
] as const;
const nonce = await evm.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: USDC_PERMIT_ABI,
functionName: 'nonces',
args: [session.address],
});
const r = await evm.signTypedData({
domain: {
name: 'USD Coin',
version: '2',
chainId,
verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
},
types: {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
message: {
owner: session.address,
spender: '0x...',
value: 100_000_000n, // 100 USDC
nonce,
deadline: BigInt(deadline),
},
});
if (r.status !== 'success') return null;
// Split the signature for the permit() call — r, s, v
const sig = r.data.signature as `0x${string}`;
return {
deadline,
v: parseInt(sig.slice(130, 132), 16),
r: `0x${sig.slice(2, 66)}`,
s: `0x${sig.slice(66, 130)}`,
};
}
```
## Check owner + metadata + transfer an NFT
```ts
async function inspectAndTransfer(
contract: `0x${string}`,
tokenId: bigint,
newOwner: `0x${string}`,
session: { address: `0x${string}` },
) {
// 1. Confirm the session owns this NFT
const { owner } = await evm.getNftOwner({ contractAddress: contract, tokenId });
if (owner.toLowerCase() !== session.address.toLowerCase()) {
throw new Error(`You don't own token ${tokenId}`);
}
// 2. Pull metadata for the confirmation UI (bridge does this automatically too)
const meta = await evm.getNftMetadata({ contractAddress: contract, tokenId });
console.log(`Transferring ${meta.metadata?.name ?? `#${tokenId}`}`);
// 3. Transfer
const ERC721_ABI = [
{
type: 'function', name: 'safeTransferFrom',
inputs: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
],
outputs: [], stateMutability: 'nonpayable',
},
] as const;
const r = await evm.writeContract({
address: contract,
abi: ERC721_ABI,
functionName: 'safeTransferFrom',
args: [session.address, newOwner, tokenId],
});
if (r.status !== 'success') throw new Error('Transfer failed');
return r.data;
}
```
## Pay with USDC via x402 (gasless)
Oviato supports the [x402 payment protocol](/x402/overview) — an HTTP-level payment flow for gated resources. The SDK signs an ERC-3009 authorization, attaches it as an HTTP header, and your server validates.
```ts
import { x402 } from '@oviato/connect';
const r = await x402.pay('https://api.example.com/premium-data');
if (r.paid) {
const data = await r.response.json();
console.log(data);
} else if (r.error) {
console.error('Payment failed:', r.error);
}
```
See [x402 Payments](/x402/overview) for the full flow, including facilitator setup.
## Fee-aware transaction
Let the user pick a speed tier before signing.
```ts
import { evm, formatUnits } from '@oviato/connect';
async function previewAndSend(to: `0x${string}`, value: bigint) {
// 1. Get current fees
const fees = await evm.estimateFeesPerGas();
const gasUnits = await evm.estimateGas({ to, value });
const costWei = gasUnits * fees.maxFeePerGas;
const costEth = formatUnits(costWei, 18);
// 2. Show the user the cost before proceeding
if (!confirm(`Send ${formatUnits(value, 18)} ETH? (gas ≈ ${costEth} ETH)`)) {
return null;
}
// 3. Send — the bridge will ultimately recompute fees, this preview is UI-only
const r = await evm.sendTransaction({ to, value, fee: 'medium' });
return r;
}
```
## More
- [Writes reference](/evm/writes) — full parameter details
- [Troubleshooting](/evm/troubleshooting) — error-keyed lookup
- [Migrating from viem](/evm/migrating-from-viem) — side-by-side
---
# EVM Troubleshooting
> Common errors you'll see when sending EVM transactions, and what to do about each.
Canonical URL: https://docs.oviato.com/evm/troubleshooting
File: evm/troubleshooting.mdx
This page is keyed by the error message you see. Search for the phrase or scan the headings.
## Replacement transaction underpriced
**What happened.** A previous transaction at the same nonce is pending in the mempool with a higher gas price. Your new tx can't replace it without ≥10% higher gas.
**What to do.** Either wait for the pending tx to clear (usually seconds-to-minutes), or retry with `fee: 'fast'` to bump the gas.
```ts
const r = await evm.sendTransaction({ to, value, fee: 'fast' });
```
The nonce tracker automatically releases your reservation on this error,
so your next send picks a fresh nonce. No manual cleanup needed.
## Nonce too low
**What happened.** The nonce you submitted has already been used on-chain. This usually means:
- The server-side tracker drifted from the actual chain state.
- You passed a manual `nonce:` override that's stale.
**What to do.** Just retry. The tracker auto-resyncs on the next `prepareEvm` call (reads `eth_getTransactionCount('pending')` from Chainstack). No code change needed on your side.
## Insufficient funds for this transaction
**What happened.** The session address doesn't have enough native token to cover `value + (gas × maxFeePerGas)`.
**What to do.**
1. Check the balance: `await rpc.getBalance({ address: session.address })`.
2. Check the gas cost: `await evm.estimateFeesPerGas()` + `evm.estimateGas()`.
3. Top up the wallet, or lower `value`, or switch to `fee: 'slow'`.
On testnets, grab faucet ETH from [Sepolia faucet](https://faucet.sepolia.dev) or [Base Sepolia faucet](https://portal.cdp.coinbase.com/products/faucet).
## Missing revert data
**What happened.** An `eth_call` reverted without a readable reason — almost always because a precondition in the contract failed silently.
**Common causes:**
- **ERC-20 transfer with 0 balance** — you're trying to move tokens you don't have. The `transfer` reverts because `balanceOf(sender) < amount`.
- **Uninitialized contract** — the address has no code.
- **Wrong function signature** — your ABI doesn't match what's on-chain.
**What to do.** Read the state first.
```ts
// Before transferring:
const { raw } = await evm.getTokenBalance({ tokenAddress });
if (raw < amount) throw new Error('Insufficient token balance');
// Verify the contract exists:
const code = await evm.getCode({ address });
if (code === '0x') throw new Error('Not a contract');
```
## Rate limit exceeded
**What happened.** Your app hit the per-(app, address) rate limit: 60 prepares/min, 30 broadcasts/min on EVM writes.
**What to do.**
- For normal UI flows, you should never hit this. Investigate why you're calling so often.
- If you genuinely need higher limits (automated system, batch job), contact us via the [Developer Dashboard](https://dashboard.oviato.com).
- The error includes a `retryAfter` hint (seconds). Exponential-backoff on the retry.
## Popup blocked by the browser
**What happened.** Your browser blocked the popup because `sendTransaction` was called outside a user gesture.
**What to do.** Always initiate writes from a direct click handler — not in a `useEffect`, not in a setTimeout, not after an async gap.
```ts
// ✅ Good: initiated from a click
// ❌ Bad: initiated from an async completion
useEffect(() => {
fetchSomething().then(() => evm.sendTransaction({ to, value }));
}, []);
```
If you genuinely need to trigger a write outside a click context (uncommon), use `method: 'iframe'` — iframes aren't subject to popup blockers.
## User closed popup
**What happened.** The user dismissed the popup without approving.
**What to do.** Not an exception — it's a normal error result. Just check `r.status`:
```ts
const r = await evm.sendTransaction({ to, value });
if (r.status === 'error') {
// User cancelled, or another error. Handle gracefully.
console.log('Cancelled or failed:', r.message);
}
```
Don't wrap this in try/catch — nothing is thrown.
## Invalid nonce. Please try again.
**What happened.** The bridge received a response with a nonce that doesn't match the one it issued. Usually means the user opened multiple popups, or a race between popup and iframe modes.
**What to do.** Retry once. If it persists, check that you're not calling `sendTransaction` from multiple places simultaneously.
## Chain mismatch (JWT network claim vs request)
**What happened.** You called an EVM method while the session is on a UTXO chain (or vice versa). The JWT's `network` claim doesn't match what the method needs.
**What to do.** Switch the session first.
```ts
import { switchNetwork, Network, getSession, getNetworkType, NetworkType } from '@oviato/connect';
const session = getSession();
if (session && getNetworkType(session.network) !== NetworkType.EVM) {
await switchNetwork(Network.BASEMAINNET); // or whatever EVM chain
}
await evm.sendTransaction({ to, value });
```
## Not connected: Please login first.
**What happened.** The SDK checked for a session before opening a write popup, and there isn't one.
**What to do.** Require the user to log in first.
```tsx
import { useOviConnect, ConnectButton } from '@oviato/connect/client';
function SendButton() {
const { session } = useOviConnect();
if (!session) return ;
return ;
}
```
## SDK not initialized
**What happened.** You called an SDK method before `initialize()` or before the `` mounted.
**What to do.** Make sure `initialize()` runs before any other SDK call. In React, `` handles this — just make sure you wrapped your app.
## Still stuck?
- Check the [Developer Dashboard](https://dashboard.oviato.com) activity logs — every prepare/broadcast event with error context.
- Report an issue at [github.com/oviato-hq](https://github.com).
- Ping the team in the dashboard's support panel.
---
# Migrating from viem
> Method-by-method mapping from viem clients to Oviato's evm.* namespace, plus adapters for the response-envelope pattern.
Canonical URL: https://docs.oviato.com/evm/migrating-from-viem
File: evm/migrating-from-viem.mdx
If you're comfortable with [viem](https://viem.sh), you'll find Oviato's EVM surface shaped almost identically — method names, parameter keys, and return shapes all match the viem convention where possible.
## The one-minute map
| viem | Oviato | Notes |
|---|---|---|
| `publicClient.readContract` | `evm.readContract` | identical params |
| `publicClient.multicall` | `evm.multicall` | identical — Multicall3 default |
| `publicClient.getBalance` | `getBalance` (top level) | chain-agnostic return |
| `publicClient.getBlockNumber` | `evm.getBlockNumber` | |
| `publicClient.getBlock` | `evm.getBlock` | |
| `publicClient.getTransaction` | `evm.getTransaction` | |
| `publicClient.getTransactionReceipt` | `evm.getTransactionReceipt` | |
| `publicClient.getTransactionCount` | `evm.getTransactionCount` | `pending` is tracker-adjusted |
| `publicClient.getCode` | `evm.getCode` | |
| `publicClient.getStorageAt` | `evm.getStorageAt` | |
| `publicClient.getGasPrice` | `evm.getGasPrice` | |
| `publicClient.estimateGas` | `evm.estimateGas` | |
| `publicClient.estimateFeesPerGas` | `evm.estimateFeesPerGas` | |
| `publicClient.estimateMaxPriorityFeePerGas` | `evm.estimateMaxPriorityFeePerGas` | |
| `publicClient.getFeeHistory` | `evm.getFeeHistory` | |
| `publicClient.waitForTransactionReceipt` | `evm.waitForTransactionReceipt` | |
| `publicClient.verifyMessage` | `evm.verifyMessage` | |
| `publicClient.verifyTypedData` | `evm.verifyTypedData` | |
| `walletClient.sendTransaction` | `evm.sendTransaction` | **returns envelope, not hash** |
| `walletClient.writeContract` | `evm.writeContract` | **returns envelope, not hash** |
| `walletClient.signTypedData` | `evm.signTypedData` | **returns envelope, not signature** |
| `walletClient.signMessage` | `signMessage` (top level) | chain-agnostic |
| `walletClient.switchChain` | `evm.switchChain` | also exported as `switchNetwork` |
| `walletClient.watchAsset` | — | not implemented; use `evm.getTokens` |
| `publicClient.simulateContract` | — | v2 roadmap |
| `publicClient.watchBlocks` | — | v2 roadmap |
| `publicClient.watchEvent` | — | v2 roadmap |
## Clients vs namespace
viem:
```ts
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createWalletClient({ chain: mainnet, transport: custom(window.ethereum) });
const balance = await publicClient.getBalance({ address });
const hash = await walletClient.sendTransaction({ to, value });
```
Oviato:
```ts
import { evm } from '@oviato/connect';
// No client to create — the namespace IS the client. Chain comes from the user's session.
const balance = await getBalance({ address }); // chain-agnostic
const r = await evm.sendTransaction({ to, value });
```
Oviato's chain is controlled by the user (via `switchNetwork`), not by the
developer. You don't pass `chain:` on every call — the session already
knows. This is deliberate: multi-chain UX without the dev having to juggle
configs.
## The envelope adapter
The biggest DX difference: Oviato returns a discriminated envelope, viem returns a raw value and throws on error.
```ts
try {
const hash = await walletClient.sendTransaction({ to, value });
console.log('Hash:', hash);
} catch (err) {
console.error('Failed:', err);
}
```
```ts
const r = await evm.sendTransaction({ to, value });
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log('Hash:', r.data.hash);
} else if (r.status === 'error') {
console.error('Failed:', r.message);
}
```
```ts
// One-line adapter — drop-in replacement for viem-style code
async function unwrap(p: Promise) {
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 }));
```
We keep the envelope by default because it makes "user cancelled popup" a first-class state instead of an exception — far easier to handle in UI code. Use the adapter if your codebase assumes the viem style.
## ABI compatibility
viem's `Abi` type and ours are structurally compatible. Pass viem ABIs verbatim:
```ts
import { erc20Abi } from 'viem';
import { evm } from '@oviato/connect';
const balance = await evm.readContract({
address: USDC,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address],
});
```
## Address + Hex types
Identical:
```ts
// viem
import type { Address, Hex } from 'viem';
// Oviato
import type { Address, Hex } from '@oviato/connect';
```
Both resolve to the TypeScript template literal type for 0x-prefixed hex strings.
## bigint conventions
Identical — wei is always `bigint`, overflow-safe.
```ts
const value = 10n ** 18n; // 1 ETH
// parseUnits / formatUnits match viem's semantics exactly
import { parseUnits, formatUnits } from '@oviato/connect';
parseUnits('1.5', 18); // 1500000000000000000n
formatUnits(1_234_500n, 6); // '1.2345'
```
## Chains
viem has explicit chain objects (`mainnet`, `base`, `sepolia`). Oviato uses string slugs on the session.
| viem chain | Oviato Network |
|---|---|
| `mainnet` | `Network.ETHMAINNET` (`'eth'`) |
| `sepolia` | `Network.ETHSEPOLIA` (`'ethsepolia'`) |
| `base` | `Network.BASEMAINNET` (`'base'`) |
| `baseSepolia` | `Network.BASESEPOLIA` (`'basesepolia'`) |
You don't pass the chain on each call — it's implicit from the session. Switch via `switchNetwork(Network.X)`.
## What Oviato adds over viem
- **Account custody.** Oviato manages keys via WebAuthn passkeys — no private-key juggling, no seed phrase UX.
- **Server-side nonce tracker.** Rapid-fire sends just work; no "pending count lag" race.
- **Specialized confirmation sheets.** `writeContract` with an ERC-20 ABI shows a "Transfer Token" sheet, not raw calldata.
- **Bridge UI.** The popup/iframe handles approval — you don't ship wallet UI.
- **Multi-chain session.** Same API on Bitcoin + EVM via `sendTransfer` / `signMessage` / `getBalance`.
- **Curated token registry.** `evm.getTokens()` returns canonical ERC-20s per chain, server-maintained.
## What Oviato doesn't do (yet)
- **`simulateContract`** — v2. For now, call `readContract` with the same ABI to preview.
- **`watchBlocks` / `watchEvent` / subscriptions** — v2. Poll via `getBlockNumber` + `getLogs` if you need it now.
- **Local ABI encoding.** The SDK ships ABIs to the server for encoding. If you need pure-client encoding, wrap viem's `encodeFunctionData` and pass the result as `data:` to `sendTransaction`.
- **Custom chains not in our supported list.** See [EVM Overview → Supported networks](/evm/overview#supported-evm-networks).
## Side-by-side: a typical swap
**viem**
```ts
import { createWalletClient, http, parseEther } from 'viem';
import { mainnet } from 'viem/chains';
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum!),
});
const hash = await walletClient.sendTransaction({
to: '0x742d35Cc...',
value: parseEther('0.01'),
});
const rx = await publicClient.waitForTransactionReceipt({ hash });
```
**Oviato**
```ts
import { evm, parseUnits } from '@oviato/connect';
const r = await evm.sendTransaction({
to: '0x742d35Cc...',
value: parseUnits('0.01', 18),
});
if (r.status !== 'success') return;
const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash });
```
No client to set up, no wallet to connect — the session handles it.
## Related
- [EVM Overview](/evm/overview) — the mental model
- [Migrating from ethers](/evm/migrating-from-ethers) — if you're coming from ethers v5/v6 instead
---
# Migrating from ethers
> Method-by-method mapping from ethers v5/v6 to Oviato's evm.* namespace, with adapters for the response-envelope pattern.
Canonical URL: https://docs.oviato.com/evm/migrating-from-ethers
File: evm/migrating-from-ethers.mdx
Oviato borrows viem's naming for most methods. If you're on ethers, the mapping is still straightforward — every ethers method has a counterpart, and we ship aliases where the names differ.
## Quick map
| ethers v5 / v6 | Oviato | Notes |
|---|---|---|
| `provider.getBlockNumber()` | `evm.getBlockNumber()` | bigint return |
| `provider.getBlock(n)` | `evm.getBlock({ blockNumber: n })` | key-arg signature |
| `provider.getTransaction(hash)` | `evm.getTransaction({ hash })` | |
| `provider.getTransactionReceipt(hash)` | `evm.getTransactionReceipt({ hash })` | |
| `provider.getTransactionCount(addr)` | `evm.getTransactionCount({ address })` | |
| `provider.getCode(addr)` | `evm.getCode({ address })` | |
| `provider.getStorageAt(addr, slot)` | `evm.getStorageAt({ address, slot })` | |
| `provider.getGasPrice()` | `evm.getGasPrice()` | |
| `provider.estimateGas(tx)` | `evm.estimateGas(tx)` | |
| `provider.getFeeData()` | `evm.getFeeData()` | alias of `estimateFeesPerGas` |
| `provider.getBalance(addr)` | `getBalance({ address })` | chain-agnostic, top-level |
| `provider.waitForTransaction(h, c, t)` | `evm.waitForTransaction(h, c, t)` | positional alias |
| `contract.(args)` (read) | `evm.readContract({ abi, functionName, args })` | explicit form |
| `contract.(args)` (write) | `evm.writeContract({ abi, functionName, args })` | returns envelope |
| `Multicall contract.aggregate(...)` | `evm.multicall({ contracts })` | Multicall3 default |
| `signer.sendTransaction(tx)` | `evm.sendTransaction(tx)` | returns envelope |
| `signer.signMessage(msg)` | `signMessage(msg)` | chain-agnostic, top-level |
| `signer.signTypedData(d, t, v)` | `evm.signTypedData({ domain, types, primaryType, message })` | params bundled |
| `verifyMessage(msg, sig)` | `evm.verifyMessage({ address, message, signature })` | explicit address |
| `verifyTypedData(d, t, v, sig)` | `evm.verifyTypedData({ address, domain, types, primaryType, message, signature })` | |
| `formatUnits`, `parseUnits` | `formatUnits`, `parseUnits` (top-level) | identical semantics |
| `formatEther`, `parseEther` | use `formatUnits(x, 18)` / `parseUnits(x, 18)` | no dedicated export |
## Provider / Signer — gone
Ethers is built around a `Provider` (reads) + `Signer` (writes) model. Oviato doesn't expose either — the session *is* the signer, and reads go through our backend RPC.
**ethers v6**
```ts
import { BrowserProvider, parseEther } from 'ethers';
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const balance = await provider.getBalance(address);
const tx = await signer.sendTransaction({ to, value: parseEther('0.01') });
await tx.wait();
```
**Oviato**
```ts
import { evm, getBalance, parseUnits } from '@oviato/connect';
const { balance } = await getBalance({ address });
const r = await evm.sendTransaction({ to, value: parseUnits('0.01', 18) });
if (r.status === 'success') {
await evm.waitForTransactionReceipt({ hash: r.data.hash });
}
```
## Contract instance → explicit ABI calls
Ethers' `Contract` class wraps a contract with ABI-derived methods. Oviato's equivalent is passing the ABI to `readContract` / `writeContract` explicitly.
**ethers**
```ts
const usdc = new Contract(USDC_ADDRESS, ERC20_ABI, signer);
const bal = await usdc.balanceOf(address);
await usdc.transfer(to, amount);
```
**Oviato**
```ts
const bal = await evm.readContract({
address: USDC_ADDRESS,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [address],
});
await evm.writeContract({
address: USDC_ADDRESS,
abi: ERC20_ABI,
functionName: 'transfer',
args: [to, amount],
});
```
A touch more verbose per call, but it keeps the ABI at the call site — easier to audit + tree-shake.
If you miss the `contract.fn()` style, wrap it yourself:
```ts
function tokenContract(address: `0x${string}`, abi: readonly AbiItem[]) {
return new Proxy({}, {
get(_, fn: string) {
return (...args: unknown[]) =>
evm.readContract({ address, abi, functionName: fn, args });
},
});
}
const usdc = tokenContract(USDC_ADDRESS, ERC20_ABI);
const bal = await usdc.balanceOf(address); // works
```
(Reads only — for writes you'd branch to `evm.writeContract`. Not recommended for production.)
## The envelope adapter
Ethers methods throw on failure and return raw values. Oviato returns a discriminated envelope. Drop-in adapter:
```ts
async function unwrap(p: Promise) {
const r = await p;
if (r.status === 'success') return r.data;
throw new Error(r.message ?? 'Unknown error');
}
// Now looks like ethers
const { hash } = await unwrap(evm.sendTransaction({ to, value }));
```
## BigNumber → bigint
Ethers v5 used `BigNumber` (custom class). Ethers v6 moved to native `bigint`. Oviato is bigint-native — no class wrappers, no `.toBigInt()` calls.
If you're on v5, the conversion is:
```ts
// v5
const b = BigNumber.from('1000000');
b.add(500).toBigInt();
// Oviato
const b = 1_000_000n;
b + 500n;
```
## Signatures: split vs joined
Ethers v5 had both split (`{ r, s, v }`) and joined (`string`) signatures depending on the API. Oviato returns joined hex strings (`0x` + 130 bytes) — matching v6 + viem.
```ts
// Oviato
const r = await evm.signTypedData({ domain, types, primaryType, message });
const sig = r.data.signature; // '0x...' (130 bytes)
// If you need split (for a permit() call)
const v = parseInt(sig.slice(130, 132), 16);
const r_ = `0x${sig.slice(2, 66)}`;
const s_ = `0x${sig.slice(66, 130)}`;
```
## `signTypedData`: domain/types/message bundle
Ethers passes domain, types, and value (ethers v5) or message (ethers v6) as three positional args. Oviato bundles them:
**ethers v6**
```ts
const sig = await signer.signTypedData(domain, types, message);
```
**Oviato**
```ts
const r = await evm.signTypedData({
domain,
types,
primaryType: 'YourType',
message,
});
```
Oviato requires `primaryType` explicitly. Ethers infers it from the single
top-level type in `types`. If your ethers code relied on that inference,
declare the primary type directly.
Also: don't include `EIP712Domain` in `types`. Oviato computes the domain hash from the `domain` object directly. Adding it manually changes the hash.
## Chain switching
**ethers** doesn't have a standard chain-switch API — you'd call `window.ethereum.request({ method: 'wallet_switchEthereumChain' })`.
**Oviato**
```ts
import { switchNetwork, Network } from '@oviato/connect';
await switchNetwork(Network.BASEMAINNET);
// Also available as evm.switchChain(...) for familiarity
await evm.switchChain(Network.BASEMAINNET);
```
Switching requires a passkey tap (server re-issues the JWT for the new chain). Different from ethers' UX where switching is silent.
## What's different by design
- **No custom Provider/Signer objects.** The session is implicit global state. You don't thread a provider through your code.
- **No local key management.** Oviato holds keys via WebAuthn passkeys. `sendTransaction` triggers a bridge popup, not a local signer.
- **No ABI-level type inference.** Ethers v6 types `contract.fn()` return types from the ABI. Oviato's `readContract` asks you to declare `` explicitly — the ABI travels to the server for encoding.
- **Server-side nonce tracker.** You can't override the nonce by default — the tracker handles it. Pass `nonce:` explicitly if you really need to.
## What Oviato adds over ethers
Same list as for viem — passkey custody, server-side nonce tracking, specialized bridge UI, multi-chain session, curated token registry.
## Side-by-side: a typical tx flow
**ethers v6**
```ts
import { BrowserProvider, parseEther } from 'ethers';
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
try {
const tx = await signer.sendTransaction({
to: '0x742d35Cc...',
value: parseEther('0.01'),
});
const rx = await tx.wait();
console.log('Mined in block', rx?.blockNumber);
} catch (err) {
console.error('Tx failed', err);
}
```
**Oviato**
```ts
import { evm, parseUnits } from '@oviato/connect';
const r = await evm.sendTransaction({
to: '0x742d35Cc...',
value: parseUnits('0.01', 18),
});
if (r.status === 'success') {
const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash });
console.log('Mined in block', rx?.blockNumber);
} else {
console.error('Tx failed', r.message);
}
```
## Related
- [EVM Overview](/evm/overview)
- [Migrating from viem](/evm/migrating-from-viem)
---
# Section: UTXO
# UTXO Overview
> Oviato's UTXO surface — PSBT signing for Bitcoin, scoped under utxo.*. Everything else lives at the top level (sendTransfer, signMessage, getBalance).
Canonical URL: https://docs.oviato.com/utxo/overview
File: utxo/overview.mdx
The `utxo` namespace is narrow on purpose — most UTXO-chain work is either chain-agnostic (covered at the top level) or a PSBT flow.
```ts
import { utxo } from '@oviato/connect';
const r = await utxo.signPsbt({
psbt: '70736274ff...', // PSBT hex
broadcast: true,
});
```
## What's in the namespace
| Method | Purpose |
|---|---|
| `utxo.signPsbt` | Sign a Partially-Signed Bitcoin Transaction (PSBT), optionally broadcast after signing. |
That's it for v1. More UTXO-specific surface area arrives with future releases.
## What's NOT in the namespace
Most of the UTXO workflow lives at the top level because it's chain-agnostic:
| Task | Use |
|---|---|
| Send BTC to an address | [`sendTransfer`](/chain-agnostic/send-transfers) |
| Multi-recipient send | [`sendTransfer`](/chain-agnostic/send-transfers) with `recipients: [...]` |
| Sign a message | [`signMessage`](/chain-agnostic/sign-messages) |
| Fetch balance (confirmed + unconfirmed) | [`rpc.getBalance`](/chain-agnostic/get-balance) |
| Switch to Bitcoin | [`switchNetwork`](/chain-agnostic/session-and-network) |
Use `utxo.*` specifically when you have a **PSBT** — which happens when:
- Coin-selection or output construction runs server-side (your backend builds, frontend signs)
- You're integrating with a protocol that hands you a PSBT to co-sign
- You need explicit control over inputs, outputs, and sighash flags
## PSBT — the quick explainer
A [PSBT](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki) (Partially-Signed Bitcoin Transaction) is Bitcoin's native format for transactions that need external logic or multiple signatures before broadcast.
Key ideas:
- **It's data, not code.** A PSBT is just a hex string you can pass around.
- **It's partial.** Inputs can be unsigned, partially signed (multi-sig), or fully signed.
- **It's verifiable.** Anyone can inspect the inputs + outputs before signing — no hidden state.
- **It's broadcastable.** Once fully signed, it's serialized into a standard transaction + broadcast.
Oviato's `utxo.signPsbt` signs the inputs your session owns (leaving others untouched), and optionally broadcasts after signing.
## When to reach for PSBTs
- **Server-side coin selection.** Your backend selects UTXOs and constructs the tx; the frontend just signs.
- **Batching with custom outputs.** `sendTransfer` covers multi-recipient; PSBTs give you more control (change address placement, OP_RETURN content, specific sighash flags).
- **Pre-built transactions from a third party.** A counterparty hands you a PSBT for you to add your signature to.
If you don't need any of the above — just `sendTransfer`. It's simpler.
## Supported UTXO networks
- `Network.BTCMAINNET` (`'bitcoin'`)
- `Network.BTCTESTNET` (`'bitcointestnet'`)
More UTXO chains (Dogecoin, Litecoin) are on the roadmap.
## Session scope
A Bitcoin session can't use `evm.*` methods (they throw). Use `switchNetwork(Network.BTCMAINNET)` to scope the session to Bitcoin before calling `utxo.*`.
## Explore
---
# Sign PSBT
> Sign a Partially-Signed Bitcoin Transaction with the user's passkey. Supports selective input signing, sighash flags, and optional broadcast.
Canonical URL: https://docs.oviato.com/utxo/sign-psbt
File: utxo/sign-psbt.mdx
`utxo.signPsbt` signs the inputs your session owns in a PSBT, leaving all other inputs untouched. Optionally broadcasts the fully-signed result.
```ts
import { utxo } from '@oviato/connect';
const r = await utxo.signPsbt({
psbt: '70736274ff01007d...', // PSBT as hex
broadcast: true,
});
if (r.status === 'success') {
if (r.type === 'signPsbtBroadcastSuccess') {
console.log('TxID:', r.data.txid);
} else if (r.type === 'signPsbtSuccess') {
console.log('Signed PSBT:', r.data.psbt);
}
}
```
## Parameters
| Field | Type | Notes |
|---|---|---|
| `psbt` | `string` | PSBT as **hex** (not base64). Required. |
| `signInputs` | `SignPsbtInput[]?` | If omitted, every owned input is signed. |
| `broadcast` | `boolean?` | If `true`, broadcasts after signing. Default `false`. |
| `method` | `'popup' \| 'iframe'?` | How the approval UI opens. |
### `SignPsbtInput`
```ts
type SignPsbtInput = {
index: number; // Which input to sign (0-based)
address: string; // The address that owns that input — used for key derivation
sighashTypes?: number[]; // Optional SIGHASH flags (default: SIGHASH_ALL)
};
```
## Response shape
Discriminated by whether you asked to broadcast.
```ts
type Result =
| { status: 'success'; type: 'signPsbtSuccess'; data: { psbt: string } }
| { status: 'error'; message: string };
```
Use this when a different party (a marketplace, aggregator, second cosigner) will broadcast after you sign.
```ts
type Result =
| { status: 'success'; type: 'signPsbtBroadcastSuccess'; data: { txid: string; explorerUrl?: string } }
| { status: 'error'; message: string };
```
Use this for straightforward "sign + send" flows where you built the PSBT yourself.
## Common flows
### Sign all owned inputs + broadcast
The simplest case. `psbt` in, `txid` out.
```ts
const r = await utxo.signPsbt({
psbt: rawPsbtHex,
broadcast: true,
});
if (r.status === 'success' && r.type === 'signPsbtBroadcastSuccess') {
window.open(r.data.explorerUrl!, '_blank');
}
```
### Sign without broadcasting
Useful for marketplace flows — you sign your half, the counterparty signs + broadcasts.
```ts
const r = await utxo.signPsbt({
psbt: marketplaceBuiltPsbt,
broadcast: false,
});
if (r.status === 'success' && r.type === 'signPsbtSuccess') {
await fetch('/api/marketplace/complete', {
method: 'POST',
body: JSON.stringify({ signedPsbt: r.data.psbt }),
});
}
```
### Sign specific inputs only
If the PSBT has inputs owned by multiple addresses and you want to sign only some:
```ts
import { SigHashTypes } from '@oviato/connect';
const r = await utxo.signPsbt({
psbt: rawPsbtHex,
signInputs: [
{ index: 0, address: session.address },
{ index: 2, address: session.address, sighashTypes: [SigHashTypes.SINGLE | SigHashTypes.ANYONECANPAY] },
],
broadcast: false,
});
```
Default sighash is `SIGHASH_ALL` — signs over all inputs + outputs. Use
`SIGHASH_SINGLE | SIGHASH_ANYONECANPAY` for marketplace flows where your
signature covers only your input and its matching output.
## Getting a PSBT
PSBTs usually come from one of two places:
1. **Your own backend.** You do coin-selection + construct the transaction server-side and pass the PSBT hex to the frontend for signing.
2. **A counterparty or protocol.** A different party builds the PSBT and hands it to you to add your signature.
Either way, the PSBT arrives as a hex string that you pass directly to `utxo.signPsbt`.
## Validation before signing
The bridge UI decodes the PSBT before showing the user, flagging:
- Which inputs they own (green), which they don't (grey)
- Which outputs go to them (change), which go elsewhere
- Estimated network fee
- Any unusual characteristics (OP_RETURN, large opcodes, etc.)
The user can always see the full decoded tx before approving.
## Related
- [Send Transfers](/chain-agnostic/send-transfers) — simpler native BTC sends
- [Recipes](/utxo/recipes) — common UTXO flows
---
# UTXO Recipes
> Common Bitcoin flows — multi-recipient send, balance prechecks, confirmation polling.
Canonical URL: https://docs.oviato.com/utxo/recipes
File: utxo/recipes.mdx
Copy-paste flows for the Bitcoin tasks most apps need.
## Multi-recipient payout
Send to N addresses in one transaction. Saves on fees vs N separate txs, since Bitcoin supports multiple outputs natively.
```ts
import { sendTransfer } from '@oviato/connect';
const r = await sendTransfer({
recipients: [
{ address: 'bc1q...', amount: 10000 },
{ address: 'bc1p...', amount: 5000 },
{ address: 'bc1q...', amount: 7500 },
],
fee: 'auto',
memo: 'Weekly payouts',
});
if (r.status === 'success' && r.type === 'sendTransferSuccess') {
console.log('Batch txid:', r.data.txid);
}
```
## Balance precheck before a send
```ts
import { rpc, sendTransfer, getSession } from '@oviato/connect';
async function sendWithPrecheck(to: string, amount: number) {
const session = getSession()!;
const { result } = await rpc.getBalance({ address: session.address });
if (!result || result.chainType !== 'utxo') throw new Error('Not a UTXO session');
const confirmed = result.balance.confirmed.raw;
const estFee = 1500; // rough sat estimate; better: query mempool fees
if (confirmed < amount + estFee) {
throw new Error(`Insufficient confirmed balance: ${confirmed} sats`);
}
return sendTransfer({
recipient: { address: to, amount },
fee: 'auto',
});
}
```
Use `confirmed` (not total) for the precheck. Unconfirmed funds can be
replaced via RBF before they mine — relying on them for the next send
risks a double-spend.
## Sign a server-built PSBT
Common pattern when coin selection happens server-side — your backend builds a PSBT, your frontend calls `utxo.signPsbt` to sign + broadcast.
```ts
import { utxo } from '@oviato/connect';
async function acceptListing(listingId: string) {
// 1. Backend returns a PSBT
const { psbt } = await fetch(`/api/listings/${listingId}/prepare`).then((r) => r.json());
// 2. User signs + broadcasts
const r = await utxo.signPsbt({ psbt, broadcast: true });
if (r.status === 'success' && r.type === 'signPsbtBroadcastSuccess') {
return { txid: r.data.txid };
}
throw new Error(r.status === 'error' ? r.message : 'Sign failed');
}
```
## Sign-only (counterparty broadcasts)
When a different party will broadcast after you sign:
```ts
const r = await utxo.signPsbt({ psbt: serverPsbt, broadcast: false });
if (r.status === 'success' && r.type === 'signPsbtSuccess') {
await fetch('/api/listings/complete', {
method: 'POST',
body: JSON.stringify({ signedPsbt: r.data.psbt }),
});
}
```
## Watch for confirmation
The SDK doesn't currently ship a `waitForConfirmation` for UTXO. Poll via the RPC client:
```ts
import { rpc } from '@oviato/connect';
async function waitForConfirmation(txid: string, address: string, minConfirms = 1) {
while (true) {
const r = await rpc.getTransactionHistory({ address, limit: 20 });
const match = r.result?.transactions.find((t) => t.txid === txid);
if (match && match.confirmations >= minConfirms) return match;
await new Promise((r) => setTimeout(r, 15_000)); // 15s poll
}
}
```
For production, prefer a block-indexer webhook over polling.
## Related
- [Sign PSBT](/utxo/sign-psbt) — full `utxo.signPsbt` reference
- [Send Transfers](/chain-agnostic/send-transfers) — simpler native BTC sends
---
# Section: React Components
# React Components Overview
> The React-only surface of @oviato/connect — provider, hooks, and drop-in UI components.
Canonical URL: https://docs.oviato.com/react-components/overview
File: react-components/overview.mdx
`@oviato/connect/client` is where the React-specific surface lives — a provider for session state, hooks for accessing it, and pre-built UI components you can drop into your app.
```tsx
import {
OviConnectProvider,
useOviConnect,
ConnectButton,
Connect,
} from '@oviato/connect/client';
```
Everything else — chain-agnostic actions, `evm.*`, `utxo.*`, RPC — lives at the main `@oviato/connect` entry point (which re-exports `@oviato/sdk`). The `/client` subpath is **strictly for React code**.
Not on React? Everything in this section is unavailable. See
[Packages → Choosing a Package](/packages/choosing) for the
framework-agnostic `@oviato/sdk` path.
## The two-import pattern
A normal Oviato React app imports from two places:
```tsx
// Actions (works anywhere — just called from React)
import { sendTransfer, signMessage, evm, utxo } from '@oviato/connect';
// React-only: provider, hooks, components
import { OviConnectProvider, useOviConnect, ConnectButton } from '@oviato/connect/client';
```
The split keeps your bundle clean — server components and non-React code paths don't pull in React.
## What's in here
### Provider
Wraps your app to initialize the SDK + expose session state to hooks. One per tree.
```tsx
{children}
```
Config: `appId`, `theme`, `dns`, `type`, `domain`. See [Provider](/react-components/provider).
### Hooks
`useOviConnect()` — the primary hook. Gives you `session`, `isLoading`, `disconnect`, plus `signMessage` / `sendTransfer` / `switchNetwork` as callbacks.
`useOviStore()` — direct access to the Zustand store for advanced state subscriptions.
See [Hooks](/react-components/hooks).
### Components
`` — full auth form (username input + passkey flow). Good for dedicated sign-in pages.
`` — one-click auth button. Good for headers, modals.
Both are styled + theme-aware out of the box. See [Components](/react-components/components).
## SSR-safe
Everything in this section is compatible with Next.js App Router, Pages Router, and other SSR frameworks:
- Provider mounts client-side only (no hydration flash)
- Hooks guard against `useEffect`-less server renders
- Components handle `'use client'` boundaries cleanly
See the [Quickstart](/getting-started/quickstart) for the exact provider placement pattern.
## CSS
The React components ship with their own stylesheet. Import once at the app root:
```tsx
import '@oviato/connect/client.css';
```
Or import the full styles (includes modal, button, overlays):
```tsx
import '@oviato/connect/styles.css';
```
Both are scoped with a CSS-layer prefix so they don't collide with your Tailwind / shadcn / MUI.
## Theme
The provider accepts `theme: 'light' | 'dark' | 'auto'`. `'auto'` respects `prefers-color-scheme`. Override via the dashboard for per-app branding (accent color, logo).
## Explore
---
# OviConnectProvider
> The React provider that initializes the SDK and exposes session state to child hooks + components.
Canonical URL: https://docs.oviato.com/react-components/provider
File: react-components/provider.mdx
`` is the entry point for every React app using Oviato. It initializes the SDK with your `appId`, mounts the session store, and makes the `useOviConnect` hook work across the tree.
```tsx
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';
```
Place this **as high in your tree as possible** — above any component that
calls `useOviConnect()` or renders ``. A common pattern is
wrapping the whole `` or the root `` layout.
## Where to mount it
Create a client-only Providers wrapper (required because the provider uses React context).
```tsx title="app/providers.tsx"
'use client';
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```tsx title="app/layout.tsx"
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
{children}
);
}
```
```tsx title="pages/_app.tsx"
import type { AppProps } from 'next/app';
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';
export default function App({ Component, pageProps }: AppProps) {
return (
);
}
```
```tsx title="src/main.tsx"
import React from 'react';
import ReactDOM from 'react-dom/client';
import { OviConnectProvider } from '@oviato/connect/client';
import '@oviato/connect/client.css';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
,
);
```
## Props
| Prop | Type | Default | Notes |
|---|---|---|---|
| `appId` | `string` | — | **Required.** From the [Developer Dashboard](/developer-dashboard/setting-up-app). |
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | `'auto'` respects `prefers-color-scheme`. |
| `domain` | `string` | session-set | Override the domain the SDK registers under. Useful for custom subdomains. |
| `dns` | `string` | auto | Override the backend DNS. For staging or self-hosted deployments. |
| `type` | `'production' \| 'staging' \| 'development'` | auto | Forces a specific environment tier; normally detected from `appId`. |
| `accent_color` | `string` | dashboard | Hex color for UI accents. Overrides the dashboard setting. |
| `logo_url` | `string` | dashboard | Logo shown in the auth popup. Overrides the dashboard setting. |
**Do not pass `network:` to the provider.** Earlier versions accepted a
provider-level network, but that made per-action network overrides
ambiguous. Specify the network on ``, ``, and
imperative calls like `login`, `register`, `authenticate` instead.
## What happens on mount
The provider is a thin wrapper around `initialize()` from the core SDK.
Everything useful flows through the Zustand session store — the provider
just sets it up and keeps it alive for the component tree.
1. Client-side hydration runs → provider reads localStorage for a cached session.
2. `initialize()` is called with `{ appId, theme, ... }` → backend app config is fetched + cached.
3. Session state becomes available via `useOviConnect()`.
4. If there's a cached session, `isLoading` flips to `false` with `session` populated. Otherwise, `session` is `null`.
## Dev-mode overrides
For local development against a staging backend, you can override the bridge + RPC origins via `window` globals:
```tsx
// Before the provider mounts — e.g. in a useEffect in _app.tsx
if (process.env.NODE_ENV === 'development') {
(window as any).__OVIATO_BRIDGE_ORIGIN__ = 'http://localhost:5173';
(window as any).__OVIATO_RPC_ORIGIN__ = 'http://localhost:8787';
(window as any).__OVI_FORCE_IFRAME__ = true; // popups unreliable in local dev
}
```
These are convenience hooks for local iteration — not a production config surface.
## SSR safety
The provider is `'use client'` — it never runs on the server. A server-rendered page will see `session` as `undefined` on first paint, then populate on client hydration. Use `isLoading` to avoid layout shift:
```tsx
function MyComponent() {
const { session, isLoading } = useOviConnect();
if (isLoading) return ;
return session ? : ;
}
```
## Multiple providers
Don't nest. The provider uses React context + a global Zustand store. Nesting duplicates state but only the innermost wins — confusing failure modes.
## Related
- [Hooks](/react-components/hooks) — `useOviConnect`, `useOviStore`
- [Components](/react-components/components) — ``, ``
- [Quickstart](/getting-started/quickstart) — end-to-end setup
---
# Using Hooks
> React hooks for Oviato Connect - useOviConnect and useOviStore
Canonical URL: https://docs.oviato.com/react-components/hooks
File: react-components/hooks.mdx
@oviato/connect provides React hooks to access authentication state and SDK functionality in your React components.
## useOviConnect Hook
The primary hook for accessing Oviato authentication state and methods. Must be used inside `OviConnectProvider`.
### Basic Usage
```tsx
import { useOviConnect } from '@oviato/connect/client';
function MyComponent() {
const { session, isLoading, disconnect } = useOviConnect();
if (isLoading) {
return
Loading...
;
}
if (!session) {
return
Not connected
;
}
return (
Address: {session.address}
);
}
```
### Return Values
The `useOviConnect` hook returns an object with the following properties:
```typescript
{
// Current user session (null if not authenticated)
session: Session | null;
// Whether SDK is initializing
isLoading: boolean;
// Whether SDK is ready to use
ready: boolean;
// Update the session
setSession: (session: Session) => void;
// Disconnect and clear session
disconnect: () => void;
// Get current initialization options
getInitOptions: () => InitOptions | null;
}
```
### Session Object
When authenticated, the session object contains:
```typescript
type Session = {
address: string; // Wallet address
publicKey: string; // Public key (hex)
username?: string; // OVI.ID username (if set)
network: Network; // Current network
appId: string; // Your app ID
userId: string; // Oviato user ID
createdAt: number; // Timestamp
};
```
### Complete Example
```tsx
'use client';
import { useOviConnect } from '@oviato/connect/client';
import { authenticate, signMessage } from '@oviato/connect';
export default function WalletPage() {
const { session, isLoading, ready, disconnect } = useOviConnect();
const handleConnect = async () => {
const result = await authenticate();
if (result.status === 'error') {
console.error('Auth failed:', result.message);
}
};
const handleSign = async () => {
const result = await signMessage('Hello from Oviato!');
if (result.status === 'success') {
console.log('Signature:', result.data.signature);
}
};
if (isLoading || !ready) {
return
Loading Oviato...
;
}
if (!session) {
return (
Connect Your Wallet
);
}
return (
Connected
Address: {session.address}
{session.username &&
Username: @{session.username}
}
Network: {session.network}
);
}
```
## useOviStore Hook
Advanced hook for accessing the Zustand store directly. Provides more granular control over state subscriptions.
### Basic Usage
```tsx
import { useOviStore } from '@oviato/connect';
function MyComponent() {
// Subscribe to specific state
const session = useOviStore(state => state.session);
const isAuthenticated = useOviStore(state => state.isAuthenticated());
return (
{isAuthenticated ? (
Address: {session?.address}
) : (
Not connected
)}
);
}
```
### When to Use useOviStore
Use `useOviStore` when you need:
- Fine-grained subscriptions to avoid unnecessary re-renders
- Access to store methods directly
- Integration with other state management solutions
- Advanced state debugging
For most cases, `useOviConnect` is recommended as it provides a simpler API.
### Available State
```typescript
{
// Session state
session: Session | null;
isAuthenticated: () => boolean;
// Loading state
isLoading: boolean;
ready: boolean;
// Methods
setSession: (session: Session) => void;
clearSession: () => void;
setLoading: (loading: boolean) => void;
setReady: (ready: boolean) => void;
}
```
### Performance Optimization
```tsx
import { useOviStore } from '@oviato/connect';
function AddressDisplay() {
// Only re-renders when address changes
const address = useOviStore(state => state.session?.address);
return
{address || 'Not connected'}
;
}
function LoadingIndicator() {
// Only re-renders when loading state changes
const isLoading = useOviStore(state => state.isLoading);
return isLoading ? : null;
}
```
### Combining Multiple State Values
```tsx
import { useOviStore } from '@oviato/connect';
function WalletInfo() {
// Efficient: single subscription for multiple values
const { address, network, username } = useOviStore(state => ({
address: state.session?.address,
network: state.session?.network,
username: state.session?.username,
}));
return (
Address: {address}
Network: {network}
{username &&
Username: @{username}
}
);
}
```
## Hook Rules
Both hooks must follow React's rules of hooks:
- ✅ Must be used inside `OviConnectProvider`
- ✅ Only call at the top level of components
- ✅ Only call from React function components or custom hooks
- ❌ Don't call inside loops, conditions, or nested functions
### Error: Hook Used Outside Provider
```tsx
// ❌ Wrong: Used outside provider
function App() {
const { session } = useOviConnect(); // Error!
return
{session?.address}
;
}
// ✅ Correct: Used inside provider
function App() {
return (
);
}
function MyComponent() {
const { session } = useOviConnect(); // Works!
return
{session?.address}
;
}
```
## TypeScript Support
Both hooks have full TypeScript support:
```tsx
import { useOviConnect } from '@oviato/connect/client';
import type { Session } from '@oviato/connect';
function MyComponent() {
const { session }: { session: Session | null } = useOviConnect();
// TypeScript knows session can be null
if (session) {
// Here, TypeScript knows session is Session
console.log(session.address); // ✅ Type-safe
}
}
```
## Next Steps
- **[Components Reference](/react-components/components)** - Explore pre-built UI components
- **[Signing Messages](/chain-agnostic/sign-messages)** - Sign messages with hooks
- **[Signing Transactions](/chain-agnostic/send-transfers)** - Send transactions with hooks
---
# Components Reference
> Pre-built UI components for Oviato Connect
Canonical URL: https://docs.oviato.com/react-components/components
File: react-components/components.mdx
@oviato/connect provides pre-built React components for authentication and wallet management. These components handle the complex UI and authentication flow, allowing you to integrate Oviato quickly.
## Connect
A pre-built authentication form with username input and availability checking. This is the easiest way to add Oviato authentication to your app.
### Basic Usage
```tsx
import { Connect } from '@oviato/connect/client';
function MyComponent() {
return (
{
console.log('Connected:', data.address);
console.log('Public Key:', data.publicKey);
}}
/>
);
}
```
### Props
```typescript
interface ConnectProps {
/** Callback fired on successful authentication */
callback?: (data: {
address: string;
publicKey: string;
}) => void;
}
```
### Features
The `Connect` component includes:
- **Username input** with real-time validation
- **Availability checking** - verifies username is not taken
- **Auto login/register** - automatically determines if user is new or existing
- **Loading states** - shows feedback during authentication
- **Built-in styling** - works out of the box with minimal setup
### Complete Example
```tsx
'use client';
import { Connect } from '@oviato/connect/client';
import { useOviConnect } from '@oviato/connect/client';
export default function AuthPage() {
const { session } = useOviConnect();
if (session) {
return (
);
}
```
### Popup vs Iframe Method
Choose between popup or iframe authentication:
```tsx
// Iframe (default) - opens in modal overlay
// Popup - opens in new browser window
```
**When to use each:**
- **Iframe** (recommended): Better UX, stays in-page, works on mobile
- **Popup**: Use when iframe is blocked or you need popup-specific behavior
### Custom Auth Flow Example
```tsx
'use client';
import { ConnectButton, useOviConnect } from '@oviato/connect/client';
export default function AuthPage() {
const { session, disconnect } = useOviConnect();
if (session) {
return (
Connected: {session.address}
);
}
return (
Welcome to My App
Connect your wallet to get started
{
console.log('Connected successfully!');
}}
/>
);
}
```
## Styling Components
All components support className and style props for customization:
```tsx
// Using className
// Using inline styles
// Using custom children with full control
```
## TypeScript Support
All components have full TypeScript support:
```tsx
import type { ConnectButtonProps, ConnectProps } from '@oviato/connect/client';
const buttonProps: ConnectButtonProps = {
text: 'Connect',
onSuccess: (data) => {
// data is fully typed
console.log(data.address);
},
};
const connectProps: ConnectProps = {
callback: (data) => {
// data is fully typed
console.log(data.publicKey);
},
};
```
## Next Steps
- **[Using Hooks](/react-components/hooks)** - Learn about useOviConnect and useOviStore
- **[Signing Messages](/chain-agnostic/sign-messages)** - Sign messages with the wallet
- **[Signing Transactions](/chain-agnostic/send-transfers)** - Send transactions
---
# Section: x402 Payments
# x402 Payments
> Enable HTTP 402 Payment Required flows with @oviato/connect
Canonical URL: https://docs.oviato.com/x402/overview
File: x402/overview.mdx
x402 is a protocol for native web payments using HTTP 402 (Payment Required) responses. Oviato SDK provides built-in support for handling x402 payments on Base.
## What is x402?
x402 enables seamless micropayments on the web:
1. Server returns `402 Payment Required` with payment details
2. Client signs the payment request with user's wallet
3. Server verifies signature and grants access
This enables pay-per-use APIs, premium content, and micropayments without traditional payment processors.
## Quick Start: x402.pay()
The easiest way to handle x402 payments — one line handles everything:
```javascript
import { x402 } from "@oviato/connect";
// Fetch with automatic payment handling
const { response, paid, amount, token } = await x402.pay(
"https://api.example.com/premium"
);
const data = await response.json();
if (paid) {
console.log(`Paid ${amount} ${token} for access`);
}
```
`x402.pay()` automatically:
1. Makes the initial request
2. Detects 402 Payment Required
3. Opens the payment popup for user approval
4. Signs the payment
5. Retries with the signed payment header
### With Fetch Options
```javascript
const { response, paid } = await x402.pay("https://api.example.com/data", {
method: "POST",
body: JSON.stringify({ query: "..." }),
headers: { "Content-Type": "application/json" },
});
```
## Low-Level: x402.sign()
For more control, use `x402.sign()` to sign payment headers manually:
```javascript
import { x402 } from "@oviato/connect";
// Make initial request
const response = await fetch("https://api.example.com/premium");
if (response.status === 402) {
// Get payment header
const paymentHeader = response.headers.get("payment-required");
// Sign the payment
const { signedPayload } = await x402.sign({ paymentHeader });
// Retry with signature
const paidResponse = await fetch("https://api.example.com/premium", {
headers: { "payment-signature": signedPayload },
});
const data = await paidResponse.json();
}
```
## Helper Functions
### Check if Payment Required
```javascript
import { x402 } from "@oviato/connect";
const response = await fetch(url);
if (x402.isPaymentRequired(response)) {
// Handle 402...
}
```
### Get Payment Details
```javascript
import { x402 } from "@oviato/connect";
const response = await fetch(url);
const details = x402.getPaymentDetails(response);
if (details) {
console.log("Amount:", details.accepts[0].amount);
console.log("Network:", details.accepts[0].network);
}
```
## Supported Networks
x402 payments are currently supported on Base:
- `base` - Base Mainnet
- `basesepolia` - Base Sepolia
## API Reference
### x402.pay(url, options?)
Fetch a URL with automatic x402 payment handling.
```typescript
type X402PayOptions = RequestInit & {
acceptIndex?: number; // Which payment option to use (default: 0)
method?: "popup" | "iframe"; // Bridge method
};
type X402PayResult = {
response: Response; // Final HTTP response
paid: boolean; // Whether payment was made
amount?: string; // Formatted amount paid
token?: string; // Token name (e.g., "USDC")
network?: string; // Network name
};
```
### x402.sign(options)
Sign a payment-required header.
```typescript
type X402SignOptions = {
paymentHeader: string | X402PaymentRequired; // Base64 header or parsed object
acceptIndex?: number; // Which payment option (default: 0)
method?: "popup" | "iframe"; // Bridge method
};
type X402SignResult = {
signedPayload: string; // Signed payment to send in header
authorization: object; // Authorization details
signature: string; // Raw signature
network: string; // Network used
chainId: number; // Chain ID
};
```
## Error Handling
```javascript
import { x402 } from "@oviato/connect";
try {
const { response, paid } = await x402.pay(url);
// Success...
} catch (error) {
if (error.message.includes("User rejected")) {
console.log("User cancelled payment");
} else if (error.message.includes("Insufficient")) {
console.log("Not enough balance");
} else {
console.log("Payment failed:", error.message);
}
}
```
## Security Considerations
1. **Verify payment details** - Always show users what they're paying for
2. **Check amounts** - Validate payment amounts before signing
3. **Trusted servers only** - Only pay servers you trust
4. **Review permissions** - Users approve each payment in the signing modal
## Use Cases
- **Pay-per-API** - Charge per API call
- **Premium content** - Unlock articles, videos, data
- **Micropayments** - Sub-cent transactions
- **AI inference** - Pay for compute on-demand
- **Data feeds** - Real-time market data access
## Next Steps
- **[Wallet Operations](/chain-agnostic/overview)** - Other wallet functions
- **[RPC & Utilities](/reference/rpc)** - Query balances and transactions
---
# Section: Reference
# RPC & Utilities
> Query blockchain data, resolve usernames, and use utility helpers with @oviato/connect
Canonical URL: https://docs.oviato.com/reference/rpc
File: reference/rpc.mdx
@oviato/connect provides a powerful RPC client for querying blockchain data and a collection of utility functions for common tasks.
## RPC Client Overview
The RPC client uses JSON-RPC 2.0 protocol to communicate with Oviato's backend services. It automatically handles:
- **Authentication** - Includes auth tokens when available
- **Network context** - Routes requests to the correct network
- **Rate limiting** - Respects backend rate limits
- **Error handling** - Provides typed error responses
```javascript
import { rpc, rpcRequest } from "@oviato/connect";
// Use the default client
const balance = await rpc.getBalance({ address: "bc1q..." });
```
## Get Balance
Retrieve the balance for any address. The response format varies based on network type.
```javascript
import { rpc, getSession } from "@oviato/connect";
const session = getSession();
const result = await rpc.getBalance({
address: session.address,
});
if (result.result) {
const balance = result.result;
console.log("Address:", balance.address);
console.log("Network:", balance.networkName);
console.log("Symbol:", balance.symbol);
// Balance is returned in three formats
console.log("Raw (satoshis/wei):", balance.balance.raw);
console.log("Decimal value:", balance.balance.value);
console.log("Formatted:", balance.balance.formatted); // e.g., "1.5 BTC"
}
```
### EVM Balance Response
For Ethereum and EVM-compatible networks:
```typescript
interface EvmBalanceResponse {
address: string;
network: string; // e.g., "ethmainnet"
networkName: string; // e.g., "Ethereum Mainnet"
chainType: "evm";
symbol: string; // e.g., "ETH"
decimals: number; // e.g., 18
balance: {
raw: string; // Balance in wei
value: string; // Decimal value (e.g., "1.50000000")
formatted: string; // Human-readable (e.g., "1.5 ETH")
};
}
```
### UTXO Balance Response
For Bitcoin and UTXO-based networks, includes confirmed/unconfirmed breakdown:
```typescript
interface UtxoBalanceResponse {
address: string;
network: string; // e.g., "bitcoinmainnet"
networkName: string; // e.g., "Bitcoin Mainnet"
chainType: "utxo";
symbol: string; // e.g., "BTC"
decimals: number; // e.g., 8
balance: {
raw: string; // Total balance in satoshis
value: string; // Decimal value (e.g., "0.00100000")
formatted: string; // Human-readable (e.g., "0.001 BTC")
confirmed: { // On-chain confirmed balance
raw: string;
value: string;
formatted: string;
};
unconfirmed: { // Mempool unconfirmed balance
raw: string;
value: string;
formatted: string;
};
};
}
```
### Display Balance Example
```javascript
import { rpc, getSession } from "@oviato/connect";
async function displayBalance() {
const session = getSession();
const result = await rpc.getBalance({ address: session.address });
if (!result.result) {
console.error("Failed to fetch balance");
return;
}
const { balance, chainType, symbol } = result.result;
// Show formatted balance
console.log(`Balance: ${balance.formatted}`);
// For UTXO networks, show confirmed/unconfirmed
if (chainType === "utxo") {
console.log(`Confirmed: ${balance.confirmed.formatted}`);
console.log(`Pending: ${balance.unconfirmed.formatted}`);
}
}
```
## Resolve OVI.ID Username
Look up blockchain addresses associated with an OVI.ID username:
```javascript
import { resolveUsername } from "@oviato/connect";
const result = await resolveUsername("alice");
if (result.status === "success") {
console.log("Names:", result.data.names);
// Addresses for different networks
result.data.addresses.forEach((addr) => {
console.log(`${addr.network}: ${addr.address}`);
});
}
```
### Using RPC Client
```javascript
import { rpc } from "@oviato/connect";
const result = await rpc.resolveUsername({ username: "alice" });
if (result.result) {
const { names, addresses } = result.result;
// Find Bitcoin address
const btcAddress = addresses.find(a => a.network === "bitcoinmainnet");
if (btcAddress) {
console.log("Bitcoin address:", btcAddress.address);
console.log("Public key:", btcAddress.publicKey);
}
}
```
### Username Resolution Response
```typescript
interface ResolveUsernameResponse {
names: Record; // Name mappings
addresses: Array<{
network: string; // Network identifier
address: string; // Blockchain address
publicKey: string; // Public key
path: number; // Derivation path index
}>;
}
```
## Custom RPC Calls
Make arbitrary RPC calls for advanced use cases:
```javascript
import { rpcRequest, rpc } from "@oviato/connect";
// Using rpcRequest directly
const result = await rpcRequest("customMethod", {
param1: "value1",
param2: "value2",
});
// Using the client's call method
const result2 = await rpc.call("anotherMethod", {
data: "example",
});
// With type parameter
const typedResult = await rpcRequest<{ custom: string }>("getCustomData", {
id: "123",
});
```
### Request Headers
The RPC client automatically includes these headers:
| Header | Description |
| ------------------- | ------------------------------------ |
| `X-Ovi-App-Id` | Your application ID |
| `X-Ovi-Network` | Current network (e.g., bitcoinmainnet) |
| `X-Ovi-Type` | Network type (e.g., utxo, evm) |
| `X-Ovi-Environment` | Environment (mainnet, testnet) |
| `X-Ovi-Version` | SDK version |
| `X-Ovi-Origin` | Request origin |
| `X-Ovi-Address` | User's address (if authenticated) |
| `Authorization` | Bearer token (if authenticated) |
## RPC Error Handling
Handle RPC errors gracefully:
```javascript
import { rpc } from "@oviato/connect";
try {
const result = await rpc.getBalance({ address: "invalid" });
if (result.error) {
console.error("RPC Error:", result.error.message);
console.error("Error Code:", result.error.code);
}
} catch (error) {
console.error("Request failed:", error.message);
}
```
### Error Codes
Standard JSON-RPC 2.0 error codes plus custom Oviato codes:
| Code | Name | Description |
| -------- | ------------------- | ------------------------------ |
| `-32700` | Parse Error | Invalid JSON |
| `-32600` | Invalid Request | Malformed request |
| `-32601` | Method Not Found | Unknown RPC method |
| `-32602` | Invalid Params | Invalid method parameters |
| `-32603` | Internal Error | Server-side error |
| `-32001` | Unauthorized | Authentication required |
| `-32002` | Rate Limit | Too many requests |
| `-32003` | Insufficient Funds | Not enough balance |
## Utility Functions
@oviato/connect includes utility functions for common tasks.
### Browser Detection
Detect browser environment and capabilities:
```javascript
import {
isBrowser,
isIOS,
isSafari,
supportsWebAuthn,
getOrigin,
} from "@oviato/connect/utils/browser";
// Check if running in browser (not SSR)
if (isBrowser()) {
console.log("Running in browser");
}
// Detect iOS devices
if (isIOS()) {
console.log("iOS device detected");
}
// Detect Safari browser
if (isSafari()) {
console.log("Safari browser");
}
// Check WebAuthn support
if (supportsWebAuthn()) {
console.log("WebAuthn is supported");
} else {
console.log("WebAuthn not available");
}
// Get current origin
const origin = getOrigin();
console.log("Origin:", origin); // e.g., "https://example.com"
```
### Validation Utilities
Validate URLs, colors, and domains:
```javascript
import {
isValidUrl,
isValidImageUrl,
isValidHexColor,
isValidDomain,
normalizeDomain,
} from "@oviato/connect/utils/validation";
// Validate any URL
isValidUrl("https://example.com"); // true
isValidUrl("not-a-url"); // false
// Validate image URLs
isValidImageUrl("https://example.com/logo.png"); // true
isValidImageUrl("https://example.com/file.pdf"); // false
// Validate hex colors
isValidHexColor("#ff0000"); // true
isValidHexColor("#f00"); // true
isValidHexColor("red"); // false
// Validate domains
isValidDomain("example.com"); // true
isValidDomain("sub.example.com"); // true
// Normalize domain (remove protocol and trailing slash)
normalizeDomain("https://example.com/"); // "example.com"
```
### Storage Utilities
SSR-safe localStorage and sessionStorage helpers:
```javascript
import {
getItem,
setItem,
removeItem,
getJSON,
setJSON,
clear,
} from "@oviato/connect/utils/storage";
// Basic string storage
setItem("myKey", "myValue");
const value = getItem("myKey");
removeItem("myKey");
// JSON storage (auto serialize/deserialize)
setJSON("user", { name: "Alice", id: 123 });
const user = getJSON("user");
console.log(user.name); // "Alice"
// Use session storage instead of local
setItem("tempKey", "tempValue", "session");
const temp = getItem("tempKey", "session");
// Clear all storage
clear(); // Clear localStorage
clear("session"); // Clear sessionStorage
```
### Storage Type Reference
```typescript
type StorageType = "local" | "session";
// All functions accept optional storage type (default: "local")
getItem(key: string, type?: StorageType): string | null;
setItem(key: string, value: string, type?: StorageType): boolean;
removeItem(key: string, type?: StorageType): boolean;
getJSON(key: string, type?: StorageType): T | null;
setJSON(key: string, value: T, type?: StorageType): boolean;
clear(type?: StorageType): boolean;
```
## Complete RPC Example
Here's a comprehensive example using RPC and utilities:
```javascript
import {
initialize,
authenticate,
getSession,
rpc,
resolveUsername,
} from "@oviato/connect";
async function main() {
// Initialize SDK
await initialize({
appId: "your-app-id",
network: "bitcoinmainnet",
});
// Authenticate user
const authResult = await authenticate();
if (authResult.status !== "success") {
console.error("Authentication failed");
return;
}
const session = getSession();
console.log("Logged in as:", session.username);
// Get balance with detailed breakdown
const balanceResult = await rpc.getBalance({
address: session.address,
});
if (balanceResult.result) {
const { balance, chainType } = balanceResult.result;
console.log("\n--- Balance ---");
console.log("Total:", balance.formatted);
if (chainType === "utxo") {
console.log("Confirmed:", balance.confirmed.formatted);
console.log("Pending:", balance.unconfirmed.formatted);
}
}
// Get recent transactions
const historyResult = await rpc.getTransactionHistory({
address: session.address,
limit: 5,
});
if (historyResult.result) {
console.log("\n--- Recent Transactions ---");
historyResult.result.transactions.forEach((tx) => {
const type = tx.type === "received" ? "+" : "-";
const date = new Date(tx.timestamp * 1000).toLocaleDateString();
console.log(`${type}${tx.amount} sats | ${tx.confirmations} conf | ${date}`);
});
}
// Resolve a friend's username
const friendResult = await resolveUsername("bob");
if (friendResult.status === "success") {
console.log("\n--- Bob's Addresses ---");
friendResult.data.addresses.forEach((addr) => {
console.log(`${addr.network}: ${addr.address}`);
});
}
}
main();
```
## Security Considerations
### Read-Only Access
The RPC client is **read-only** by design:
- Cannot sign transactions
- Cannot send funds
- Cannot modify wallet state
All signing operations require WebAuthn authentication through the SDK's wallet methods.
### Token Security
- Auth tokens are automatically included when available
- Tokens expire and require re-authentication
- Tokens are domain-scoped (only work from registered origins)
### Rate Limiting
- Requests are rate-limited per app/token/IP
- Handle `-32002` error codes gracefully
- Implement exponential backoff for retries
```javascript
async function fetchWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.message.includes("-32002") && i < maxRetries - 1) {
// Rate limited - wait before retry
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
continue;
}
throw error;
}
}
}
```
## Next Steps
- **[Wallet Operations](/chain-agnostic/overview)** - Sign messages and send transactions
- **[Authentication](/chain-agnostic/authentication)** - Learn about user authentication
- **[Installation](/getting-started/quickstart)** - SDK setup guide
---
# Section: Changelog
# Changelog
> Combined release history for @oviato/sdk and @oviato/connect.
Canonical URL: https://docs.oviato.com/changelog
File: changelog.mdx
Release history for **`@oviato/sdk`** and **`@oviato/connect`** in a single timeline.
Because `@oviato/connect` re-exports `@oviato/sdk`, most versions ship in both packages together — those entries show once, annotated with both package names. When a release has React-only or SDK-only content, the affected package is labelled explicitly.
Releases are managed via [changesets](https://github.com/changesets/changesets) and published to npm via GitHub Actions. This page regenerates on every docs deploy.
## 0.4.1
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Restore missing type declarations.
---
## 0.4.0
**Packages:** `@oviato/sdk` + `@oviato/connect`
_Introducing EVM wallet support_
**`@oviato/sdk`**
Adds full EVM wallet support on **Ethereum Mainnet, Base, Ethereum Sepolia, and Base Sepolia**. Networks share one `Session` and one passkey; switch via `switchNetwork()` or per-action `network` overrides.
New top-level **`evm.*`** namespace, shaped like viem/ethers so existing EVM devs drop in with familiar idioms. UTXO (Bitcoin) moves under `utxo.*`.
**Reads:** `readContract`, `multicall` (Multicall3), `getTokenBalance`, `getTokenAllowance`, `getNftOwner`, `getNftBalance`, `getNftMetadata` (resolves `data:` / `ipfs://` / `ar://` / `https://` and normalizes image URL), `getTokens` (curated ERC-20 registry per network — USDC, USDT, WETH, WBTC/cbBTC where canonical).
**Writes:** `sendTransaction` (native + raw calldata), `writeContract` (ABI-aware, with decoded confirmation sheets for ERC-20 transfer, approve, ERC-721, ERC-1155, and generic contract calls), `waitForTransactionReceipt`.
**Signing:** `signMessage` (EIP-191), `signTypedData` (EIP-712), plus offchain `verifyMessage` / `verifyTypedData`.
**Payments:** `x402` helpers for gasless USDC payments (ERC-3009 `transferWithAuthorization`).
**Breaking changes (one-line migrations):**
- Flat EVM exports are now namespaced: `import { writeContract }` → `import { evm }` then
`evm.writeContract()`. Applies to every EVM primitive.
- EVM `Transaction` type renamed to `EvmTransaction` to disambiguate from the UTXO `Transaction`.
**`@oviato/connect`**
- Tracks `@oviato/sdk` 0.4.0 — exposes the new `evm.*` and `utxo.*` namespaces through the React provider.
`useOviConnect`, `OviConnectProvider`, `Connect`, and `ConnectButton` surfaces unchanged.
---
## 0.3.1
**Packages:** `@oviato/sdk` + `@oviato/connect`
**`@oviato/sdk`**
- Enhanced bridge experience with themed loading states
- Improved SDK initialization and configuration handling
- Added dark mode theme propagation to popup and iframe
**`@oviato/connect`**
- Added dark mode support with automatic system preference detection
- Added `invertLogo` prop to `` component for dark logo compatibility
- Improved UI styling and component consistency
---
## 0.3.0
**Packages:** `@oviato/sdk` + `@oviato/connect`
_x402 payments, typed-data signing, and Base network_
- Add x402 payment signing, EIP-712 typed data signing, and BASE network support
- x402 payment signing support for HTTP 402 payment flows
- EIP-712 typed data signing (`signTypedData`) for EVM chains
- BASE network support (mainnet and Sepolia testnet)
- Tailwind CSS 4.2.2 upgrade with CSS isolation fix (no more global style conflicts)
- Enter key support on Connect form for better UX
---
## 0.2.1
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Patch for UX improvement on choosing between iframe/popup for localhost
---
## 0.2.0
**Packages:** `@oviato/sdk` + `@oviato/connect`
_Minor changes_
- Add PSBT signing with prepare/decode support, multi-recipient transfers for UTXO chains, SigHash types, fee options, and enhanced RPC methods
---
## 0.1.15
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Added common getBalance functionality for EVM and UTXO chains
---
## 0.1.14
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Improved app configuration caching and reactivity. Config updates now reflect immediately in the UI, and ETag-based caching reduces unnecessary network requests.
---
## 0.1.13
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Bug fixes
---
## 0.1.12
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Optimized build output size by removing debug logs in production
---
## 0.1.11
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Bridge method selection improvements: removed flawed Storage Access API check and redundant Safari/iOS fallback, allowing iframe mode to work correctly on HTTPS sites
---
## 0.1.10
**Packages:** `@oviato/sdk` + `@oviato/connect`
**`@oviato/sdk`**
### Improved
- Bridge method selection logic for better iframe usage on HTTPS sites
**`@oviato/connect`**
- ### Improved
- Bridge method selection logic for better iframe usage on HTTPS sites
---
## 0.1.9
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Fix workspace dependency handling for manual publishing workflow
---
## 0.1.8
**Packages:** `@oviato/sdk` + `@oviato/connect`
- Add hex validation to signPsbt function
- Make signInputs parameter optional (signs all inputs if omitted)
- Update signPsbt documentation with improved examples
- Fix SSR hydration issue in Next.js and other SSR frameworks
---
---