KCC20 SDKSCORPION docs · v166

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 canHow
Connect a user’s Kaspa addressconnect() / requestAccounts() — popup, user Approves
Show which account is connectedgetAccounts(), kcc.accounts
Know mainnet vs TN10getNetwork(), switchNetwork()
Build a transactiongetPublicKey() + getUtxoEntries() (silent after Connect)
Show KAS / KCC20 bagsgetBalance(), getHoldings(), getTokenBalance('KKDAG')
Let the user sign your PSKTsignPskt({ txJsonString, options }) — you build, they sign
Compile a vault (Argent)compileVault({ type, params }) — PWA compiles P2SH, user funds. See Argent
Send native KASsendKas({ dest, amount }) — plain transfer, not a vault
Broadcast the signed txpushTx(signed) or your own node
Buy any KRON / KCC20 token with KASbuyKron({ 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 changeson('accountsChanged'|'networkChanged'|'disconnect')
List next to KasWare in a pickerKIP-12 kaspa:provider rdns: app.kcc20.wallet
Disconnectdisconnect() — 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.

jsDelivr / this docs host still open the live PWA, not the docs origin.

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);

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: global tx.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 error false stack entry at end of script execution).
  • sighashType must be 1 (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 }) and signedTx as 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 — KRON symbol. 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.

  • tick default KKDAG. 2–12 A–Z0–9.
  • amount human string (e.g. "10"). If missing, host may default to 10.
  • dest full kaspa:q… (not truncated, not kaspa:p vault).
  • 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

MessageMeaning
Tap Connect KCC20 WalletNo user gesture / not a click
Connect KCC20 Wallet firstNo 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 rejectedUser 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 TICKWrong 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.

ControlWhat it does
Origin isolationPrivate keys live only on kcc-20-wallet.vercel.app. dApps talk via postMessage.
User ApproveConnect, Sign, Send, Broadcast each show a sheet. Reject stops the request.
PINNative sign requires the wallet PIN (salted hash). Not sent to the dApp.
AllowlistAfter Connect, that https origin may call silent reads. Disconnect forgets it.
HTTPS onlyHost ignores non-https origins (except localhost).
signInputsOnly P2PK of the connected address. Covenant/curve/pool inputs must not be listed.
No blind KAS sendShim sendKaspa is rejected. Use sendToken / signPskt.
Frame lockThe PWA sets frame-ancestors 'self' so random sites cannot iframe the PIN pad.
Targeted postMessageReplies go to the requesting origin, not *.

What a dApp must not do

  • Load sdk.js from a random mirror. Pin kcc-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)

  1. Connect once. Popup closes.
  2. Silent: getAccounts, getNetwork, getPublicKey, getUtxoEntries.
  3. You build the unsigned KRON/PSKT from quote + pubkey + UTXOs.
  4. signPskt with only user P2PK indexes. Optional pushTx.

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 (sendKaspa rejected on the shim).
  • Does not replace the wallet app. Wallet UI lives in KCC20-wallet.

Live demo · Markdown · GitHub