Developers

Keythings Integration Patterns

Copy-paste ready recipes taken from the production wallet and dApp. Each pattern includes capability guidance and recommended error handling so teams follow the supported RPCs and flows.

Detect the Wallet Provider

Always feature-detect window.keeta before calling APIs. Fallback to actionable messaging when the wallet is missing or locked.

import type { KeetaProvider } from 'keythings-extension-wallet/types'

declare global {
  interface Window {
    keeta?: KeetaProvider
  }
}

export function getKeetaProvider(): KeetaProvider {
  const provider = window.keeta
  if (!provider?.isKeeta) {
    throw new Error('Keythings Wallet provider not detected. Ask the user to install or enable the extension.')
  }
  return provider
}

Request Accounts & Capabilities

Prompt the user once, then cache capability tokens server-side or in app state to avoid unnecessary prompts.

const provider = getKeetaProvider()

// requestAccounts triggers the approval UI
const accounts = await provider.requestAccounts()
if (accounts.length === 0) {
  throw new Error('User rejected the connection request or no accounts available.')
}

// For read-only dashboards, requestCapabilities(['read']) instead of default read+transact
await provider.requestCapabilities(['read'])

provider.on?.('accountsChanged', (next) => {
  console.info('Accounts changed', next)
})
provider.on?.('chainChanged', (chainId) => {
  console.info('Network switched', chainId)
})
provider.on?.('disconnect', () => {
  console.warn('Wallet disconnected — clear session state and prompt to reconnect.')
})

Send Tokens via Builder (Recommended)

Always build sends via userClient.initBuilder() so the wallet runs capability-aware approval instead of calling keeta_sendTransaction directly. For anchors and cross-chain bridging, use this pattern with an external data payload (see the Anchors pattern below).

const provider = getKeetaProvider()
await provider.requestCapabilities(['read', 'transact'])

const client = await provider.getUserClient()
const builder = client.initBuilder()

builder.send(
  { publicKeyString: recipientAddress },
  amountInBaseUnits,
  { publicKeyString: tokenAddress }
)

if (typeof builder.computeBlocks === 'function') {
  await builder.computeBlocks()
}

const receipt = await client.publishBuilder(builder)
console.log('Submitted hash', receipt?.blocks?.[0]?.hash ?? receipt)

Send from Storage Account (SEND_ON_BEHALF)

When moving funds out of a storage account, set the signing context using builder.send(..., { account: storageAccountRef }).

const provider = getKeetaProvider()
await provider.requestCapabilities(['read', 'transact'])

const client = await provider.getUserClient()
const builder = client.initBuilder()

builder.send(
  destinationAddress,
  withdrawalAmount,
  tokenAccountRef,
  undefined,
  { account: storageAccountRef }
)

const receipt = await client.publishBuilder(builder)
console.log('Storage withdrawal submitted', receipt)

Anchors & external data (Base ↔ KTA)

When interacting with asset-movement anchors (for example, bridging from KTA to Base), pass your anchor payload as the external data argument to builder.send. The wallet forwards this into the Keeta SDK's external field so the anchor can route funds to the correct destination network.

import type { KeetaProvider } from 'keythings-extension-wallet/types'

declare global {
  interface Window {
    keeta?: KeetaProvider
  }
}

function getKeetaProvider(): KeetaProvider {
  const provider = window.keeta
  if (!provider?.isKeeta) {
    throw new Error('Keythings Wallet provider not detected. Ask the user to install or enable the extension.')
  }
  return provider
}

type AnchorParams = {
  anchorAddress: string
  amount: bigint
  tokenAddress?: string
  // This should be produced by your anchor integration (for example, an asset movement anchor payload)
  externalData: string
}

export async function sendViaAnchor(params: AnchorParams) {
  const provider = getKeetaProvider()

  // Make sure the dApp has transact capability before building transactions
  await provider.requestCapabilities(['read', 'transact'])

  const client = await provider.getUserClient()
  const builder = client.initBuilder()

  // Route the transfer through the anchor account on Keeta
  builder.send(
    { publicKeyString: params.anchorAddress },
    params.amount,
    params.tokenAddress
      ? { publicKeyString: params.tokenAddress }
      : client.baseToken,
    params.externalData,
  )

  // Optional but recommended: compute blocks before publishing for better UX
  if (typeof builder.computeBlocks === 'function') {
    await builder.computeBlocks()
  }

  const receipt = await client.publishBuilder(builder)
  console.log('Anchor transaction submitted', receipt)
  return receipt
}

Refresh Capability Tokens

Tokens expire. Refresh in the background and surface UI when permissions lapse.

const provider = getKeetaProvider()

try {
  await provider.refreshCapabilities(['read', 'transact'])
} catch (error) {
  console.warn('Capability refresh failed', error)
  await provider.requestCapabilities(['read', 'transact'])
}

Handle Wallet Errors

Map KETA_ERROR_CODES into actionable UI states.

import { KETA_ERROR_CODES } from 'keythings-extension-wallet/lib/wallet-provider'

function toUserMessage(error: unknown): string {
  if (!error || typeof error !== 'object') return 'Unknown wallet error'
  const { code, message } = error as { code?: number; message?: string }

  switch (code) {
    case KETA_ERROR_CODES.USER_REJECTED_REQUEST:
      return 'You rejected the request in Keythings Wallet.'
    case KETA_ERROR_CODES.UNAUTHORIZED:
      return 'Keythings Wallet requires new permissions for this action.'
    case KETA_ERROR_CODES.DISCONNECTED:
      return 'Wallet disconnected. Please reconnect your wallet.'
    default:
      return message ?? 'Unknown wallet error'
  }
}