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.
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);
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 PWA | Silent (no window) |
|---|---|
connect / requestAccounts | getAccounts |
signPskt / signPsbt | getNetwork |
pushTx | getPublicKey |
sendToken / payKcc20 | getUtxoEntries |
switchNetwork | getBalance / 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
| KasWare | KCC20 (SCORPION) | |
|---|---|---|
| How it appears | Chrome extension injects window.kasware | Your page loads sdk.js → window.kcc20 |
| Connect | requestAccounts() | connect() / requestAccounts() |
| Sign | signPskt | same |
| Keys | extension | hosted PWA, PIN, never on your origin |
| When to use | user has the extension | you 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)
- Stale SDK — first load cached an old
sdk.js. RequiresdkVersion === "169". Script URL must include?v=169. - Connect first after a successful Connect — old SDK needed the popup to stay open. v166 keeps the session. Do not Connect-loop.
- Wrong object — call
getPublicKeyon the samewindow.kcc20that ranconnect(). Do not clone the provider before Connect. - Empty UTXOs — that
kaspa:qhas no KAS. User must fund it. Do not fake UTXOs. - Network strings —
kaspa_mainnetvsmainnet. Normalize/testnet/→testnet-10. - signInputs — only P2PK of the connected address. Signing a KRON curve/pool/token cell is how KasWare PSKTs break.
- Popup blocked on Sign — Sign must be in the same user-click as Prepare, or a dedicated Sign button. Browsers block
window.openwithout a gesture. - sendToken ≠ curve buy —
sendTokentransfers a KCC20 bag. A KRON purchase is a tx you build, thensignPskt. - Eager script load — loading
sdk.json every page mount can trip Replit/Vite overlays. Load on Connect click. - 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
- You build. They sign. Never send a private key or PIN.
- Never invent amounts. Never credit a user without a real
txId. - SIGHASH_ALL (1) only on this build.
- Do not store
x-access-token, seeds, or hex keys.
Links
- Wallet: kcc-20-wallet.vercel.app
- Demo: dapp-demo.html
- SDK GitHub: mrzeku2000XTTT/kcc20-sdk
- Wallet GitHub: mrzeku2000XTTT/KCC20-wallet