Keythings Wallet API Reference

Complete, accurate reference for integrating with the Keythings (Keeta) in‑page provider available at window.keeta.

Note: The provider injects only on HTTPS origins or during development at http://localhost:3000 and http://127.0.0.1:3000.

Provider and Events

The provider exposes a request-based API plus convenience helpers and emits standard events:

  • accountsChanged — payload: string[] of account addresses
  • chainChanged — payload: chainId as 0x-prefixed hex string
  • lockChanged — payload: boolean (true when wallet locked)
  • disconnect — no payload

Example:

const provider = window.keeta;
provider.on('accountsChanged', (accounts) => console.log(accounts));
provider.on('chainChanged', (chainId) => console.log(chainId));

Capability Model

APIs are protected by capability tokens: read, sign, transact, network.

  • The built-in helpers automatically obtain and refresh tokens as needed.
  • When calling raw methods via provider.request, pass { capabilityToken } in the last parameter position shown below.

Requesting capabilities explicitly:

await provider.requestCapabilities(['sign', 'transact']);

Public APIs (no approval)

MethodDescription
keeta_getAccountsReturns [] until origin is approved. Wrapper: provider.getAccounts()
keeta_isLockedReturns true/false. Wrapper: provider.isLocked()

Connection API

MethodTriggersDefault capabilities
keeta_requestAccountsConnection approval UIread, transact

Optional: provider.request({ method: 'keeta_requestAccounts', params: [{ capabilities: ['read','sign'] }] })

Protected Read APIs (require prior approval)

MethodCapabilityWrapper
keeta_getNetworkreadprovider.getNetwork()
keeta_getAccountreadprovider.request({ method: 'keeta_getAccount', params: [ { capabilityToken } ] })
keeta_getBalancereadprovider.getBalance(address?)
keeta_getAllBalancesreadprovider.getAllBalances()
keeta_getNormalizedBalancesreadprovider.getNormalizedBalances()
keeta_getAccountStatereadprovider.getAccountState(address)
keeta_getNetworkInforeadprovider.request({ method: 'keeta_getNetworkInfo' })
keeta_getBaseTokenreadprovider.getBaseToken()
keeta_getAccountInfoapprovalprovider.getAccountInfo(address)
keeta_listStorageAccountsByOwnerapprovalprovider.listStorageAccountsByOwner(owner)
keeta_getKtaPriceapprovalprovider.getKtaPrice()

History: The wallet no longer exposes a transaction history RPC. For ledger history, use the Keeta JS SDK (UserClient.history / Client.history) or an indexer service directly with the account and network returned by the wallet.

  • keeta_getAccountInfo and keeta_listStorageAccountsByOwner require approval but no explicit capability token.

Write operations (approval + per‑request confirmation)

MethodCapabilityWrapper
keeta_sendTransactiontransactprovider.sendTransaction(tx)
keeta_signMessagesignprovider.signMessage(message)
keeta_sendtransactprovider.request({ method: 'keeta_send', params: [to, amount, token] })
keeta_receivetransactprovider.request({ method: 'keeta_receive', params: [from, amount, token] })
keeta_createTokentransactprovider.request({ method: 'keeta_createToken', params: [name, symbol, decimals, supply, accessMode] })
keeta_modifyTokenSupplytransactprovider.request({ method: 'keeta_modifyTokenSupply', params: [tokenAddress, amount, operation] })
keeta_modifyTokenBalancetransactprovider.request({ method: 'keeta_modifyTokenBalance', params: [account, amount, tokenAddress] })
keeta_updatePermissionstransactprovider.request({ method: 'keeta_updatePermissions', params: [account, permissions, target] })
keeta_setInfotransactprovider.request({ method: 'keeta_setInfo', params: [info, account] })
keeta_createStorageAccounttransactprovider.request({ method: 'keeta_createStorageAccount', params: [name, description, metadata, permissions] })

keeta_sendTransaction

Wrapper: provider.sendTransaction(tx)

The tx payload is a JSON object with this shape:

type KeetaTransaction = {
  to: string;                // required recipient account on Keeta
  amount: string;            // required amount as a string (decimal or raw, depending on integration)
  from?: string;             // optional explicit sender; usually omitted so the active account is used
  token?: string;            // optional token account; omitted for base token transfers
  data?: string;             // optional external payload (anchors, contract calls, protocol metadata)
  gasLimit?: string;         // optional network fee hint
  nonce?: string;            // optional client nonce
  operations?: unknown[];    // reserved for advanced builder-style operations
}

When data is provided, the wallet:

  • Validates the field with Zod as part of the transaction payload.
  • For direct dApp-initiated sends, passes tx.data into the underlying Keeta SDK builder as the external argument to builder.send(...).
  • This is the recommended way to attach asset-movement anchor payloads and other cross-network routing metadata (for example, bridging from KTA to Base).

Network and Capabilities

MethodCapabilityWrapper
keeta_switchNetworknetworkprovider.switchNetwork('test'
keeta_requestCapabilitiesapprovalprovider.requestCapabilities([...])
keeta_refreshCapabilitiesapprovalprovider.refreshCapabilities([...])

User Client and Builder

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

// Generate a STORAGE or TOKEN identifier
const storage = builder.generateIdentifier('STORAGE');

// Example: send on behalf of a storage account
builder.send(
  'keeta_recipient_address',
  '1000000',
  client.baseToken,
  undefined,
  { account: storage.account }
);

// Compute and publish
await client.publishBuilder(builder);

Supported builder RPCs (raw): keeta_builder_generateIdentifier, keeta_builder_computeBlocks, keeta_builder_publish.

Quick examples

Detect and connect:

const provider = window.keeta;
if (!provider) throw new Error('Keythings Wallet not detected');
const accounts = await provider.requestAccounts();

Get balances:

const [account] = await provider.getAccounts();
const balance = await provider.getBalance(account);
const normalized = await provider.getNormalizedBalances();

Network:

const network = await provider.getNetwork();
await provider.switchNetwork('test');

Get balances:

Error codes

CodeMeaning
4001USER_REJECTED_REQUEST
4100UNAUTHORIZED
4200UNSUPPORTED_METHOD
4290RATE_LIMITED
4900DISCONNECTED
4901CHAIN_DISCONNECTED
5000UNKNOWN_ERROR
5100BALANCE_UNAVAILABLE

@keetanetwork/wallet-core public surface

The Keythings extension and external apps share a common helper library, @keetanetwork/wallet-core, which wraps the official Keeta JS SDK ("keetanet-client"). Core exports:

// packages/wallet-core/src/index.ts
export interface WalletCoreConfig { }
export function createWalletCore(config: WalletCoreConfig = {}): WalletCoreConfig;
export * from './keeta-sdk';

Key pieces from keeta-sdk.ts:

import * as KeetaNet from '@keetanetwork/keetanet-client';
import type { UserClient } from '@keetanetwork/keetanet-client';

// Permission helpers
export type PermissionsAcceptedInput = Parameters<(typeof KeetaNet.lib.Permissions)['FromAcceptedTypes']>[0];
export type PermissionFlagNames = Extract<
  Parameters<(typeof KeetaNet.lib.Permissions)['BaseSet']['Create']>[0],
  string[]
>;
export type PermissionsInput =
  | InstanceType<typeof KeetaNet.lib.Permissions>
  | PermissionsAcceptedInput
  | PermissionFlagNames
  | null
  | undefined;

export function permissionsFromAcceptedTypes(value?: PermissionsInput):
  InstanceType<typeof KeetaNet.lib.Permissions> | undefined;

export function ensurePermissions(
  value?: PermissionsInput,
  fallback?: PermissionsAcceptedInput,
): InstanceType<typeof KeetaNet.lib.Permissions>;

export async function fetchAccountDefaultPermissions(
  client: UserClient,
  account: InstanceType<typeof KeetaNet.lib.Account> | string,
): Promise<InstanceType<typeof KeetaNet.lib.Permissions>>;

export function permissionFlags(permission?:
  InstanceType<typeof KeetaNet.lib.Permissions> | null
): string[];

// Seed and account helpers
export const seedBytesToString: (bytes: Uint8Array) => string;
export const hexStringToBytes: (hex: string) => Uint8Array;
export async function accountFromSeed(
  seed: string | Uint8Array | ArrayBuffer,
  index?: number,
): Promise<KeetaNet.lib.Account.AccountInstance>;
export async function seedToBytes(seed: string): Promise<Uint8Array>;
export const deriveAccountPublicKeys: (seedBytes: Uint8Array, count?: number, startIndex?: number) => string[];

// History helpers
export interface HistoryRecord extends Record<string, unknown> {
  id?: string;
  block?: string;
  to?: unknown;
  from?: unknown;
  amount?: string | bigint | number;
  token?: unknown;
  tokenAddress?: unknown;
  timestamp?: number;
  operationType?: string;
  type?: unknown;
}
export interface HistoryPageOptions { cursor?: string | null; depth?: number; }
export interface HistoryPageResult { records: HistoryRecord[]; cursor: string | null; hasMore: boolean; }
export const normaliseBalances: (payload: unknown) => HistoryRecord[];
export async function fetchHistoryPage(
  client: UserClient,
  options?: HistoryPageOptions,
): Promise<HistoryPageResult>;

// User client bootstrap
export type KeetaSdkNetwork = 'test' | 'main' | 'staging' | 'dev';
export interface UserClientBootstrapResult {
  client: UserClient;
  account: KeetaNet.lib.Account.AccountInstance;
  accountAddress: string;
  network: KeetaSdkNetwork;
}
export interface CreateUserClientOptions {
  accountIndex?: number;
  expectedAccountAddress?: string;
}
export async function createUserClientForSeed(
  network: KeetaSdkNetwork,
  seed: string | Uint8Array | ArrayBuffer,
  options?: CreateUserClientOptions,
): Promise<UserClientBootstrapResult>;

export async function destroyUserClient(client: UserClient | null | undefined): Promise<void>;
export async function recoverFromVotingConflict(client: UserClient): Promise<void>;

export async function quoteBuilderFee(
  client: UserClient,
  builder: ReturnType<UserClient['initBuilder']>,
): Promise<bigint>;

Typical usage from a dApp or backend service:

import {
  createUserClientForSeed,
  fetchHistoryPage,
  ensurePermissions,
  permissionFlags,
} from '@keetanetwork/wallet-core';

const { client, account } = await createUserClientForSeed('test', seedHex);
const history = await fetchHistoryPage(client, { depth: 50 });
const perms = ensurePermissions(['ACCESS']);
console.log(permissionFlags(perms));

Messaging contracts (inpage ⇄ content script ⇄ background)

The bridge between window.keeta and the background service worker is strictly typed via message contracts in src/lib/wallet-provider.ts and src/content-scripts/inject-provider.ts.

Inpage ⇄ content script

// src/lib/wallet-provider.ts
export const KEYTHINGS_INPAGE_BRIDGE_TARGET = 'keythings:inpage-provider' as const;
export const KEYTHINGS_CONTENT_BRIDGE_TARGET = 'keythings:content-script' as const;

export type InpageToContentMessage =
  | {
      target: typeof KEYTHINGS_CONTENT_BRIDGE_TARGET;
      type: 'init';
      id?: number;
    }
  | {
      target: typeof KEYTHINGS_CONTENT_BRIDGE_TARGET;
      type: 'request';
      id: number;
      method: string;
      params?: unknown[];
    };

export type ContentToInpageMessage =
  | {
      target: typeof KEYTHINGS_INPAGE_BRIDGE_TARGET;
      type: 'state';
      state: KeetaProviderState;
    }
  | {
      target: typeof KEYTHINGS_INPAGE_BRIDGE_TARGET;
      type: 'response';
      id: number;
      result?: unknown;
      error?: SerializedKeetaError;
    }
  | {
      target: typeof KEYTHINGS_INPAGE_BRIDGE_TARGET;
      type: 'event';
      event: 'accountsChanged' | 'chainChanged' | 'disconnect' | 'lockChanged';
      payload?: unknown;
    };

Example Zod schemas (for dApps that want to validate messages):

import { z } from 'zod';

const InpageToContentMessageSchema = z.discriminatedUnion('type', [
  z.object({
    target: z.literal('keythings:content-script'),
    type: z.literal('init'),
    id: z.number().int().optional(),
  }),
  z.object({
    target: z.literal('keythings:content-script'),
    type: z.literal('request'),
    id: z.number().int(),
    method: z.string().min(1),
    params: z.array(z.unknown()).optional(),
  }),
]);

const ContentToInpageMessageSchema = z.discriminatedUnion('type', [
  z.object({
    target: z.literal('keythings:inpage-provider'),
    type: z.literal('state'),
    state: z.object({
      isConnected: z.boolean(),
      isUnlocked: z.boolean(),
      isLocked: z.boolean(),
      chainId: z.string(),
      selectedAddress: z.string().nullable(),
      network: z.enum(['test', 'main']),
    }),
  }),
  z.object({
    target: z.literal('keythings:inpage-provider'),
    type: z.literal('response'),
    id: z.number().int(),
    result: z.unknown().optional(),
    error: z
      .object({ code: z.number().int(), message: z.string(), data: z.unknown().optional() })
      .optional(),
  }),
  z.object({
    target: z.literal('keythings:inpage-provider'),
    type: z.literal('event'),
    event: z.enum(['accountsChanged', 'chainChanged', 'disconnect', 'lockChanged']),
    payload: z.unknown().optional(),
  }),
]);

Content script ⇄ background service worker

Content scripts forward trusted messages to the background using chrome.runtime.sendMessage wrapped by sendRuntimeMessage in src/lib/runtime-messaging.ts. Two key request shapes:

// src/content-scripts/inject-provider.ts (simplified)
type BackgroundResponse = { id: number; result?: unknown; error?: SerializedKeetaError };

// Site status
sendRuntimeMessage<BackgroundResponse | undefined>({
  source: KEYTHINGS_CONTENT_BRIDGE_TARGET,
  type: 'keeta:get-site-status',
  origin: window.location.origin,
  pageUrl: window.location.href,
});

// RPC request
sendRuntimeMessage<BackgroundResponse | undefined>({
  source: KEYTHINGS_CONTENT_BRIDGE_TARGET,
  type: 'keeta:rpc-request',
  id,
  method,
  params,
  origin: window.location.origin,
  pageUrl: window.location.href,
});

On the background side, wallet-provider-handler.ts validates origin and type before dispatching:

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  // Session messages (keeta:session:*) are handled elsewhere
  if (isRpcRequestMessage(message)) {
    await this.requireTrustedContentSender(sender, message);
    const origin = this.extractSenderOrigin(sender, message.origin);
    const pageUrl = this.extractSenderPageUrl(sender, message.pageUrl);
    this.ensureOriginIsAllowlisted(origin);
    const response = await this.handleRpcRequest(message, origin, pageUrl);
    sendResponse(safeSerialize(response, 'RPC response serialization'));
    return true;
  }
});

Auditors can mirror the extension's behaviour by validating outbound/inbound messages with Zod before passing them into business logic, matching the schemas above.