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:
| Provider | Global | Networks | Standard |
|---|---|---|---|
| Kaspa L1 | window.kache | Kaspa mainnet / testnet | Kache native API |
| EVM L2 | window.ethereum | Kasplex, Igra | EIP-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).
| Event | Payload | Fires when |
|---|---|---|
accountsChanged | string | null | The active account changes. null means the newly-selected wallet has not approved this site — treat it as disconnected until it reconnects. |
disconnect | — | The 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
| Network | Chain ID | Hex | Native | RPC |
|---|---|---|---|---|
| Kasplex | 202555 | 0x31777 | wKAS | https://evmrpc.kasplex.org |
| Igra | 38833 | 0x97b1 | iKAS | https://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(...):
| Event | Payload |
|---|---|
accountsChanged | string[] (empty when disconnected) |
chainChanged | chain 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; anull(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.