sdkVersion 168
What people can do with the KCC20 SDK
This is the dApp client. It does not hold keys. It opens the hosted PWA at https://kcc-20-wallet.vercel.app, talks over postMessage ns:'kcc20', and gives your app a KasWare-shaped API: window.kcc20.
| Your app can | How |
|---|---|
| Connect a user’s Kaspa address | connect() / requestAccounts() — popup, user Approves |
| Show which account is connected | getAccounts(), kcc.accounts |
| Know mainnet vs TN10 | getNetwork(), switchNetwork() |
| Build a transaction | getPublicKey() + getUtxoEntries() (silent after Connect) |
| Show KAS / KCC20 bags | getBalance(), getHoldings(), getTokenBalance('KKDAG') |
| Let the user sign your PSKT | signPskt({ txJsonString, options }) — you build, they sign |
| Compile a vault (Argent) | compileVault({ type, params }) — PWA compiles P2SH, user funds. See Argent |
| Send native KAS | sendKas({ dest, amount }) — plain transfer, not a vault |
| Broadcast the signed tx | pushTx(signed) or your own node |
| Buy any KRON / KCC20 token with KAS | buyKron({ tick, amount }) — amount is KAS. Wallet builds Home TRADE. Live list: Tokens |
| Send a KCC20 token (TTT Fund) | sendToken({ tick, amount, dest }) — bag they already hold, not a buy |
| Listen for account / network changes | on('accountsChanged'|'networkChanged'|'disconnect') |
| List next to KasWare in a picker | KIP-12 kaspa:provider rdns: app.kcc20.wallet |
| Disconnect | disconnect() — popup closes, stay on your tab |
Use the search bar or tap a keyword to jump to that method. Or open Try it / the panel below — Connect is a real PWA popup.
Try it in this page
Anyone can test SCORPION here. Tap Connect (allow popups). The wallet window closes after Approve; then tap the silent reads. Sign / Send open the PWA again. This site never sees your PIN or key.
Loading tester…
Install
script src="https://kcc-20-wallet.vercel.app/sdk.js?v=167"
Creates window.kcc20 and window.kcc20wallet. Fires kcc20#initialized. Require kcc.sdkVersion === "167" for buyKron.
const s = document.createElement('script');
s.src = 'https://kcc-20-wallet.vercel.app/sdk.js?v=167';
document.head.appendChild(s);
window.addEventListener('kcc20#initialized', (e) => {
const kcc = e.detail; // same as window.kcc20
});
Optional override before load: window.KCC20_WALLET_ORIGIN = 'https://kcc-20-wallet.vercel.app'.
Only call Connect from a user click. Never on page load. Never on every app switch. Browsers block popups without a gesture; Replit overlays if you inject the script on mount.
Quick start
const kcc = window.kcc20;
const accounts = await kcc.connect(); // POPUP, then closes
const address = accounts[0];
const network = await kcc.getNetwork(); // silent
const pubKey = await kcc.getPublicKey(); // silent
const utxos = await kcc.getUtxoEntries(address);
// you BUILD unsignedSafeJson here
const signed = await kcc.signPskt({
txJsonString: unsignedSafeJson,
options: { signInputs: userP2pkIndexes.map(i => ({ index: i, sighashType: 1 })) }
});
const { txId } = await kcc.pushTx(signed);
Popup vs silent popup silent
After a successful Connect the wallet window closes on purpose. Reads keep working from the session + public Kaspa APIs. The window comes back only for user approval.
| Opens PWA | Silent |
|---|---|
| connect, requestAccounts, switchNetwork | getAccounts, getNetwork, getPublicKey |
| signPskt, signPsbt, pushTx | getUtxoEntries, getBalance, getHoldings |
| sendToken, payKcc20, fundCredits | getTokenBalance, getState, detect |
connect() / requestAccounts() popup
await kcc.connect() → string[]
Opens the PWA. User picks a chip (if they have several) and taps Connect. Returns ["kaspa:q…"]. Also fills session: address, publicKey, network, balance, holdings.
requestAccounts() is the KasWare alias. If already connected with a stored pubkey, it returns cached accounts without a popup.
kcc.request('connect') returns a snapshot object instead of only the array: { address, accounts, network, publicKey, balance, holdings, kas, kkdags }.
Errors: Tap Connect KCC20 Wallet (no user gesture), popup blocked, user rejected, wallet locked / no key yet.
getAccounts() silent
await kcc.getAccounts() → string[]
Same addresses as Connect. No extra prompt if the origin is already allowed. Also: getter kcc.accounts.
getNetwork() silent
await kcc.getNetwork() → "kaspa_mainnet" | "kaspa_testnet_10"
KIP-12 provider maps those to mainnet / testnet-10. Always normalize: /testnet/.test(n) ? 'testnet-10' : 'mainnet'.
switchNetwork(id) popup
await kcc.switchNetwork('testnet-10' | 'mainnet' | 'kaspa_testnet_10' | 'tn10')
User confirms in the PWA. Same key; address prefix follows the network. Emits networkChanged and chainChanged.
getPublicKey() silent
await kcc.getPublicKey() → hex string
From the Connect snapshot. Needed to build P2PK scripts / KRON owner fields. If you see No public key in this KCC20 session. Connect again. the session was opened on an old SDK — Disconnect once, Connect once.
getUtxoEntries(address?) silent
await kcc.getUtxoEntries(address?) → Utxo[]
Public Kaspa UTXOs for that kaspa:q (or the connected account). Each item has both REST and KasWare-flat fields:
{
address,
outpoint: { transactionId, index },
utxoEntry: { amount, scriptPublicKey: { version, scriptPublicKey }, blockDaaScore, isCoinbase },
transactionId, index, amount,
scriptPublicKey: { version, script, scriptPublicKey },
blockDaaScore, isCoinbase
}
amount is sompi as a string. Empty array = that address has no spendable KAS — do not fake UTXOs.
getBalance(address?) silent
await kcc.getBalance(addr?) → { confirmed, unconfirmed, address }
Sompi. kcc.request('getBalance', { address }) also returns { balanceKAS, pending, address } (KAS units) for KasWare-shaped UIs.
getHoldings() silent
await kcc.getHoldings() → { address, network, holdings[] }
KAS + KCC20 (+ KRC-20 if the wallet listed them). Each holding: { tick, name, decimals, raw, balance, protocol, address }. Alias: getKcc20Holdings via request().
getTokenBalance(tick) silent
await kcc.getTokenBalance('KKDAG') → { tick, balance, raw, decimals, protocol, address }
Default tick is KKDAG. Alias: getKcc20Balance. Use this to show “this wallet holds N KKDAG” before Fund / pay.
getState() silent
await kcc.getState() → snapshot
Full Connect snapshot: accounts, address, network, publicKey, name, balance, holdings, kas, kkdags. Cached after Connect if the popup is closed.
detect() silent
kcc.detect() → { available, isKcc20, sdkVersion, name, embedded, origin, accounts, network }
Synchronous. Use to paint a “KCC20 Wallet” chip before Connect. isEmbedded() is true only inside the KCC20 PWA iframe (TTT Profile), not a Replit iframe.
signPskt / signPsbt popup
await kcc.signPskt({ txJsonString, options: { signInputs: [{ index, sighashType: 1 }] } }) → string
You build rusty-kaspa Safe JSON. The wallet PIN-signs (or KasWare if that chip is KasWare-only). Returns a string.
signInputs: globaltx.inputs[]indexes (0-based), only the connected address’s P2PK funding slots. Not “0 = first wallet input.” Never list curve/inventory/covenant slots (that yields node errorfalse stack entry at end of script execution).sighashTypemust be1(SIGHASH_ALL).- Never list covenant / KRON curve / pool / token-cell inputs. That is the usual KasWare PSKT break.
- If you omit the list, KCC20 signs only unsigned inputs whose UTXO address matches the connected wallet.
- Also accepts
signPskt(jsonString, { signInputs })andsignedTxas an alias field. - Reject →
User rejected. Show it. No retry loop.
Needs a user click (popup). If Connect already used the gesture, give the user a Sign button.
pushTx(signedJson) popup
await kcc.pushTx(signed) → { txId, node }
Broadcasts a signed Safe JSON string. Alias: request('broadcast', { signedTx }). Optional if you submit to your own node. Do not double-broadcast.
buyKron / buyToken popup
await kcc.buyKron({ tick: 'KKDAG', amount: '10' }) → { txId, quote, explorer }
Let a user buy any KCC20 launched on kron.technology on your platform. The wallet quotes and builds the same swap as Home → TRADE. You do not assemble curve/pool PSKTs. amount is KAS to spend.
tick— KRONsymbol. Live list: Tokens tab, tokens.json, KRON tokenlist.- Skip empty ticks and ticks containing
?. - Optional preview:
quoteKron({ tick, side:'buy', amount }). If it throws, skip — Buy still quotes. - Sell:
sellKron({ tick, amount })where amount is tokens. - Aliases:
buyToken,tradeKron({ tick, side, amount }),request('buyKron', { tick, amount }). - Needs
sdk.js?v=167. Mainnet only.
await kcc.buyKron({ tick: 'KKDAG', amount: '10' });
sendToken / payKcc20 / fundCredits popup
await kcc.sendToken({ tick, amount, dest }) → { txId, tick, amount, raw, dest, from, explorer }
The wallet builds a native KRON/KCC20 send (same path as Home → Send). User Signs in the PWA. Use this for TTT Fund / “pay N KKDAG”. Do not use it to buy — buying is buyKron.
tickdefaultKKDAG. 2–12 A–Z0–9.amounthuman string (e.g."10"). If missing, host may default to 10.destfullkaspa:q…(not truncated, notkaspa:pvault).- Aliases:
sendKcc20,payToken,payKcc20,fundCredits. - Cell dust is ~0.50 KAS (KRON floor), leftover KAS returns as change. Not a KAS payment.
- Mainnet only. Treasury chip (ews) cannot Fund itself.
request(method, params)
await kcc.request('getTokenBalance', { tick: 'KKDAG' })
Single dispatcher for all of the above. Unknown method → error listing supported calls.
disconnect() silent
await kcc.disconnect()
Forgets this origin, clears session, emits disconnect, closes the wallet window, focuses your tab (does not jump to the next browser tab).
Events
kcc.on('accountsChanged', (accounts) => {});
kcc.on('networkChanged', (network) => {});
kcc.on('chainChanged', (network) => {});
kcc.on('balanceChanged', (snapshot) => {});
kcc.on('disconnect', () => {});
kcc.off(event, fn);
kcc.removeListener(event, fn);
Session
After Connect, sessionStorage kcc20_dapp_sess_v1 stores accounts, network, lastState (including publicKey). Popup can close. Refresh of your dApp tab restores it until Disconnect.
If window.kcc20.sdkVersion is missing or not "167": delete window.kcc20; delete window.kcc20wallet; then inject sdk.js?v=167 and hard-reload. The old IIFE returns early and keeps the broken “Connect first” behavior.
Errors
| Message | Meaning |
|---|---|
Tap Connect KCC20 Wallet | No user gesture / not a click |
Connect KCC20 Wallet first | No session. If you just connected, you are on a stale SDK |
No public key in this KCC20 session. Connect again. | Old session without pubkey — Disconnect + Connect once |
User rejected | User tapped Reject. Stop. No loop |
Allow popups… | Browser blocked window.open |
KCC20 Wallet timed out on … | User left the sheet open too long |
Need a full kaspa:q… | Truncated dest or kaspa:p vault |
This wallet has 0 TICK | Wrong chip / buy the token first |
Security
Same threat model as KasWare: the dApp is untrusted. The wallet is trusted. Keys never go to Nilla, TTT, or sdk.js.
| Control | What it does |
|---|---|
| Origin isolation | Private keys live only on kcc-20-wallet.vercel.app. dApps talk via postMessage. |
| User Approve | Connect, Sign, Send, Broadcast each show a sheet. Reject stops the request. |
| PIN | Native sign requires the wallet PIN (salted hash). Not sent to the dApp. |
| Allowlist | After Connect, that https origin may call silent reads. Disconnect forgets it. |
| HTTPS only | Host ignores non-https origins (except localhost). |
| signInputs | Only P2PK of the connected address. Covenant/curve/pool inputs must not be listed. |
| No blind KAS send | Shim sendKaspa is rejected. Use sendToken / signPskt. |
| Frame lock | The PWA sets frame-ancestors 'self' so random sites cannot iframe the PIN pad. |
| Targeted postMessage | Replies go to the requesting origin, not *. |
What a dApp must not do
- Load
sdk.jsfrom a random mirror. Pinkcc-20-wallet.vercel.app/sdk.js?v=167(or this GitHub repo). - Ask for a seed, PIN, or hex key. If a site does, it is phishing — close it.
- Treat Connect as a payment. Connect only shares an address. Spend needs Sign.
- Auto-connect on page load.
- Sign covenant / KRON curve / pool / token-cell inputs for the user.
What Connect is not
Connect is not a cryptographic login proof (same as KasWare requestAccounts). Anyone can display an address. To prove control, have the user signPskt or (later) signMessage. Do not credit funds on Connect alone.
KIP-12 discovery
window.addEventListener('kaspa:provider', (ev) => {
const { info, provider } = ev.detail || {};
// info.rdns === 'app.kcc20.wallet'
});
window.dispatchEvent(new Event('kaspa:requestProvider'));
Load sdk.js before you dispatch kaspa:requestProvider. Provider methods include connect, getAccounts, getNetwork, getPublicKey, getUtxoEntries, getBalance, signPskt, pushTx, disconnect.
KasWare
On TTT / inside the PWA iframe, window.kasware may be a KCC20 shim (isKcc20Shim) so Connect does not open the Chrome extension.
On your own dApp (Nilla, etc.): if a real extension exists, do not overwrite it. SCORPION path = window.kcc20 only. KasWare path = window.kasware. sendKaspa on the shim is rejected — use sendToken / signPskt.
buyKron on a KasWare chip is supported (BUILD 170+). The PWA builds the KRON swap and pops KasWare for the P2PK funding input only — not the curve/pool. Desktop Chrome/Edge. If the extension is missing (phone), switch to a native PIN chip. Do not treat “KasWare chip” as incompatible for buys. Do not build the covenant PSKT in React and send it to window.kasware.signPskt.
Iframe / mobile
If your app is iframed inside KCC20 Profile (TTT), the SDK talks to window.parent — no extra popup. A Replit/Nilla iframe is not the wallet; Connect still opens a popup.
If the popup becomes a browser tab, Disconnect focuses your origin then closes. Installed PWA can handle web+kcc20: when popups are blocked.
Recipes
Builder dApp (KRON buy)
- Connect once. Popup closes.
- Silent: getAccounts, getNetwork, getPublicKey, getUtxoEntries.
- You build the unsigned KRON/PSKT from quote + pubkey + UTXOs.
signPsktwith only user P2PK indexes. OptionalpushTx.
TTT Fund (real KKDAG)
await kcc.sendToken({
tick: 'KKDAG',
amount: '10',
dest: 'kaspa:qq5yhvly6338dspa9mm24g8q6chvy6v0jww3k4dgqywh0lju5mmm5pj334ews'
});
Credit off-chain only after txId. Do not fake a grant.
Shop that sells any launched KCC20
Load ticks from tokens.json / Tokens tab. Buy is buyKron, not sendToken.
await kcc.buyKron({ tick: 'KKDAG', amount: '10' });
What this SDK is not
- Not a Chrome extension. No
chrome.runtime. - Not a hosted signer. If the PWA is locked or killed, signing stops.
- Does not invent routes or amounts. You (or KRON/Cook) build the tx except
sendToken/buyKron/sellKron(wallet-built). - Does not send KAS blindly (
sendKasparejected on the shim). - Does not replace the wallet app. Wallet UI lives in KCC20-wallet.