# Client API

> Every function and component the scaffold writes to client/src/bsv/, with its exact signature, return value, parameters and errors.

> Agents: search these docs with the `search_docs` tool on the MCP server at https://createbsvapp.vercel.app/mcp, or read everything at https://createbsvapp.vercel.app/llms-full.txt.

These live in your project, in `client/src/bsv/` (or `src/bsv/` for the `react` starter). They're plain TypeScript you own, so open them, change them, or delete what you don't use. The source of each one is at the end of its section, exactly as create-bsv-app 1.1.2 generates it (with the [build fix](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails) applied).

**Hover any underlined name** in the examples for its real type.

## useWallet()

The connected wallet, the user's identity key, and the connect flow. From [`wallet-connect`](https://createbsvapp.vercel.app/docs/capabilities#wallet-connect).

```ts
import { useWallet } from './bsv/WalletContext'
```

### Usage

```tsx [client/src/Profile.tsx] twoslash
import { useWallet } from './bsv/WalletContext'

export function Profile () {
  const { status, identityKey, connect } = useWallet()
  if (status !== 'connected') return <button onClick={() => void connect()}>Connect wallet</button>
  return <p>Signed in as {identityKey}</p>
}
```

### Returns

An object, re-rendered whenever the connection changes:

| Field | Type | Meaning |
| --- | --- | --- |
| `status` | `ConnectStatus` | `'disconnected'`, `'connecting'`, `'choosing'`, `'pairing'` or `'connected'` |
| `connected` | `boolean` | `wallet !== null` |
| `wallet` | `WalletInterface \| null` | the full [BRC-100](https://createbsvapp.vercel.app/docs/glossary#brc-100) wallet from `@bsv/sdk` |
| `identityKey` | `string \| null` | the user's [identity key](https://createbsvapp.vercel.app/docs/glossary#identity-key), 66 hex characters |
| `connect` | `() => Promise<void>` | try the desktop wallet; on failure, move to `'choosing'` |
| `connectMobile` | `() => Promise<void>` | start QR pairing (`'pairing'`); needs the server's relay |
| `cancel` | `() => void` | back to `'disconnected'` |

### Throws

- `useWallet must be used within WalletProvider` if the component isn't inside [`<WalletProviders>`](#walletproviders).

:::: deep Source: WalletContext.tsx
```tsx [client/src/bsv/WalletContext.tsx]
// App-wide wallet state + connect state machine (desktop-first, relay fallback).
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
import type { WalletInterface } from '@bsv/sdk'
import { connectDesktopWallet } from './walletAcquisition.js'
import { useWalletConnection } from './WalletConnectionContext.js'

export type ConnectStatus = 'disconnected' | 'connecting' | 'choosing' | 'pairing' | 'connected'
interface WalletState {
  wallet: WalletInterface | null
  identityKey: string | null
  connected: boolean
  status: ConnectStatus
  connect: () => Promise<void>          // desktop-first; on failure -> 'choosing'
  connectMobile: () => Promise<void>    // relay QR -> 'pairing'
  cancel: () => void
}
const Ctx = createContext<WalletState | null>(null)

export function WalletProvider ({ children }: { children: ReactNode }) {
  const relay = useWalletConnection()
  const [wallet, setWallet] = useState<WalletInterface | null>(null)
  const [identityKey, setIdentityKey] = useState<string | null>(null)
  const [status, setStatus] = useState<ConnectStatus>('disconnected')

  const connect = useCallback(async () => {
    setStatus('connecting')
    try {
      const { wallet, identityKey } = await connectDesktopWallet()
      setWallet(wallet); setIdentityKey(identityKey); setStatus('connected')
    } catch {
      setStatus('choosing')   // no desktop wallet -> show modal
    }
  }, [])

  const connectMobile = useCallback(async () => {
    setStatus('pairing')
    try {
      await relay.createSession()   // shows QR via relay.session.qrDataUrl
    } catch {
      setStatus('choosing')         // relay unavailable -> back to the choice modal
    }
  }, [relay])

  const cancel = useCallback(() => { relay.cancelSession?.(); setStatus('disconnected') }, [relay])

  // bridge: when the relay session connects, adopt its wallet
  useEffect(() => {
    if (relay.session?.status === 'connected' && relay.wallet != null && wallet == null) {
      const w = relay.wallet as unknown as WalletInterface
      w.getPublicKey({ identityKey: true }).then(({ publicKey }) => {
        setWallet(w); setIdentityKey(publicKey); setStatus('connected')
      }).catch(() => {})
    }
  }, [relay.session?.status, relay.wallet, wallet])

  return <Ctx.Provider value={{ wallet, identityKey, connected: wallet !== null, status, connect, connectMobile, cancel }}>{children}</Ctx.Provider>
}
export function useWallet (): WalletState {
  const v = useContext(Ctx)
  if (v === null) throw new Error('useWallet must be used within WalletProvider')
  return v
}
```
::::

## WalletProviders

Wraps your app so every component can call `useWallet()`. New projects already have it in `main.tsx`; add mode prints the snippet in `AGENTS.md`.

```tsx [client/src/main.tsx] twoslash
// @filename: App.tsx
export default function App () { return null }
// @filename: main.tsx
// ---cut---
import { createRoot } from 'react-dom/client'
import { WalletProviders } from './bsv/WalletProviders'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <WalletProviders>
    <App />
  </WalletProviders>,
)
```

It nests the mobile relay provider (`WalletConnectionProvider`, from `@bsv/wallet-relay`) above the wallet provider, which is the order `useWallet()` needs.

:::: deep Source: WalletProviders.tsx and WalletConnectionContext.tsx
::: code-group
```tsx [WalletProviders.tsx]
// Compose the wallet providers in the required order (relay above wallet).
import './bsv.css'
import type { ReactNode } from 'react'
import { WalletConnectionProvider } from './WalletConnectionContext.js'
import { WalletProvider } from './WalletContext.js'

export function WalletProviders ({ children }: { children: ReactNode }) {
  return (
    <WalletConnectionProvider>
      <WalletProvider>{children}</WalletProvider>
    </WalletConnectionProvider>
  )
}
```
```tsx [WalletConnectionContext.tsx]
// Relay-session context: wraps @bsv/wallet-relay's hook so a single relay client
// (mobile QR / remote wallet) lives above the router. Port/extend from your app as needed.
import { createContext, useContext, type ReactNode } from 'react'
import { useWalletRelayClient } from '@bsv/wallet-relay/react'
import { API_BASE_URL } from './config.js'

type RelayValue = ReturnType<typeof useWalletRelayClient>
const Ctx = createContext<RelayValue | null>(null)

export function WalletConnectionProvider ({ children, apiUrl = API_BASE_URL }: { children: ReactNode, apiUrl?: string }) {
  // apiUrl points at the server running the WalletRelayService (REST /api/session + WS /ws).
  const relay = useWalletRelayClient({ apiUrl, autoCreate: false })
  return <Ctx.Provider value={relay}>{children}</Ctx.Provider>
}

export function useWalletConnection (): RelayValue {
  const v = useContext(Ctx)
  if (v === null) throw new Error('useWalletConnection must be used within WalletConnectionProvider')
  return v
}
```
:::
::::

## ConnectWallet

The ready-made button: **Connect wallet**, then `Connected: 02ab…`, with the *No desktop wallet found* dialog (mobile QR or install link) in between.

```tsx twoslash
import { ConnectWallet } from './bsv/ConnectWallet'

export const Header = () => <header><ConnectWallet /></header>
```

It takes no props. Restyle it in `bsv.css`, or build your own UI on `useWallet()`.

## useWalletLogin()

Passwordless login: sign a `login` proof and post it to the server. From [`wallet-login`](https://createbsvapp.vercel.app/docs/capabilities#wallet-login).

```ts
import { useWalletLogin } from './bsv/useWalletLogin'
```

### Usage

```tsx [client/src/LoginButton.tsx] twoslash
import { useWalletLogin } from './bsv/useWalletLogin'

export function LoginButton () {
  const { login } = useWalletLogin()
  const onClick = async () => {
    const { identityKey } = await login()
    console.log(identityKey)
    // → 02a1f3c9e8b7d6a5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3
  }
  return <button onClick={() => void onClick()}>Log in with wallet</button>
}
```

### Returns

| Field | Type | Meaning |
| --- | --- | --- |
| `login` | `() => Promise<{ identityKey: string }>` | runs the whole exchange and resolves with the verified key |
| `identityKey` | `string \| null` | the connected wallet's key (before login) |
| `connected` | `boolean` | whether a wallet is connected |

### Parameters

One optional object:

#### serverIdentityKey (optional) {#usewalletlogin-serveridentitykey}

- **Type:** `string`

Pin the server's identity instead of fetching it from `GET /api/identity`. See [why you might](https://createbsvapp.vercel.app/docs/security#server-identity-is-trusted-via-your-api-origin).

#### loginEndpoint (optional) {#usewalletlogin-loginendpoint}

- **Type:** `string`
- **Default:** `'/api/login'`

The path to post the proof to. It must be a plain path: [no query strings](https://createbsvapp.vercel.app/docs/errors#api-endpoint-must-be-a-safe-absolute-path).

### Throws

- `connect a wallet first (initializeWallet / relay)` when no wallet is connected.
- `login failed: <status>` when the server rejects the proof (usually `401`).
- `server returned another wallet identity` when the verified key isn't the connected wallet's.
- Anything [`apiFetch`](#apifetch) or [`getServerIdentity`](#getserveridentity) throws.

::: warning On create-bsv-app 1.1.2, apply the import fix
Without the [three-line fix](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails), `login()` throws `ReferenceError: requireIdentityKey is not defined`.
:::

:::: deep Source: useWalletLogin.tsx
```tsx [client/src/bsv/useWalletLogin.tsx]
// Wallet login: prove identity with the connected wallet, then POST the proof.
import { useCallback } from 'react'
import { useWallet } from './WalletContext.js'
import { createAuthProof } from './auth.js'
import { getServerIdentity, readIdentityKeyResponse, requireIdentityKey } from './serverIdentity.js'
import { apiFetch } from './apiClient.js'

// serverIdentityKey is optional: when omitted it's fetched from GET /api/identity.
export interface UseWalletLoginOptions { serverIdentityKey?: string, loginEndpoint?: string }

export function useWalletLogin (opts: UseWalletLoginOptions = {}) {
  const { wallet, identityKey } = useWallet()
  const login = useCallback(async (): Promise<{ identityKey: string }> => {
    if (wallet === null) throw new Error('connect a wallet first (initializeWallet / relay)')
    const counterparty = requireIdentityKey(opts.serverIdentityKey ?? await getServerIdentity())
    const proof = await createAuthProof(wallet, { counterparty, action: 'login' })
    const res = await apiFetch(opts.loginEndpoint ?? '/api/login', {
      method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(proof)
    })
    if (!res.ok) throw new Error('login failed: ' + String(res.status))
    const loggedInIdentity = await readIdentityKeyResponse(res)
    if (identityKey !== null && loggedInIdentity !== identityKey) throw new Error('server returned another wallet identity')
    return { identityKey: loggedInIdentity }
  }, [wallet, opts.serverIdentityKey, opts.loginEndpoint])
  return { login, identityKey, connected: wallet !== null }
}
```
::::

## useSignedRequest()

Authenticate a single API call. From [`signed-requests`](https://createbsvapp.vercel.app/docs/capabilities#signed-requests).

```ts
import { useSignedRequest } from './bsv/useSignedRequest'
```

### Usage

```tsx [client/src/NewNote.tsx] twoslash
import { useSignedRequest } from './bsv/useSignedRequest'

export function NewNote () {
  const { signedFetch, connected } = useSignedRequest()
  const save = async () => {
    const res = await signedFetch('/api/notes', { action: 'create-note', body: { text: 'gm' } })
    console.log(res.status)
    // → 200
  }
  return <button disabled={!connected} onClick={() => void save()}>Save</button>
}
```

### Returns

| Field | Type | Meaning |
| --- | --- | --- |
| `signedFetch` | `(path: string, opts: { action: string, body?: RequestBody }) => Promise<Response>` | signs `{ action, body }` and `POST`s `{ proof, body }` as JSON |
| `connected` | `boolean` | whether a wallet is connected |

### Parameters

#### serverIdentityKey (optional) {#usesignedrequest-serveridentitykey}

- **Type:** `string`

The first argument (not an object). Pins the server's identity instead of fetching it.

#### signedFetch: path {#signedfetch-path}

- **Type:** `string`

A plain API path such as `/api/notes`. Query strings aren't allowed.

#### signedFetch: opts.action {#signedfetch-action}

- **Type:** `string`

Names the operation. The server must verify the **same** action, or the request fails with `401`.

#### signedFetch: opts.body (optional) {#signedfetch-body}

- **Type:** `RequestBody` (a string, binary, or JSON-compatible object or array)

Bound into the signature byte for byte. See [why exact bytes matter](https://createbsvapp.vercel.app/docs/capabilities#how-the-proof-works).

### Throws

- `connect a wallet first` when no wallet is connected.
- Anything [`apiFetch`](#apifetch) or [`getServerIdentity`](#getserveridentity) throws. A rejected proof is **not** thrown: check `res.ok` / `res.status`.

:::: deep Source: useSignedRequest.ts and signedRequest.ts
::: code-group
```ts [useSignedRequest.ts]
// Hook: signedFetch attaches a proof bound to the route + JSON body.
import { useCallback } from 'react'
import { useWallet } from './WalletContext.js'
import { createSignedRequest } from './signedRequest.js'
import { getServerIdentity, requireIdentityKey } from './serverIdentity.js'
import { apiFetch } from './apiClient.js'
import type { RequestBody } from './auth.js'

// serverIdentityKey is optional: when omitted it's fetched from GET /api/identity.
export function useSignedRequest (serverIdentityKey?: string) {
  const { wallet } = useWallet()
  const signedFetch = useCallback(async (url: string, opts: { action: string, body?: RequestBody }): Promise<Response> => {
    if (wallet === null) throw new Error('connect a wallet first')
    const counterparty = requireIdentityKey(serverIdentityKey ?? await getServerIdentity())
    const proof = await createSignedRequest(wallet, { serverIdentityKey: counterparty, action: opts.action, body: opts.body })
    return await apiFetch(url, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ proof, body: opts.body })
    })
  }, [wallet, serverIdentityKey])
  return { signedFetch, connected: wallet !== null }
}
```
```ts [signedRequest.ts]
// Create a signed request: an @bsv/auth proof bound to a route (action) + body.
import type { WalletInterface } from '@bsv/sdk'
import { createAuthProof, type AuthProof, type RequestBody } from './auth.js'

export async function createSignedRequest (
  wallet: WalletInterface,
  opts: { serverIdentityKey: string, action: string, body?: RequestBody }
): Promise<AuthProof> {
  return await createAuthProof(wallet, { counterparty: opts.serverIdentityKey, action: opts.action, body: opts.body })
}
```
:::
::::

## apiFetch()

The one HTTP client every generated call uses. Bounded and redirect-free by design: see the [security model](https://createbsvapp.vercel.app/docs/security#the-api-client-is-deliberately-strict).

```ts
import { apiFetch, readApiJson } from './bsv/apiClient'
```

### Usage

```ts twoslash
import { apiFetch, readApiJson } from './bsv/apiClient'

const res = await apiFetch('/api/identity')
const data = await readApiJson(res)
console.log(data)
// → { identityKey: '03d234c27a69dfff…' }
```

### Returns

`Promise<Response>`: a fresh `Response` whose body has already been read within the limits, so you can call `.json()`, `.text()` or [`readApiJson`](#readapijson) on it.

### Parameters

#### path {#apifetch-path}

- **Type:** `string`

Must match `/^\/[A-Za-z0-9/_-]*$/` and contain no `..`. It's appended to [`API_BASE_URL`](#config).

#### init (optional) {#apifetch-init}

- **Type:** `RequestInit`

Standard fetch options. `body` must be a string (use `JSON.stringify`). `credentials`, `redirect`, `referrerPolicy` and `signal` are always overridden.

### Throws

`API endpoint must be a safe absolute path`, `API request body must be a string`, `API request exceeds the byte limit`, `API response exceeds the byte limit`, `API response has an invalid Content-Length`, `API response length does not match Content-Length`, `API response changed network authority`, plus `AbortError` after 10 seconds. All are listed in [Errors](https://createbsvapp.vercel.app/docs/errors#apiclientts).

## readApiJson()

Parses a response body as strict UTF-8 JSON.

- **Signature:** `readApiJson(response: Response): Promise<unknown>`
- **Throws:** `API response is not valid UTF-8`, `API response is not valid JSON`.

:::: deep Source: apiClient.ts
```ts [client/src/bsv/apiClient.ts]
// One bounded client for every generated API request. It refuses redirects so
// proofs, identities, and future credentials never move to another network authority.
import { API_BASE_URL, API_ORIGIN } from './config.js'

const API_TIMEOUT_MS = 10_000
const MAX_API_REQUEST_BYTES = 1024 * 1024
const MAX_API_RESPONSE_BYTES = 1024 * 1024

function endpointUrl (path: string): string {
  if (!/^\/[A-Za-z0-9/_-]*$/.test(path) || path.includes('..')) {
    throw new TypeError('API endpoint must be a safe absolute path')
  }
  return API_BASE_URL + path
}

function contentLength (response: Response): number | undefined {
  // Fetch exposes decoded response bytes while some implementations retain the
  // encoded Content-Length. The streaming ceiling below remains authoritative.
  if (response.headers.get('content-encoding') !== null) return undefined
  const value = response.headers.get('content-length')
  if (value === null) return undefined
  if (!/^(?:0|[1-9]\d*)$/.test(value)) throw new Error('API response has an invalid Content-Length')
  const length = Number(value)
  if (!Number.isSafeInteger(length)) throw new Error('API response has an invalid Content-Length')
  return length
}

async function readBoundedBody (response: Response): Promise<Uint8Array<ArrayBuffer>> {
  const declared = contentLength(response)
  if (declared !== undefined && declared > MAX_API_RESPONSE_BYTES) {
    throw new Error('API response exceeds the byte limit')
  }
  if (response.body === null) return new Uint8Array()
  const reader = response.body.getReader()
  const chunks: Uint8Array[] = []
  let total = 0
  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      total += value.byteLength
      if (total > MAX_API_RESPONSE_BYTES) {
        await reader.cancel()
        throw new Error('API response exceeds the byte limit')
      }
      chunks.push(value)
    }
  } finally {
    reader.releaseLock()
  }
  if (declared !== undefined && declared !== total) {
    throw new Error('API response length does not match Content-Length')
  }
  const body = new Uint8Array(total)
  let offset = 0
  for (const chunk of chunks) { body.set(chunk, offset); offset += chunk.byteLength }
  return body
}

export async function apiFetch (path: string, init: RequestInit = {}): Promise<Response> {
  if (init.body != null && typeof init.body !== 'string') {
    throw new TypeError('API request body must be a string')
  }
  if (typeof init.body === 'string' && new TextEncoder().encode(init.body).byteLength > MAX_API_REQUEST_BYTES) {
    throw new Error('API request exceeds the byte limit')
  }
  const controller = new AbortController()
  const timer = setTimeout(() => { controller.abort() }, API_TIMEOUT_MS)
  try {
    const response = await fetch(endpointUrl(path), {
      ...init,
      credentials: 'omit',
      redirect: 'error',
      referrerPolicy: 'no-referrer',
      signal: controller.signal
    })
    if (response.redirected || (response.url !== '' && new URL(response.url).origin !== API_ORIGIN)) {
      throw new Error('API response changed network authority')
    }
    const body = await readBoundedBody(response)
    const headers = new Headers(response.headers)
    headers.delete('content-encoding')
    headers.delete('content-length')
    return new Response(body.length === 0 ? null : body, {
      status: response.status,
      statusText: response.statusText,
      headers
    })
  } finally {
    clearTimeout(timer)
  }
}

export async function readApiJson (response: Response): Promise<unknown> {
  const bytes = new Uint8Array(await response.arrayBuffer())
  let text: string
  try {
    text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
  } catch {
    throw new Error('API response is not valid UTF-8')
  }
  try {
    return JSON.parse(text)
  } catch {
    throw new Error('API response is not valid JSON')
  }
}
```
::::

## getServerIdentity()

Fetches and caches the server's identity key, the counterparty of every proof.

```ts twoslash
import { getServerIdentity } from './bsv/serverIdentity'

const serverKey = await getServerIdentity()
// → 03d234c27a69dfff14eb8190cfdc1c980c4b31450f9ba412926c0755bfc0d5472f
```

- **Signature:** `getServerIdentity(endpoint?: string): Promise<string>` (default endpoint `'/api/identity'`)
- **Caching:** the first successful result is kept for the life of the page; concurrent calls share one request.
- **Throws:** `failed to fetch server identity: <status>`, `server returned an invalid identity response`, `server returned an invalid identity key`.

`requireIdentityKey(value)` and `readIdentityKeyResponse(res)` are exported from the same file. They validate a key (compressed `02`/`03` hex, on the curve) and a strict `{ identityKey }` response.

:::: deep Source: serverIdentity.ts
```ts [client/src/bsv/serverIdentity.ts]
// Fetch the configured API's identity public key once and cache it.
// This is endpoint/TLS trust, not independent key authentication. Pass a pinned key to
// the login/signed-request hooks when the application requires identity continuity.
import { PublicKey } from '@bsv/sdk'
import { apiFetch, readApiJson } from './apiClient.js'

let cached: string | null = null
let pending: Promise<string> | null = null

export function requireIdentityKey (value: unknown): string {
  if (typeof value !== 'string' || !/^(?:02|03)[0-9a-f]{64}$/.test(value)) {
    throw new Error('server returned an invalid identity key')
  }
  try {
    if (PublicKey.fromString(value).toString() !== value) throw new Error()
  } catch {
    throw new Error('server returned an invalid identity key')
  }
  return value
}

export async function readIdentityKeyResponse (response: Response): Promise<string> {
  const parsed = await readApiJson(response)
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) ||
      (Object.getPrototypeOf(parsed) !== Object.prototype && Object.getPrototypeOf(parsed) !== null) ||
      Object.getOwnPropertySymbols(parsed).length !== 0) {
    throw new Error('server returned an invalid identity response')
  }
  const descriptors = Object.getOwnPropertyDescriptors(parsed)
  if (Object.keys(descriptors).length !== 1 || !Object.prototype.hasOwnProperty.call(descriptors, 'identityKey') ||
      Object.values(descriptors).some(property => property.get != null || property.set != null)) {
    throw new Error('server returned an invalid identity response')
  }
  return requireIdentityKey(descriptors.identityKey?.value)
}

export async function getServerIdentity (endpoint = '/api/identity'): Promise<string> {
  if (cached !== null) return cached
  pending ??= (async () => {
    const res = await apiFetch(endpoint)
    if (!res.ok) throw new Error('failed to fetch server identity: ' + String(res.status))
    const identityKey = await readIdentityKeyResponse(res)
    cached = identityKey
    return identityKey
  })()
  try {
    return await pending
  } finally {
    pending = null
  }
}
```
::::

## Config {#config}

`client/src/bsv/config.ts` reads the environment once, at import:

| Export | From | Default (dev) |
| --- | --- | --- |
| `API_BASE_URL` | `VITE_API_URL` | `http://localhost:3000` |
| `API_ORIGIN` | derived from `API_BASE_URL` | `http://localhost:3000` |
| `BSV_NETWORK` | `VITE_BSV_NETWORK` | `'test'` |

In production builds it **throws at import** without an HTTPS `VITE_API_URL`. See [Environment](https://createbsvapp.vercel.app/docs/environment).

:::: deep Source: config.ts
```ts [client/src/bsv/config.ts]
// Centralized client configuration. Vite loads VITE_-prefixed vars from client/.env.
// Base URL of the server API. Defaults to the dev server; set VITE_API_URL in production
// (or whenever the client is served from a different origin than the API).
const configuredApiUrl = import.meta.env.VITE_API_URL
if (import.meta.env.PROD && configuredApiUrl == null) {
  throw new Error('VITE_API_URL is required in production')
}
const parsedApiUrl = new URL(configuredApiUrl ?? 'http://localhost:3000')
const localDevelopment = parsedApiUrl.protocol === 'http:' &&
  (parsedApiUrl.hostname === 'localhost' || parsedApiUrl.hostname === '127.0.0.1' || parsedApiUrl.hostname === '[::1]')
if ((parsedApiUrl.protocol !== 'https:' && !localDevelopment) || parsedApiUrl.username !== '' ||
    parsedApiUrl.password !== '' || parsedApiUrl.search !== '' || parsedApiUrl.hash !== '') {
  throw new Error('VITE_API_URL must be credential-free HTTPS (or exact HTTP localhost development)')
}
export const API_BASE_URL = parsedApiUrl.href.replace(/\/$/, '')
export const API_ORIGIN = parsedApiUrl.origin

// The scaffolded network default is concrete and can be overridden per deployment.
const configuredNetwork = import.meta.env.VITE_BSV_NETWORK ?? 'test'
if (configuredNetwork !== 'main' && configuredNetwork !== 'test' && configuredNetwork !== 'ttn') {
  throw new Error('VITE_BSV_NETWORK must be main, test, or ttn')
}
export const BSV_NETWORK = configuredNetwork
```
::::

## createAuthProof()

The primitive under login and signed requests, shared by client and server (`auth.ts`). You rarely call it directly; [`signedFetch`](#usesignedrequest) and [`login`](#usewalletlogin) do. See [Server API → verifyAuthProof](https://createbsvapp.vercel.app/docs/api-server#verifyauthproof) for the other half.

```ts twoslash
import type { WalletInterface } from '@bsv/sdk'
declare const wallet: WalletInterface
declare const serverKey: string
// ---cut---
import { createAuthProof } from './bsv/auth'

const proof = await createAuthProof(wallet, { counterparty: serverKey, action: 'login' })
```

- **Signature:** `createAuthProof(wallet, { counterparty: string, action: string, body?: RequestBody }): Promise<AuthProof>`
- **Returns:** `{ data, signature }`: the signed `{ action, identityKey, expiresAt, nonce }` and the signature bytes. Valid for 2 minutes.
