Kache logoKache

How to Integrate

This is the developer reference for connecting a dApp to Kache. If you just want to use dApps as an end user, see Connecting to dApps instead.

Kache injects two providers into every page:

ProviderGlobalNetworksStandard
Kaspa L1window.kacheKaspa mainnet / testnetKache native API
EVM L2window.ethereumKasplex, IgraEIP-1193 + EIP-6963

Both run in the page's MAIN world and relay every request to the wallet over an isolated bridge. No keys, seed phrase, or secrets are ever exposed to the page — the provider only sees what the user explicitly approves.

Every method that reads private state or moves funds opens a Kache approval popup. A site can never sign or send without an explicit user confirmation.

For the Kaspa L1 side, the @kache/sdk npm package wraps window.kache with types, injection handling, and unit helpers — reach for it unless you specifically need the raw provider.


Detecting Kache

window.kache is injected at document_start, and dispatches a kache#initialized event on window once it is ready. Guard for the case where your code runs first:

function getKache() {
  return new Promise((resolve) => {
    if (window.kache) return resolve(window.kache);
    window.addEventListener('kache#initialized', () => resolve(window.kache), { once: true });
    setTimeout(() => resolve(window.kache ?? null), 3000); // not installed
  });
}

const kache = await getKache();
if (!kache) {
  // Prompt the user to install Kache.
}

You can also feature-detect synchronously with if (window.kache?.isKache).


Kaspa L1 — window.kache

The L1 provider is a small, promise-based API. Every method returns a Promise.

connect()

Requests permission to connect. Opens an approval popup the first time an origin connects. Resolves with the active account.

const { address } = await kache.connect();
// address → "kaspa:qq..."

getAccount()

Returns the active address if the site is connected and the wallet is unlocked, otherwise null. Does not prompt.

const address = await kache.getAccount(); // string | null

getBalance()

Confirmed balance of the active account, in sompi (1 KAS = 100,000,000 sompi), returned as a string.

const sompi = await kache.getBalance();      // "12345678900"
const kas = Number(sompi) / 1e8;             // 123.456789

signMessage(message)

Signs an arbitrary string per KIP-0005. Opens an approval popup. Resolves with the signature.

const signature = await kache.signMessage('Sign in to Example @ ' + Date.now());

sendKaspa(to, amountSompi)

Builds, signs, and broadcasts a KAS transfer in one call. Opens an approval popup. amountSompi accepts a string or bigint. Resolves with the transaction id.

const { txid } = await kache.sendKaspa('kaspa:qr...', 150_000_000n); // 1.5 KAS

disconnect()

Drops the site's connection and emits a disconnect event.

await kache.disconnect();

Events

Subscribe with kache.on(event, handler) and remove with kache.off(event, handler).

EventPayloadFires when
accountsChangedstring | nullThe active account changes. null means the newly-selected wallet has not approved this site — treat it as disconnected until it reconnects.
disconnectThe site was disconnected (by the user, or a wallet reset).
kache.on('accountsChanged', (address) => {
  if (!address) return handleDisconnected();
  refreshFor(address);
});

Watch-only wallets cannot sign, so signMessage and sendKaspa will reject when a watch-only account is active.


EVM L2 — window.ethereum

On the Kaspa L2s, Kache exposes a standard EIP-1193 provider, so libraries like wagmi, viem, ethers, RainbowKit, and Reown/WalletConnect work out of the box. Kache is also announced via EIP-6963 so modern wallet selectors discover it automatically:

window.addEventListener('eip6963:announceProvider', (e) => {
  // e.detail.info → { name: "Kache", rdns: "com.kache.wallet", ... }
});
window.dispatchEvent(new Event('eip6963:requestProvider'));

Supported networks

NetworkChain IDHexNativeRPC
Kasplex2025550x31777wKAShttps://evmrpc.kasplex.org
Igra388330x97b1iKAShttps://rpc.igralabs.com:8545

Both L2s are built into Kache — you do not need wallet_addEthereumChain. Use wallet_switchEthereumChain to move between them.

Connecting

const [address] = await window.ethereum.request({ method: 'eth_requestAccounts' });

Supported methods

request({ method, params }) supports the standard EIP-1193 surface:

  • Accounts: eth_requestAccounts, eth_accounts
  • Network: eth_chainId, net_version, wallet_switchEthereumChain
  • Signing: personal_sign, eth_sign, eth_signTypedData_v4 (and _v3)
  • Transactions: eth_sendTransaction
  • Permissions: wallet_requestPermissions, wallet_getPermissions
  • Assets: wallet_watchAsset (accepted as a no-op)

Read-only RPC calls (eth_call, eth_getBalance, eth_estimateGas, …) are served by the active L2's public RPC.

Events

Standard EIP-1193 events via window.ethereum.on(...):

EventPayload
accountsChangedstring[] (empty when disconnected)
chainChangedchain id hex string, e.g. "0x31777"
disconnect

Using with viem / wagmi

import { createWalletClient, custom } from 'viem';

const client = createWalletClient({ transport: custom(window.ethereum) });
const [account] = await client.requestAddresses();

Kache sets isMetaMask = true on the EVM provider because many dApps filter their injected-wallet list by that flag. Prefer EIP-6963 discovery (rdns: "com.kache.wallet") to target Kache specifically.


Security & UX guidance

  • Always show what you're asking for. Kache displays the message or transaction, but a clear in-app summary reduces user drop-off.
  • Handle rejection. Approval prompts can be denied — EVM rejections surface as error code 4001. Never assume a request succeeds.
  • Re-check on accountsChanged. Users can switch wallets mid-session; a null (L1) or empty array (EVM) means you've been disconnected.
  • Never ask for the recovery phrase. No Kache method exposes it, and any site that requests it is a scam.

Have an integration question or need a method we don't expose yet? Reach us at the channels listed on kachewallet.com.