Kache logoKache

Kache SDK

@kache/sdk is a tiny, dependency-free TypeScript library for integrating the Kache wallet into a web app. It covers both surfaces the extension exposes: Kaspa L1 via window.kache, and the Kasplex / Igra EVM L2s via a standard EIP-1193 window.ethereum. It handles the injection race for you, so you can call connect() on page load without waiting.

If you'd rather talk to the raw providers directly, see How to Integrate.

Install

npm install @kache/sdk

Quick start

import { connect, getAccount, getBalance, signMessage, sendKaspa, kasToSompi } from '@kache/sdk';

// Opens the Kache approval popup; resolves once the user approves.
const { address } = await connect();

const account = await getAccount();   // 'kaspa:…' (or null if not connected)
const balance = await getBalance();   // sompi as a string

const sig = await signMessage('Login to Acme #42');

const { txid } = await sendKaspa(address, kasToSompi('1.5')); // 1.5 KAS

The top-level helpers wait for the extension to inject (they listen for the kache#initialized event), so calling connect() immediately on page load won't race the injection.

Detect and wait explicitly

For finer control — showing an "install Kache" prompt, or a custom timeout — use isInstalled() and getProvider():

import { isInstalled, getProvider } from '@kache/sdk';

if (!isInstalled()) {
  // Prompt the user to install Kache.
}

const kache = await getProvider(5000); // wait up to 5s for injection, else throws
await kache.connect();

Grab the provider once with getProvider() when you need to subscribe to events from the very first tick, or unsubscribe synchronously — the top-level on/off helpers await the provider, so an event fired in that gap could be missed.

API

FunctionDescription
connect()Request connection. Opens an approval popup. → { address }
disconnect()Forget this site's connection.
getAccount()Active kaspa: address if connected, else null. No popup.
getBalance()Confirmed balance in sompi (string). No popup.
signMessage(msg)Sign a message (KIP-0005). Opens an approval popup. → hex
sendKaspa(to, amountSompi)Request a KAS transfer. Opens an approval popup. → { txid }
getProvider(timeoutMs?)Resolve the raw window.kache provider once injected (default 3000ms).
isInstalled()true if Kache injected its provider.
kasToSompi(kas) / sompiToKas(sompi)Unit helpers (1 KAS = 100,000,000 sompi).

Every function is exported both as a top-level helper and, for the wallet methods, on the object returned by getProvider().

Units

Amounts are always in sompi (bigint or string), never floating-point KAS. Convert at the edges:

import { kasToSompi, sompiToKas, SOMPI_PER_KAS } from '@kache/sdk';

kasToSompi('1.5');          // 150000000n
sompiToKas('150000000');    // '1.5'
SOMPI_PER_KAS;              // 100000000n

kasToSompi throws on more than 8 decimal places — it never silently truncates a send amount. Prefer passing a string for user-entered amounts; a JS number can't represent every 8-decimal value exactly.

EVM L2 (Kasplex / Igra)

On the Kaspa L2s, Kache injects a standard EIP-1193 window.ethereum provider and announces it via EIP-6963. The SDK's EVM helpers discover Kache specifically (by its com.kache.wallet rdns), so they never return another wallet's provider.

import { connectEvm, getChainId, switchChain, IGRA_MAINNET } from '@kache/sdk';

const [account] = await connectEvm();   // eth_requestAccounts → 0x… address
const chainId = await getChainId();     // e.g. 202555 (Kasplex)
await switchChain(IGRA_MAINNET.id);     // hop to Igra

You can also hand the raw provider to any EIP-1193 library (viem, wagmi, ethers):

import { getEthereumProvider } from '@kache/sdk';
import { createWalletClient, custom } from 'viem';

const provider = await getEthereumProvider();
const client = createWalletClient({ transport: custom(provider) });
FunctionDescription
connectEvm()Request L2 connection. Opens a popup. → string[] of 0x accounts
getEvmAccounts()Authorized 0x accounts (empty if not connected). No popup.
getChainId()Active L2 chain id as a decimal number.
switchChain(id)Switch the active L2 (Kasplex ↔ Igra).
personalSign(msg, addr)personal_sign with the active account. Opens a popup.
sendEvmTransaction(tx)eth_sendTransaction. Opens a popup. → tx hash
getEthereumProvider(ms?)Kache's raw EIP-1193 provider once injected.
isEvmInstalled()true if Kache injected its EVM provider.
getChain(id)Look up a supported chain by decimal or hex id.

The chain constants KASPLEX_MAINNET, IGRA_MAINNET, KASPLEX_TESTNET, and IGRA_TESTNET (plus KACHE_EVM_CHAINS) carry each network's id, hex id, RPC URL, explorer, and native currency — so you never hardcode them.

Subscribe to EVM events (accountsChanged, chainChanged, disconnect) on the provider from getEthereumProvider(). accountsChanged carries a string[] (empty when disconnected) — unlike the L1 accountsChanged below, which carries a single address or null.

Events (Kaspa L1)

Subscribe through the provider so you never miss the first event:

import { getProvider } from '@kache/sdk';

const kache = await getProvider();
kache.on('accountsChanged', () => { /* re-read getAccount() */ });
kache.on('disconnect', () => { /* user disconnected the site */ });

accountsChanged fires with the new address, or null when the newly-selected wallet hasn't approved your site — treat null as disconnected until it reconnects.

Authentication & security

The SDK is a thin client over the injected provider and never sees your keys — you only ever get an address, a signature, or a txid, and every signature or transfer requires explicit user approval in Kache.

An address from connect()/getAccount() is not proof of identity. For login, always verify a signMessage signature server-side against the claimed address — don't trust the address alone. Likewise, confirm a txid on-chain before crediting anything.

When the real extension is installed it defines window.kache as non-writable at document_start, so it can't be overridden by the page — but a look-alike could exist if Kache isn't installed, which is why server-side signature verification matters.