KCC20 Wallet SDK
SCORPION · plug-and-play dApp connect · no Chrome extension

sdkVersion 175 · VEYRA · Forge

Connect KCC20 Wallet in any app

One script. Creates window.kcc20. This is the VEYRA signing boundary: dApps request, this wallet authorizes, the user decides, Kaspa settles. Hosted PWA — keys never leave kcc-20-wallet.vercel.app. Your app builds the unsigned transaction. The user Approves in our window. Fork the wallet: FORK.md.

Install (one line)
script src="https://kcc-20-wallet.vercel.app/sdk.js?v=176"
After load: window.kcc20.sdkVersion === "176" · window.kcc20.veyra.principle · window.kcc20.forge

Wallet Forge

You → Apps → Wallet Forge. Drag Kaspa primitives (official mark, identity, KAS, KCC20, activity, receive QR, send, TTT, network) onto a canvas. They bind to the live Scorpion core. Edit/delete in the preview. Export HTML that Connects with this SDK. Keys stay in the core. Spec: FORGE.md.

await window.kcc20.request('forge')

VEYRA

TTT Phase 6. Silent descriptor — no popup:

await window.kcc20.request('veyra')
// { name, principle, spec, fork, rdns, rules }

Working spec: VEYRA.md. Build your own wallet from this PWA: FORK.md.

1. Getting started

Only call Connect from a user click. Never on page load. Never on app switch. That is the same rule KasWare documents.

const s = document.createElement('script');
s.src = 'https://kcc-20-wallet.vercel.app/sdk.js?v=175';
s.onload = () => console.log(window.kcc20.sdkVersion); // "175"
document.head.appendChild(s);

// or wait for:
window.addEventListener('kcc20#initialized', (e) => {
  const kcc = e.detail; // same as window.kcc20
});

2. Full happy path

const kcc = window.kcc20;

// POPUP — user taps Connect in your UI, Approves in KCC20, popup CLOSES. That close is required.
const accounts = await kcc.connect();
const address = accounts[0];                 // kaspa:q…

// SILENT — popup stays closed. Do not connect() again for these.
const network = await kcc.getNetwork();      // kaspa_mainnet | kaspa_testnet_10
const pubKey  = await kcc.getPublicKey();    // hex
const utxos   = await kcc.getUtxoEntries(address);

// YOU build unsigned rusty-kaspa Safe JSON from pubKey + utxos + your route.
// POPUP — user Signs.
const signed = await kcc.signPskt({
  txJsonString: unsignedSafeJson,
  options: { signInputs: userP2pkIndexes.map(i => ({ index: i, sighashType: 1 })) }
});

// POPUP — optional. Or broadcast with your own node.
const { txId } = await kcc.pushTx(signed);
After connect() the wallet window closes on purpose so reads stay silent. To self-send KAS (TTT app-store unlock) call request('sendKaspa', { to, amount }) — it reopens the popup and the user confirms. Pin sdk.js?v=169.

3. Popup vs silent

Opens the PWASilent (no window)
connect / requestAccountsgetAccounts
signPskt / signPsbtgetNetwork
pushTxgetPublicKey
sendToken / payKcc20getUtxoEntries
switchNetworkgetBalance / getHoldings / getTokenBalance

4. API reference

connect() / requestAccounts()

Returns Promise<string[]> of kaspa:q… addresses. Opens the PWA. User must already have created or imported a key and unlocked with PIN. Allow popups for your origin.

getAccounts()

Same addresses, no extra prompt if already connected.

getNetwork()

kaspa_mainnet or kaspa_testnet_10. KIP-12 provider normalizes to mainnet / testnet-10. Handle both.

getPublicKey()

Hex public key of the connected account. From the Connect snapshot. Silent.

getUtxoEntries(address?)

UTXOs for building a PSKT. Each item has REST fields (outpoint, utxoEntry) and KasWare-flat aliases (transactionId, index, amount, scriptPublicKey).

getBalance(address?)

{ confirmed, unconfirmed, address } in sompi.

signPskt({ txJsonString, options: { signInputs } })

You pass unsigned rusty-kaspa Safe JSON. We return a signed string. signInputs: only this wallet’s P2PK input indexes, sighashType: 1 (SIGHASH_ALL). Never list covenant / KRON curve / pool / token-cell inputs. Reject → error User rejected.

pushTx(signedJson)

{ txId, node }. Optional if you broadcast yourself.

sendToken({ tick, amount, dest })

Wallet-built KCC20 send (TTT Fund). Not for KRON curve buys — those you build, then signPskt.

disconnect()

Forget this origin. Popup closes. Stay on your tab.

Events

kcc.on('accountsChanged', (accounts) => {});
kcc.on('networkChanged', (network) => {});
kcc.on('disconnect', () => {});

5. Wallet-agnostic discovery (KIP-12)

window.addEventListener('kaspa:provider', (ev) => {
  const { info, provider } = ev.detail || {};
  // info.rdns === 'app.kcc20.wallet'  → KCC20 PWA
});
window.dispatchEvent(new Event('kaspa:requestProvider'));

Load sdk.js before you dispatch kaspa:requestProvider.

6. KasWare vs KCC20

KasWareKCC20 (SCORPION)
How it appearsChrome extension injects window.kaswareYour page loads sdk.js → window.kcc20
ConnectrequestAccounts()connect() / requestAccounts()
SignsignPsktsame
Keysextensionhosted PWA, PIN, never on your origin
When to useuser has the extensionyou do not want to require an extension

If a real KasWare extension is present, do not overwrite window.kasware. Offer both adapters. SCORPION uses window.kcc20 only.

7. Pitfalls (what dApps actually hit)

  1. Stale SDK — first load cached an old sdk.js. Require sdkVersion === "169". Script URL must include ?v=169.
  2. Connect first after a successful Connect — old SDK needed the popup to stay open. v166 keeps the session. Do not Connect-loop.
  3. Wrong object — call getPublicKey on the same window.kcc20 that ran connect(). Do not clone the provider before Connect.
  4. Empty UTXOs — that kaspa:q has no KAS. User must fund it. Do not fake UTXOs.
  5. Network strings — kaspa_mainnet vs mainnet. Normalize /testnet/ → testnet-10.
  6. signInputs — only P2PK of the connected address. Signing a KRON curve/pool/token cell is how KasWare PSKTs break.
  7. Popup blocked on Sign — Sign must be in the same user-click as Prepare, or a dedicated Sign button. Browsers block window.open without a gesture.
  8. sendToken ≠ curve buy — sendToken transfers a KCC20 bag. A KRON purchase is a tx you build, then signPskt.
  9. Eager script load — loading sdk.js on every page mount can trip Replit/Vite overlays. Load on Connect click.
  10. Mobile — if the popup becomes a tab, Disconnect focuses your origin first, then closes.

8. KasDistro (and any pay-many dApp)

KasDistro lives in this wallet at You → Apps → KasDistro (https://kasdistro.com). Same SDK as TTT. They never hold keys. Copy this for any AI or site that needs Connect + sign + send KAS.

<script src="https://kcc-20-wallet.vercel.app/sdk.js?v=169"></script>
<button id="connect">Connect KCC20 Wallet</button>
<script>
const kcc = window.kcc20; // sdkVersion "169"
document.getElementById('connect').onclick = async () => {
  const accounts = await kcc.connect();          // user click only
  const from = accounts[0];                      // kaspa:q…
  const network = await kcc.getNetwork();        // silent
  const utxos = await kcc.getUtxoEntries(from);  // silent — you build the tx
  // ONE payout: wallet-built send (user Signs in the PWA)
  const paid = await kcc.sendKaspa({ to: 'kaspa:q…', amount: '1.5' });
  // paid.txId
  // MANY payouts: you assemble one rusty-kaspa Safe JSON with N outputs, then:
  // const signed = await kcc.signPskt({
  //   txJsonString,
  //   options: { signInputs: [{ index: fundingIndex, sighashType: 1 }] }
  // });
  // const { txId } = await kcc.pushTx(signed);
};
</script>

Full page for scrapers: kasdistro.html. Rules: Connect on click. After Approve the popup closes. Reads stay silent. signInputs.index is the global tx.inputs[] P2PK slot. Never list covenant inputs. Never ask for PIN or hex key. pushTx returns { txId, node }.

9. Safety

Links