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)
| Method | Description |
|---|---|
| keeta_getAccounts | Returns [] until origin is approved. Wrapper: provider.getAccounts() |
| keeta_isLocked | Returns true/false. Wrapper: provider.isLocked() |
Connection API
| Method | Triggers | Default capabilities |
|---|---|---|
| keeta_requestAccounts | Connection approval UI | read, transact |
Optional: provider.request({ method: 'keeta_requestAccounts', params: [{ capabilities: ['read','sign'] }] })
Protected Read APIs (require prior approval)
| Method | Capability | Wrapper |
|---|---|---|
| keeta_getNetwork | read | provider.getNetwork() |
| keeta_getAccount | read | provider.request({ method: 'keeta_getAccount', params: [ { capabilityToken } ] }) |
| keeta_getBalance | read | provider.getBalance(address?) |
| keeta_getAllBalances | read | provider.getAllBalances() |
| keeta_getNormalizedBalances | read | provider.getNormalizedBalances() |
| keeta_getAccountState | read | provider.getAccountState(address) |
| keeta_getNetworkInfo | read | provider.request({ method: 'keeta_getNetworkInfo' }) |
| keeta_getBaseToken | read | provider.getBaseToken() |
| keeta_getAccountInfo | approval | provider.getAccountInfo(address) |
| keeta_listStorageAccountsByOwner | approval | provider.listStorageAccountsByOwner(owner) |
| keeta_getKtaPrice | approval | provider.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_getAccountInfoandkeeta_listStorageAccountsByOwnerrequire approval but no explicit capability token.
Write operations (approval + per‑request confirmation)
| Method | Capability | Wrapper |
|---|---|---|
| keeta_sendTransaction | transact | provider.sendTransaction(tx) |
| keeta_signMessage | sign | provider.signMessage(message) |
| keeta_send | transact | provider.request({ method: 'keeta_send', params: [to, amount, token] }) |
| keeta_receive | transact | provider.request({ method: 'keeta_receive', params: [from, amount, token] }) |
| keeta_createToken | transact | provider.request({ method: 'keeta_createToken', params: [name, symbol, decimals, supply, accessMode] }) |
| keeta_modifyTokenSupply | transact | provider.request({ method: 'keeta_modifyTokenSupply', params: [tokenAddress, amount, operation] }) |
| keeta_modifyTokenBalance | transact | provider.request({ method: 'keeta_modifyTokenBalance', params: [account, amount, tokenAddress] }) |
| keeta_updatePermissions | transact | provider.request({ method: 'keeta_updatePermissions', params: [account, permissions, target] }) |
| keeta_setInfo | transact | provider.request({ method: 'keeta_setInfo', params: [info, account] }) |
| keeta_createStorageAccount | transact | provider.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.datainto the underlying Keeta SDK builder as theexternalargument tobuilder.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
| Method | Capability | Wrapper |
|---|---|---|
| keeta_switchNetwork | network | provider.switchNetwork('test' |
| keeta_requestCapabilities | approval | provider.requestCapabilities([...]) |
| keeta_refreshCapabilities | approval | provider.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
| Code | Meaning |
|---|---|
| 4001 | USER_REJECTED_REQUEST |
| 4100 | UNAUTHORIZED |
| 4200 | UNSUPPORTED_METHOD |
| 4290 | RATE_LIMITED |
| 4900 | DISCONNECTED |
| 4901 | CHAIN_DISCONNECTED |
| 5000 | UNKNOWN_ERROR |
| 5100 | BALANCE_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.