๐Ÿ› ๏ธ ORIGIN BUILDERS

DEVELOPER GUIDE

Build on the Origin Layer โ€” The Honor-Bound Bitcoin L2

Create games, DeFi apps, social platforms, and more.
Every action anchored in Bitcoin. Every TX verified with honor.

QUICK START

Start Building in Minutes

01

Every Action = 1 TX

Each user action creates a Truth Anchor (TX L2) that can be verified forever.

02

Signature Required

Every TX that moves KRAY must be signed by the user. Honor-bound.

03

TX = Source of Truth

Counters and balances are derived from TXs. Never increment directly.

04

Sacred Exits

Users can always withdraw to Bitcoin L1. This is a right, not a feature.

1 KRAY

One Fee. Every Transaction.

Simple, predictable, and fair. No variable gas, no surprises.

๐Ÿ‘ค
User
pays 1 KRAY
๐Ÿ›๏ธ
Treasury bc1p5u00...k3nhv

What costs 1 KRAY?

Click any category to see all transactions

Transfer KRAY1 KRAY
Bridge Deposit (L1โ†’L2)1 KRAY
Bridge Withdraw (L2โ†’L1)1 KRAY
Mint NFT1 KRAY
Transfer NFT1 KRAY
List on Marketplace1 KRAY
Unlist from Market1 KRAY
Buy NFT1 KRAY
Bridge NFT to L11 KRAY
Generate AI Image1 KRAY
Generate AI Avatar1 KRAY
AI Chat Message1 KRAY
Swap Tokens1 KRAY
Add Liquidity1 KRAY
Remove Liquidity1 KRAY
Stake KRAY1 KRAY
Unstake KRAY1 KRAY
Claim Rewards1 KRAY
Like Post1 KRAY
Repost / Share1 KRAY
Comment1 KRAY
Follow User1 KRAY
Send Tip1 KRAY
Quiz Vote1 KRAY
Create Post (+ optional Quiz)1 KRAY
Game Action1 KRAY
Place Bet1 KRAY
Claim Reward1 KRAY
Vote in DAO1 KRAY
Join Tournament1 KRAY
Lottery Create1 KRAY
Lottery Buy Ticket1 KRAY + ticket
FOR DEVELOPERS

Add Your Own Fee

Network fee is separate. Charge whatever you want for your service!

User Pays 11 KRAY
=
Network 1 KRAY โ†’ Treasury
+
Your Fee 10 KRAY โ†’ Your Wallet
0 KRAY 50 KRAY 100 KRAY

Real Services on L2 KRAY:

๐ŸŽจ AI Image Gen 1 + 10 = 11
๐Ÿช NFT Sale (2%) 1 + 2% = varies
๐ŸŽฎ Game Match 1 + 5 = 6
๐Ÿค– AI Avatar 1 + 5 = 6
๐Ÿ”Œ WALLET INTEGRATION

Connect KRAY Wallet in 5 Minutes

๐Ÿ‘‹ New here? "I want to use KRAY Wallet on my site"

You don't need to be a blockchain expert. KRAY is a Bitcoin L2 and the wallet is open to any developer โ€” the same way any site connects MetaMask. Follow these steps and you can accept payments, mint NFTs, and more. Everything below is open (no approval needed) and self-custodial: the wallet holds the keys, your users approve every action in a popup.

1
Detect the wallet. Check window.krayWallet. If it's missing, ask the user to install the KRAY Wallet extension.
2
Connect + prove who they are. Call requestAccounts(), then signMessageWithConfirmation() on a one-time challenge from your backend. That's a secure login โ€” no passwords.
3
Accept payments (KRAY, DOG, RADIOLA, DSC). The user signs a transfer to your address; you POST it to /l2/transaction/send. Great for plans, subscriptions, tips, in-game items.
4
Open an Ordinal gallery & mint L2 children. Any Ordinal becomes a parent when you mint a child under it. โ€œCollectionโ€ on Kray is just the gallery label (name, mint settings) via /l2/nfts/collection/create + parent_inscription_id โ€” see NFT Studio.
5
Always verify on your backend. Before granting anything, confirm the signature (Schnorr) and that the payment actually landed โ€” or, for NFT products, that the wallet currently holds your pass (see Live NFT License). The templates below do exactly this.

Open now vs. Partner access

Open to everyone (no approval): connect, login/prove, L2 token payments, L2 NFT collections & mints, reading balances/runes/inscriptions.  โ€ข  Partner access (email partners@kray.space): PSBT signing, L1 atomic-swap marketplace trading, and advanced DeFi โ€” the higher-risk surface that needs vetting. Most sites only need the open tier.

๐Ÿ‘‡ Below: a 3-line quick check, then the full copy-paste template (frontend + backend) with all the security built in.


// 1. Check if installed
if (typeof window.krayWallet !== 'undefined') {
    console.log('KRAY Wallet detected!');
}

// 2. Connect
async function connectWallet() {
    const result = await window.krayWallet.requestAccounts();
    if (result.success) {
        console.log('Connected:', result.address);
    }
}

// 3. Get data
const balance = await window.krayWallet.getBalance();
const inscriptions = await window.krayWallet.getInscriptions();
const runes = await window.krayWallet.getRunes();
            

๐Ÿ” Stay connected across reloads โ€” the standard dApp pattern

Like MetaMask and every other wallet: once the user connects, your site should stay connected through page refreshes until they click Disconnect. The trick is two pieces working together โ€” a small flag you cache locally (the user's intent to be connected) plus getAccounts(), which restores the address silently, with no popup. The extension stays the source of truth: if the user revoked your origin, getAccounts() returns [] and you fall back to the Connect button.

Golden rule: only requestAccounts() opens a popup โ€” call it from the Connect button. On page load use getAccounts(); never call requestAccounts() automatically or you'll spam a popup on every refresh.


// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// STAY CONNECTED ACROSS RELOADS โ€” the standard pattern
// Connect once; the site stays connected through refreshes until
// the user clicks Disconnect. No popup on refresh.
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const CONNECTED_KEY = "myapp:wallet-connected";

// Call ONCE on page load, before deciding to show Connected vs "Connect".
async function restoreWalletSession() {
    if (!window.krayWallet) return null;                        // not installed
    if (localStorage.getItem(CONNECTED_KEY) !== "1") return null; // user isn't opted in
    try {
        // getAccounts() is SILENT โ€” never opens a popup. Returns [] if the
        // user revoked this origin in the extension.
        const [address] = await window.krayWallet.getAccounts();
        if (!address) { localStorage.removeItem(CONNECTED_KEY); return null; } // self-heal
        return address;
    } catch {
        return null;                                            // stay logged out
    }
}

// Call from your "Connect" button (this is the one that may popup).
async function connectWallet() {
    const result = await window.krayWallet.requestAccounts();
    const address = Array.isArray(result) ? result[0] : result.address;
    if (address) localStorage.setItem(CONNECTED_KEY, "1");      // remember the choice
    return address;
}

// Call from your "Disconnect" button โ†’ a refresh now stays logged out.
function disconnectWallet() {
    localStorage.removeItem(CONNECTED_KEY);
    // ...also clear your own app/session state (cookies, JWT, UI).
    // The extension keeps the origin approved (expected); the flag above is
    // what gates auto-reconnect, so the wallet stays gone until Connect.
}

// Wire it up on load:
//   const address = await restoreWalletSession();
//   address ? showConnected(address) : showConnectButton();
            

๐Ÿงฉ Full copy-paste template โ€” Connect โ†’ Prove โ†’ Get paid

A complete, self-contained flow any site can drop in: let a user connect, prove they own their address, and pay you in any KRAY L2 token (KRAY, DOG, RADIOLA, DSC, SATSPACE). Open tier only โ€” no partner access needed. Golden rules: the wallet never exposes keys (the user confirms every action in the popup); amounts travel in RAW units (multiply by 10^divisibility); and your backend must verify the signature and confirm the payment on-chain before granting anything โ€” never trust the client.


// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// FRONTEND โ€” uses only the open, public window.krayWallet API
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const KRAY = "https://kray.space";
const DIVISIBILITY = { KRAY: 0, DOG: 5, RADIOLA: 2, DSC: 0, SATSPACE: 3 };
const toRaw = (human, token) => Math.round(human * 10 ** (DIVISIBILITY[token] ?? 0));

function wallet() {
    if (!window.krayWallet) throw new Error("Install the KRAY Wallet extension.");
    return window.krayWallet;
}

// Current sequential nonce for the account (NOT Date.now()).
async function getNonce(address) {
    const acc = await fetch(`${KRAY}/l2/account/${address}`).then(r => r.json());
    return acc.nonce ?? 0;
}

// 1) CONNECT + PROVE OWNERSHIP (login)
async function connectAndProve() {
    const w = wallet();
    const acc = await w.requestAccounts();
    const address = Array.isArray(acc) ? acc[0] : acc.address;
    const pk = await w.getPublicKey();
    const pubkey = pk.publicKey ?? pk;

    // Your backend issues a one-time nonce so the login can't be replayed.
    const { nonce } = await fetch("/your-backend/auth/challenge").then(r => r.json());
    const message = `myapp:login:${address}:${nonce}`;
    const { signature } = await w.signMessageWithConfirmation(message);

    // Send {address, pubkey, message, signature} to your backend to verify.
    return { address, pubkey, message, signature };
}

// 2) ACCEPT A PAYMENT in any L2 token (e.g. 500 DOG)
async function payWithToken({ buyer, token, amountHuman, yourReceiveAddress }) {
    const w = wallet();
    const amountRaw = toRaw(amountHuman, token);      // RAW units
    const nonce = await getNonce(buyer);
    const pk = await w.getPublicKey();
    const pubkey = pk.publicKey ?? pk;

    // Signed message format the network verifies: from:to:amount:nonce:tx_type:token
    const message = `${buyer}:${yourReceiveAddress}:${amountRaw}:${nonce}:transfer:${token}`;
    const { signature } = await w.signMessageWithConfirmation(message);

    const res = await fetch(`${KRAY}/l2/transaction/send`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            from_account: buyer, to_account: yourReceiveAddress,
            amount: amountRaw, token_symbol: token, tx_type: "transfer",
            nonce, signature, pubkey,
        }),
    });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || "Payment failed");
    return data.tx_hash;   // hand this to your backend to confirm
}
            

// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// BACKEND (Node) โ€” the source of truth. Verify, then grant.
//   npm i @bitcoinerlab/secp256k1 bitcoinjs-lib
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import * as ecc from "@bitcoinerlab/secp256k1";
import * as bitcoin from "bitcoinjs-lib";
import { createHash } from "crypto";
bitcoin.initEccLib(ecc);

// Proves the signer actually owns that taproot address (Schnorr BIP-340 + P2TR).
export function verifyOwnership(message, sigHex, pubkeyHex, address) {
    const pub = Buffer.from(pubkeyHex, "hex");
    const x = pub.length === 33 ? pub.subarray(1, 33) : pub.subarray(0, 32);
    const derived = bitcoin.payments.p2tr({
        internalPubkey: x, network: bitcoin.networks.bitcoin,
    }).address;
    if (derived !== address) return false;              // anti-spoof
    const hash = createHash("sha256").update(message).digest();
    const sig = Buffer.from(sigHex, "hex");
    return sig.length === 64 && ecc.verifySchnorr(hash, x, sig);
}

// Confirms the payment really landed with the right token + RAW amount.
export async function confirmPayment(txHash, to, token, amountRaw) {
    const tx = await fetch(`https://kray.space/l2/transaction/${txHash}`).then(r => r.json());
    return tx?.status === "confirmed" &&
           tx?.to_account === to &&
           (tx?.token_symbol || "KRAY") === token &&
           BigInt(tx?.amount ?? 0) === BigInt(amountRaw);
}

// Grant the plan / NFT / access ONLY when BOTH are true. Never before.
            

๐ŸŽซ Pattern โ€” Live NFT License (dynamic token-gating)

The NFT is the license. Access follows current possession, re-checked on-chain every time the user connects or uses your product. Connect wallet โ†’ prove ownership (Schnorr) โ†’ your backend reads holdings โ†’ still holds the pass โ†’ grant; transfer/sell โ†’ next check cuts access. Uses only existing open APIs (verifyOwnership + GET /api/l2/nfts/:address + inscription owner). No new L2 tx type. No protocol change.

  • Match by: L2 NFT id, L2 collection_id, and/or L1 Ordinals inscription id (64-hex + i + index)
  • L2 status: only ACTIVE grants access โ€” BRIDGED / INCUBATOR copies do not
  • Never trust the client claiming โ€œI hold the NFTโ€ โ€” your server must query the chain
  • Fail closed on upstream errors; re-check holdings on every privileged request

Perfect for: VIP passes, game skins, gated chat rooms, course access, club memberships โ€” any product where selling the NFT should move the access with it.


// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// LIVE NFT LICENSE โ€” backend helpers (open APIs only)
// Reuse verifyOwnership() from the template above for the wallet proof.
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const KRAY = "https://kray.space";

/**
 * Does `address` CURRENTLY hold a live pass?
 * Configure ONE OR MORE of: l2CollectionIds, l2NftIds, l1InscriptionIds.
 */
export async function holdsLivePass(address, gate) {
    const need = Math.max(1, gate.minCount ?? 1);
    const addr = address.toLowerCase();
    let held = 0;

    const wantL2 = (gate.l2CollectionIds?.length || gate.l2NftIds?.length);
    if (wantL2) {
        const res = await fetch(`${KRAY}/api/l2/nfts/${addr}`);
        if (!res.ok) return false;                          // fail closed
        const body = await res.json();
        const list = Array.isArray(body) ? body : (body.nfts ?? []);
        const collections = new Set(gate.l2CollectionIds ?? []);
        const ids = new Set(gate.l2NftIds ?? []);
        for (const n of list) {
            const status = String(n.status ?? "").toUpperCase();
            if (status !== "ACTIVE") continue;              // BRIDGED โ‰  license
            const id = String(n.id ?? n.nft_id ?? "");
            const coll = String(n.collection_id ?? n.collectionId ?? "");
            if (collections.has(coll) || ids.has(id)) {
                if (++held >= need) return true;
            }
        }
    }

    for (const inscriptionId of gate.l1InscriptionIds ?? []) {
        // L1 Ordinal owner โ€” use YOUR ord indexer (or Kray node /ord/inscription/:id
        // with your API key). Public sites should not hardcode a private node URL.
        const res = await fetch(`${YOUR_ORD_BASE}/ord/inscription/${inscriptionId}`, {
            headers: YOUR_ORD_HEADERS, // e.g. { "x-api-key": "โ€ฆ" } if required
        });
        if (!res.ok) return false;                          // fail closed
        const { address: owner } = await res.json();
        if (owner && owner.toLowerCase() === addr) {
            if (++held >= need) return true;
        }
    }
    return false;
}

// Example gate configs (pick what fits your product):
//   { l2CollectionIds: ["my_club_pass"], minCount: 1 }
//   { l2NftIds: ["nft_unique_1of1"] }
//   { l1InscriptionIds: ["a1b2โ€ฆ64charsโ€ฆi0"] }

/**
 * Full gate on a privileged action:
 *   1) verifyOwnership(message, sig, pubkey, address)  โ† wallet is real
 *   2) holdsLivePass(address, YOUR_GATE)               โ† NFT still there
 * Grant ONLY when both are true. Re-run on every request (live license).
 */
            

// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// FRONTEND โ€” prove wallet, then ask YOUR backend (never skip the proof)
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async function unlockWithNftPass() {
    const w = wallet();
    const acc = await w.requestAccounts();
    const address = Array.isArray(acc) ? acc[0] : acc.address;
    const pubkey = (await w.getPublicKey()).publicKey ?? await w.getPublicKey();

    // One-time challenge from YOUR backend (anti-replay) โ€” same as login.
    const { nonce } = await fetch("/your-backend/auth/challenge").then(r => r.json());
    const message = `myapp:nft-pass:${address}:${nonce}`;
    const { signature } = await w.signMessage(message); // login-style, frictionless

    // Backend: verifyOwnership + holdsLivePass โ†’ session / cookie / JWT you issue.
    const res = await fetch("/your-backend/nft-pass/unlock", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ address, pubkey, message, signature, nonce }),
    });
    if (!res.ok) throw new Error("Pass not held or signature invalid");
    return res.json(); // { ok: true, session: "โ€ฆ" }
}
            
๐Ÿ“š COMPLETE API REFERENCE

window.krayWallet - All Methods

Copy-paste examples for everything your app needs.

๐Ÿ”— Connection & Identity

requestAccounts() RECOMMENDED

Connect wallet with automatic popup. Use this first!

const result = await window.krayWallet.requestAccounts();
if (result.success) {
    console.log('Address:', result.address);
    console.log('Public Key:', result.publicKey);
}
connect()

Silent connect (no popup if already unlocked)

const result = await window.krayWallet.connect();
// Returns { success: true, address, publicKey, balance }

๐Ÿ” Origin approval (v2.1.0+)

The first time an unknown website calls connect() / requestAccounts(), KrayWallet opens a Connection Request screen and the user must approve that origin. Until the origin is approved, all other API calls are rejected. kray.space (and subdomains) and localhost are auto-approved. Approval is remembered per origin.

getAccounts()

Get connected accounts (array) โ€” silent, no popup. Use this on page load to restore a session (see Stay connected across reloads above). Returns [] if the origin isn't approved.

const accounts = await window.krayWallet.getAccounts();
// Returns ['bc1p...'] โ€” array with full Bitcoin taproot address
// Returns []          โ€” user not connected / origin revoked (show "Connect")
getPublicKey()

Get public key (hex)

const pubKey = await window.krayWallet.getPublicKey();
// Returns x-only hex string directly (e.g. '5a4566...' โ€” 64 chars)
// This is NOT an object. It's a plain string.
// To get the bc1p... address, use getAccounts() instead.
getExtensionInfo()

Verify it's genuine KRAY Wallet

const info = window.krayWallet.getExtensionInfo();
// { signature, version, build, isKrayWallet: true }

๐Ÿ’ฐ Assets & Balance

getBalance()

Get BTC balance in satoshis

const balance = await window.krayWallet.getBalance();
// Returns 150000 (sats)
getInscriptions(offset?, limit?)

Get user's Ordinals/NFTs

const { total, list } = await window.krayWallet.getInscriptions();
list.forEach(nft => {
    console.log('ID:', nft.inscriptionId);
    console.log('Preview:', nft.preview);
    console.log('UTXO:', nft.utxo);
});
getRunes()

Get user's Runes tokens

const { runes } = await window.krayWallet.getRunes();
runes.forEach(rune => {
    console.log('Name:', rune.spacedRune);
    console.log('Amount:', rune.amount);
    console.log('Symbol:', rune.symbol);
});
getFullWalletData() RECOMMENDED

Get EVERYTHING at once: balance + ordinals + runes

const data = await window.krayWallet.getFullWalletData();
console.log('Address:', data.address);
console.log('Balance:', data.balance);
console.log('NFTs:', data.inscriptions.length);
console.log('Runes:', data.runes.length);

โœ๏ธ Signing (L1 Bitcoin)

signMessage(message) USE WITH CAUTION

Auto-sign for harmless messages only โ€” If the wallet is unlocked AND the message is non-sensitive (login, identity proofs, read-only auth), it signs silently without popup. If locked, falls back to popup.

Since v2.1.0 the wallet inspects the message: sensitive actions ALWAYS open the confirmation popup, even when unlocked โ€” there is no way to silently sign them. This includes KRAY:cancel-listing and other KRAY:* canonical actions, Dev Scroll mutations (scroll_create / scroll_edit / scroll_topup / scroll_remove), destructive KrayChat actions (delete_group / remove_member / add_member), and any L2 transaction message (messages starting with the user's own address).

const { signature, address } = await window.krayWallet.signMessage('verify:login:timestamp');
// Returns { signature: '64-byte hex', address: 'bc1p...' }
// Non-sensitive + unlocked โ†’ signs instantly (frictionless login)
// Sensitive message โ†’ confirmation popup ALWAYS opens (user must approve)

// NOTE: a legacy second parameter `password` is IGNORED since v2.1.0.
// Web pages must NEVER handle the wallet password โ€” the popup collects it.
signMessageWithConfirmation(message) RECOMMENDED

Always popup โ€” ALWAYS opens the KrayWallet confirmation popup, even if the wallet is already unlocked. The user sees exactly what they are signing, types their password, and clicks to confirm.

Use for ALL value-moving actions: claims, transfers, purchases, minting, tips. The user explicitly approves every action.

const { signature, address } = await window.krayWallet.signMessageWithConfirmation(message);
// Returns { signature: '64-byte hex', address: 'bc1p...' }
// โœ… ALWAYS opens popup โ†’ user sees the message โ†’ types password โ†’ confirms

Both methods produce identical Schnorr signatures

The cryptography is the same โ€” same private key, same SHA-256 hash, same BIP-340 Schnorr algorithm. The only difference is UX: signMessage() can sign frictionlessly during an unlocked session (the session key only decrypts the vault โ€” the signing key is always the normal BIP86 key, and the wallet never stores the password itself), while signMessageWithConfirmation() always shows the popup. Sensitive messages route to the popup either way. The backend cannot distinguish between them.

Signature Format โ€” canonical spec (read this before building a verifier)

  • Algorithm: BIP-340 Schnorr over SHA256(UTF-8 message) โ€” a Kray custom digest. It is not BIP-322, not the legacy "Bitcoin Signed Message" format, and not ECDSA.
  • Signature: 64 bytes, returned as a 128-char hex string (never base64).
  • Pubkey: 32-byte x-only hex โ€” the BIP86 internal (untweaked) key at m/86'/0'/0'/0/0. It is returned alongside the signature and is required for verification.
  • Address binding: derive p2tr({ internalPubkey }) from the provided pubkey and compare it to the claimed bc1pโ€ฆ address (see the backend snippet above).

Common integrator mistake

Do not verify against a key extracted from the bc1pโ€ฆ address itself โ€” the 32 bytes inside the address are the tweaked output key, and the signature is made with the internal key, so that check will always fail. Always verify against the pubkey the wallet returns, then bind it to the address as shown above.

External wallets (Unisat, Xverse, โ€ฆ) authenticating against Kray backends use a separate BIP-322 (base64) verify path โ€” that path never applies to window.krayWallet.signMessage.

What the user sees in the popup

When signMessageWithConfirmation() is called, the KrayWallet popup opens and displays the message string you wrote. The wallet parses the message and shows the user a clear summary of what they are signing: the action type, the cost, a description, and the raw message. Write clear, descriptive messages so the user knows exactly what they are approving.

// The message you pass IS what the user reads in the popup.
// Write it clearly so the user understands what they are signing.

// Good โ€” clear and descriptive:
`scroll_claim:reward:${userAddress}:${timestamp}`
// โ†’ Popup shows: "Claim Reward" with the address and timestamp

// Good โ€” L2 transaction format (auto-parsed with rich UI):
`${fromAddress}:${toAddress}:${amount}:${nonce}:transfer`
// โ†’ Popup shows: "L2 Transfer โ€” Send X KRAY to bc1p..."

// Bad โ€” vague, user won't know what they're signing:
`action:12345`
// โ†’ Popup shows generic "Sign Action" with no useful info

Real-World Examples โ€” When to Use Each

signMessage() โ€” Auto-sign, no popup (use with caution)

// Login โ€” prove wallet ownership (no value moves)
const { signature } = await window.krayWallet.signMessage(`login:${Date.now()}`);

// Access gated content โ€” verify user holds a specific NFT
const { signature } = await window.krayWallet.signMessage(`verify:nft_access:${collectionId}`);

// Chat authentication โ€” connect to a chat room
const { signature } = await window.krayWallet.signMessage(`chat:join:${roomId}:${Date.now()}`);

// Profile verification โ€” prove you own this address
const { signature } = await window.krayWallet.signMessage(`profile:verify:${userAddress}`);

signMessageWithConfirmation() โ€” Always popup, user confirms (RECOMMENDED)

// Claim KRAY reward from Dev Scroll (value moves from ESCROW to user)
const { signature } = await window.krayWallet.signMessageWithConfirmation(
    `scroll_claim:reward:${userAddress}:${Date.now()}`
);

// Purchase an item with KRAY (value moves from user to seller)
const { signature } = await window.krayWallet.signMessageWithConfirmation(
    `purchase:${itemId}:${price}:${userAddress}:${Date.now()}`
);

// Mint NFT (gas fee charged)
const { signature } = await window.krayWallet.signMessageWithConfirmation(
    `mint:${collectionId}:${recipientAddress}:${Date.now()}`
);

// Tip a creator (KRAY transfer)
const { signature } = await window.krayWallet.signMessageWithConfirmation(
    `tip:${creatorAddress}:${amount}:${userAddress}:${Date.now()}`
);

Pro tip: Combine both in the same app โ€” signMessage() for frictionless login, then signMessageWithConfirmation() for every action that moves KRAY. Best UX + best security.

Marketplace cancel (L1) โ€” canonical signed message

All L1 marketplace cancellations require a signed canonical message with anti-replay protection. The server reconstructs the template byte-by-byte and rejects anything else (stale timestamp ยฑ5min, reused nonce โ†’ 409).

// Canonical template (exact format, no variations):
// KRAY:cancel-listing:<address>:<order_id>:<nonce>:<timestamp>

const nonce = Array.from(crypto.getRandomValues(new Uint8Array(12)))
    .map(b => b.toString(16).padStart(2, '0')).join('');
const timestamp = Date.now();
const message = `KRAY:cancel-listing:${sellerAddress}:${orderId}:${nonce}:${timestamp}`;

// Sensitive prefix โ†’ KrayWallet ALWAYS opens the confirmation popup
const { signature } = await window.krayWallet.signMessage(message);
const pubkey = await window.krayWallet.getPublicKey();

await fetch(`/api/atomic-swap/buy-now/${orderId}`, {
    method: 'DELETE',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        seller_address: sellerAddress,
        signature, message, pubkey, nonce, timestamp
    })
});
// Same body format works on POST /api/atomic-swap/:id/cancel
// and DELETE /api/runes-atomic-swap/:id

โšก Lightning Network

sendPayment(invoice)

Pay Lightning invoice

const result = await window.krayWallet.sendPayment('lnbc100n1...');
if (result.success) {
    console.log('Paid! Preimage:', result.preimage);
}

โšก L2 KRAY API

Fast, cheap transactions on KRAY Layer 2. Every TX costs 1 KRAY.

GET /api/l2/account/:address/balance

Get L2 KRAY balance for an address

const response = await fetch('https://kray.space/l2/account/bc1q.../balance');
const data = await response.json();
console.log('L2 Balance:', data.balance, 'KRAY');
POST /api/l2/transaction/send CORE

Send L2 transaction (requires signature from KRAY Wallet)

// 1. Get user's signature
// NOTE (v2.1.0+): L2 transaction messages are SENSITIVE โ€” KrayWallet
// always opens the confirmation popup before signing them.
const message = JSON.stringify({
    from: userAddress,
    to: recipientAddress,
    amount: 100,
    nonce: Date.now()
});
const { signature } = await window.krayWallet.signMessage(message);

// 2. Send to L2
const response = await fetch('https://kray.space/l2/transaction/send', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        from_account: userAddress,
        to_account: recipientAddress,
        amount: 100,
        signature: signature,
        nonce: Date.now(),
        tx_type: 'transfer'
    })
});
const result = await response.json();
console.log('TX Hash:', result.tx_hash);
GET /api/l2/transaction/:txid

Get transaction details

const response = await fetch('https://kray.space/l2/transaction/tx_abc123');
const tx = await response.json();
console.log('Status:', tx.status);
console.log('Amount:', tx.amount, 'KRAY');
GET /api/l2/account/:address/history

Get transaction history for account

const response = await fetch('https://kray.space/l2/account/bc1q.../history');
const { transactions } = await response.json();
transactions.forEach(tx => {
    console.log(tx.tx_hash, tx.amount, tx.memo);
});

๐Ÿ”€ Hybrid Architecture: L1 + L2

Use Bitcoin L1 for permanence and authenticity, L2 KRAY for fast interactions.

โ›“๏ธ L1 Bitcoin (Ordinals/Runes)

  • Permanent NFTs (Inscription ID)
  • Runes tokens
  • Proof of authenticity
  • Slow but immutable
โ†•๏ธ Bridge

โšก L2 KRAY

  • Game actions
  • Social interactions (likes, comments)
  • Trading & DeFi
  • Fast & cheap (1 KRAY/TX)

๐Ÿ“ฑ Example: Social Inscription

Posts are Ordinals inscribed on L1 Bitcoin. Likes and comments are L2 transactions.

// User creates a post (L1 Ordinal)
const inscriptionId = 'abc123i0';  // Inscribed on Bitcoin

// User likes a post (L2 transfer - costs 1 KRAY)
const like = await sendL2Transaction({
    from: userAddress,
    to: 'treasury',
    amount: 1,
    tx_type: 'transfer',
    data: { action: 'social_like', inscription_id: inscriptionId }
});

// Post is permanent on L1, interactions tracked on L2

๐Ÿ” L1 Bitcoin (Read-Only)

Read user's Ordinals and Runes for hybrid apps.

getInscriptions()

Get user's Ordinals (NFTs inscribed on Bitcoin)

const { total, list } = await window.krayWallet.getInscriptions();
list.forEach(ordinal => {
    console.log('Inscription ID:', ordinal.inscriptionId);
    console.log('Content Type:', ordinal.contentType);
    console.log('Preview:', ordinal.preview);
});
getRunes()

Get user's Runes tokens

const { runes } = await window.krayWallet.getRunes();
runes.forEach(rune => {
    console.log('Rune:', rune.spacedRune);
    console.log('Amount:', rune.amount);
    console.log('Symbol:', rune.symbol);
});

๐Ÿš€ Advanced Features

๐Ÿ”

Marketplace, DeFi & More

Building a marketplace, DeFi protocol, or advanced Bitcoin application?

We offer extended API access for approved partners including:

  • Transaction signing & broadcasting
  • Atomic swap integration
  • PSBT construction & validation
  • Runes & Ordinals trading
  • Custom DeFi integrations
Apply for Partner Access โ†’

๐ŸŒ Network

getNetwork()

Get current network (livenet/testnet)

const network = window.krayWallet.getNetwork();
// Returns 'livenet'
getActiveNetwork()

Get active layer (mainnet or kray-l2)

const layer = await window.krayWallet.getActiveNetwork();
// Returns 'mainnet' or 'kray-l2'

๐Ÿ’ฌ E2E Encrypted Chat

Secure end-to-end encrypted messaging between users.

activateChatSession()

Start encrypted chat session (opens popup for password)

const { publicKey } = await window.krayWallet.activateChatSession();
// Share publicKey with your backend for others to message you
encryptChatMessage(message, theirPublicKey)

Encrypt message for recipient

const encrypted = await window.krayWallet.encryptChatMessage(
    'Hello!',
    recipientPublicKey
);
// Send encrypted to your backend
decryptChatMessage(encrypted, theirPublicKey)

Decrypt message from sender

const message = await window.krayWallet.decryptChatMessage(
    encryptedPayload,
    senderPublicKey
);
console.log('Message:', message);
isChatSessionActive()

Check if chat session is active

const { active, publicKey } = await window.krayWallet.isChatSessionActive();
clearChatSession()

End chat session (logout)

await window.krayWallet.clearChatSession();
๐ŸŽฎ EXAMPLE: COSMIC VISION

Real Hybrid App on KRAY

Collectible card game: Cards as Ordinals (L1), game on L2.

๐ŸŽด 1111 cards ๐Ÿ–ผ๏ธ AI art โ›“๏ธ Ordinals โšก L2 game
TX L2 STRUCTURE

Truth Anchor Format

Every transaction must follow this structure

JavaScript TX L2 Structure
{
    // ๐Ÿ”‘ IDENTIFICATION (Required)
    id: "unique_txhash_32bytes",
    tx_type: "transfer",
    
    // ๐Ÿ’ฐ MOVEMENT (Required)
    from_account: "bc1p...",
    to_account: "bc1p...",
    amount: "1000",
    
    // ๐Ÿ” CRYPTOGRAPHY (Required for user TXs)
    signature: "schnorr_bip340_hex_64bytes",
    pubkey: "x_only_pubkey_hex_32bytes",
    
    // ๐Ÿ“Š METADATA (Required)
    gas_fee: "0",
    memo: "Human-readable description",
    status: "completed",
    
    // ๐Ÿ“Ž EXTRAS (Optional)
    metadata: { game_id: "xyz", ... },
    tx_data: { signing_params: {...}, result: {...} },
    
    // โฐ TIMESTAMP (Required)
    created_at: "2026-01-16T15:30:00.000Z"
}
๐Ÿ“‹ TX TYPES

Transaction Types Reference

All transaction types available on L2 KRAY - use these exact strings in your code

๐Ÿš€ Quick Copy

Click any type to copy to clipboard

transfer deposit withdrawal nft_mint nft_transfer nft_list nft_unlist nft_buy nft_bridge_out nft_bridge_in create_collection collection_config public_ai_mint
๐Ÿ’ฐ

Core Transactions

Essential
transfer
Send KRAY between users User A โ†’ User B
1 KRAY
deposit
Bridge from Bitcoin L1 L1 โ†’ L2 (verified by indexer)
1 KRAY
withdrawal
Bridge to Bitcoin L1 L2 โ†’ L1 (PSBT signing)
1 KRAY
๐Ÿช

NFT Marketplace

LIVE
nft_list
List NFT for sale Set price, create listing
1 KRAY
nft_unlist
Cancel listing Remove from marketplace
1 KRAY
nft_buy
Purchase NFT Pay price + 2% marketplace fee
1 KRAY + 2%
2% Marketplace Fee: Goes to bc1prwz4jegt9l503elk07nq0c2a5mkavq8af40hwa4885g3s6q2h2eqkkdnfn
๐ŸŽจ

NFT Operations

LIVE
nft_mint
Mint new NFT From collection or custom
1 KRAY + price
nft_transfer
Send NFT to another user Direct transfer, no sale
1 KRAY
nft_bridge_out
Bridge NFT to Bitcoin L1 Inscribe as Ordinal
1 KRAY + sats
nft_bridge_in
Return NFT from L1 Ordinal โ†’ L2 NFT
1 KRAY
๐Ÿ“

Collections

LIVE
create_collection
Create NFT collection Name, supply, royalty
1 KRAY
collection_config
Update collection settings Public mint, price, etc
1 KRAY
public_ai_mint
AI-generated mint Generate + mint in one tx
1 KRAY + AI fee
๐Ÿค–

AI Services

LIVE
ai_generate
Generate AI image Text-to-image generation
1 + 10 KRAY
AI Provider Fee: 10 KRAY โ†’ bc1pxd5snpedtrkqgmngclr4jk76g2xaf3aacawe37gqjkpvw6jcwylscd45wn
๐Ÿ› ๏ธ

Custom Apps (Build Yours!)

CREATE

Build any app on L2 KRAY! Always use tx_type: 'transfer' and put your custom action in the data field as JSON.

transfer
Gaming โ€” bet, reward, entry data: {app: "game", action: "place_bet"}
1 + YOUR_FEE
transfer
Subscription / Premium content data: {app: "service", action: "subscribe"}
1 + YOUR_FEE
transfer
DAO voting / Governance data: {app: "dao", action: "vote_cast"}
1 KRAY
transfer
Document certification data: {app: "certify", action: "certify_doc"}
1 + YOUR_FEE
โ€”
Live NFT License (token-gate) Holdings check โ€” see #live-nft-license
0 gas*

๐Ÿ“ Using TX Types in Code

// Example: Create a marketplace listing
const txData = {
  from_account: userAddress,
  to_account: 'treasury',
  amount: 0,
  tx_type: 'nft_list',  // โ† Use exact string
  nonce: accountNonce,
  signature: userSignature,
  pubkey: userPubkey,
  data: {
    nft_id: 'nft_abc123',
    price_kray: 100
  }
};

const response = await fetch('/api/l2/nfts/market/list', {
  method: 'POST',
  body: JSON.stringify(txData)
});
๐Ÿ” SIGNATURE FLOW

Honor-Bound Authentication

Every action is signed with Schnorr (BIP-340) — Bitcoin's own Taproot cryptography — and verified twice before becoming permanent.

1

Create Message

const message = `${address}:${to}:${amount}:${nonce}:transfer`;
โ†’
2

User Signs (Schnorr BIP-340)

schnorr.sign(sha256(message), privateKey);
โ†’
3

Send to API

{ address, signature, pubkey, message, ...data }
โ†’
4

API Verifies & Executes

schnorr.verify(sig, sha256(msg), pubkey)
โ†’
5

Block Producer Re-Verifies

// Every sig re-checked before sealing block
โ†’
6

Validator Anchors to Bitcoin

OP_RETURN KRAY + merkle_root_32bytes
โšก BLOCK ARCHITECTURE

512 MB Blocks — Bitcoin-Grade Security

The fastest Bitcoin L2. Every transaction is Schnorr-verified, Merkle-sealed, and anchored to L1.

512 MB
Max Block Size
~56,800
TPS at Full Capacity
850K+
TXs Per Block
32 bytes
Merkle Root on Bitcoin

Two-Layer Verification

Layer 1: API Verification

Every Schnorr signature is verified when the action is submitted. Invalid signatures are rejected instantly. Transaction executes and is recorded with signature + pubkey.

Layer 2: Block-Level Re-Verification

Before sealing a block, the block producer independently re-verifies every Schnorr signature. Only cryptographically valid TXs enter the Merkle tree. This is the gate.

Decentralized Anchoring

Once a block is sealed, any Guardian or Validator can pick it up and anchor the Merkle root to Bitcoin L1 by paying the OP_RETURN transaction fee. The validator has zero special power — the block is already mathematically complete. They cannot alter, reorder, or censor transactions. They simply write the 32-byte proof to Bitcoin.

Block Sealed
All sigs verified, Merkle built
โ†’
Validator Picks Up
Any guardian can anchor
โ†’
Pays L1 Fee
OP_RETURN to Bitcoin
โ†’
Anchored Forever
Immutable on Bitcoin

If one validator doesn't anchor, another can. The math enforces everything. The system is fully independent and decentralized.

DeFi tx_data Structure

For DeFi transactions (swap, add_liquidity, remove_liquidity, create_pool), the tx_data JSONB column stores both the signing parameters and execution result:

{
  "signing_params": {       // Original data used for Schnorr signing
    "from_token": "KRAY",
    "to_token": "RUNE_DOG",
    "amount": "1000"
  },
  "result": {               // Execution result for audit
    "received": "42.5",
    "price": "23.53",
    "fee": "1"
  }
}

The block producer uses signing_params to reconstruct and re-verify the exact message the user signed.

๐Ÿ“ก API REFERENCE

Endpoints

Base URL: https://api.kray.space

L2 Core

GET /l2/health Health check
GET /l2/account/:address Get balance
GET /l2/transaction/recent Recent TXs
GET /l2/transaction/:id TX details

Bridge

POST /l2/bridge/deposit Initiate deposit
POST /l2/bridge/withdraw Initiate withdrawal

Social

GET /api/social/feed Posts feed
POST /api/social/post/:id/like Like (1 KRAY)
POST /api/social/post/:id/repost Claim reward
๐Ÿ’ป CODE EXAMPLE

Game Reward Implementation

JavaScript Game Reward Function
async function giveGameReward(playerAddress, amount, gameData, signature, message) {
    // 1. Verify player signature (Honor-bound)
    if (!verifySignature(playerAddress, message, signature)) {
        throw new Error('Invalid signature');
    }
    
    // 2. Create unique TX ID (DNA of the action)
    const nonce = gameAccount.nonce || 0;
    
    // 4. Send L2 transfer (Truth Anchor) โ† THIS IS THE KEY!
    // Always use tx_type: 'transfer' โ€” custom info goes in data field
    const result = await fetch('https://kray.space/api/l2/transaction/send', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            from_account: 'GAME_TREASURY',
            to_account: playerAddress,
            amount: amount.toString(),
            tx_type: 'transfer',
            token_symbol: 'KRAY',
            signature: signature,
            pubkey: pubkey,
            nonce: nonce,
            data: JSON.stringify({ app: 'game', action: 'reward', ...gameData })
        })
    });
    
    return { success: true, txHash };
}
๐Ÿš€ BUILD ANYTHING

What Can You Create?

๐ŸŽฎ

Games

Play-to-earn, tournaments, in-game items, achievements

๐Ÿ’ฑ

DeFi

DEX, lending, staking, yield farming

๐Ÿ“ฑ

Social

Paid content, creator monetization, tipping

๐ŸŽจ

NFT

Marketplace, minting, auctions, royalties

๐Ÿ—ณ๏ธ

Governance

DAOs, voting, proposals, treasuries

๐Ÿ“„

Documents

Certificates, notarization, timestamps

๐ŸŽฐ

Lottery LIVE

Bitcoin blockhash draw ยท tickets ยท 10% creator fee ยท PWA L2

๐Ÿ“Š

Prediction Markets

Sports betting, event outcomes, binary options

๐ŸŽฌ

Streaming

Pay-per-view, subscriptions, live donations

๐ŸŽŸ๏ธ

Ticketing

Events, concerts, NFT tickets, resale market

๐Ÿ›’

E-Commerce

Digital goods, memberships, gift cards

๐Ÿค–

AI Services

Image generation, chatbots, content creation

๐Ÿ’ฐ DEVELOPER REVENUE

Build Apps. Earn Fees.

Every transaction = money in your wallet. No limits on what you can charge.

โˆž Set Any Fee You decide
โšก 1 KRAY Gas Fixed always
๐Ÿ’ณ Instant Payout Direct to wallet
๐Ÿ”’ NETWORK RULE

Fixed Gas Fee

The 1 KRAY gas is fixed and always goes to Treasury. Developers cannot change this. You only control your service fee.

1 GAS
+
? YOUR FEE
=
USER PAYS
01

๐Ÿ’ต Revenue Calculator

Simulate your earnings
Your Service Fee 10 KRAY
Daily Users 100
User Pays 11 KRAY
Daily 1,000
Monthly 30,000 KRAY
02

๐Ÿ“Š Fee Distribution

Where money goes
๐Ÿ‘ค
User 11 KRAY
๐Ÿ›๏ธ
Treasury Gas Fee
1 KRAY
๐Ÿ’ฐ
Your Wallet Service Fee
10 KRAY
Treasury bc1p5u00mj...k3nhv
You bc1p[your_address]
03

๐Ÿš€ Live on L2 KRAY

LIVE
๐ŸŽจ

Generative AI

NFT Generation10 KRAY
Avatar5 KRAY
๐Ÿ“‹ Copy Address bc1pxd5snpedtrkqgmngclr4jk76g2xaf3aacawe37gqjkpvw6jcwylscd45wn
LIVE
๐Ÿช

NFT Marketplace

Sale Commission2%
๐Ÿ“‹ Copy Address bc1prwz4jegt9l503elk07nq0c2a5mkavq8af40hwa4885g3s6q2h2eqkkdnfn
LIVE
๐ŸŽด

NFT Collections

Mint PriceDynamic
Owner receives mint fees โ†’ set by collection creator
LIVE
๐ŸŽฐ

Bitcoin Lottery

Create / Buy gas1 KRAY
Creator fee10%
Draw = blockhash % tickets (+3 confs) /lottery-l2 ยท LOTTERY_ESCROW

๐Ÿ“ Code Template

Copy this pattern to create your own L2 KRAY service:

JavaScript my-l2-service.js - Backend Pattern
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// YOUR L2 SERVICE CONFIGURATION
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// Your developer address - WHERE YOUR FEES GO
const MY_SERVICE_ADDRESS = 'bc1p[your_taproot_address]';

// Your service fee (you decide the price!)
const MY_SERVICE_FEE = 5;  // 5 KRAY per action

// Treasury address - ALWAYS the same for gas
const TREASURY_ADDRESS = 'bc1p5u00mjuxy0c040t9jdvcjxmzjsy2yluzukdzkta0fnu0hc5m29aqhk3nhv';

// Gas fee - ALWAYS 1 KRAY (network rule)
const GAS_FEE = 1;

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// YOUR SERVICE FUNCTION
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

async function executeMyService(userAddress, serviceData, signature, pubkey) {
    const totalRequired = MY_SERVICE_FEE + GAS_FEE;  // 5 + 1 = 6 KRAY
    
    // 1. Send the L2 transfer via the official API
    // tx_type is ALWAYS 'transfer' โ€” your custom info goes in data
    const txResult = await fetch('https://kray.space/api/l2/transaction/send', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            from_account: userAddress,
            to_account: MY_SERVICE_ADDRESS,
            amount: totalRequired.toString(),
            tx_type: 'transfer',
            token_symbol: 'KRAY',
            signature: signature,
            pubkey: pubkey,
            nonce: serviceData.nonce,
            data: JSON.stringify({
                app: 'my_service',
                action: 'service_action',
                ...serviceData
            })
        })
    });
    
    const tx = await txResult.json();
    if (!tx.tx_hash) throw new Error(tx.error || 'Transaction failed');
    
    // 2. Execute your service logic after payment confirmed
    const result = await doYourServiceLogic(serviceData);
    
    const txHash = tx.tx_hash;
    
    return { success: true, tx_hash: txHash, result };
}
JavaScript Frontend - User Signs Transaction
// Frontend: User pays for your service
async function useMyService(serviceData) {
    // 1. Get user's nonce
    const account = await fetch(`/api/l2/account/${userAddress}`);
    const nonce = account.nonce || 0;
    
    // 2. Create signature message
    // Format: from:to:amount:nonce:transfer (ALWAYS 'transfer')
    const MY_SERVICE_ADDRESS = 'bc1p[dev_address]';
    const totalFee = 6;  // 5 service + 1 gas
    const message = [
        userAddress,
        MY_SERVICE_ADDRESS,
        totalFee.toString(),
        nonce.toString(),
        'transfer'
    ].join(':');
    
    // 3. User signs with KRAY Wallet
    // (L2 transfer message โ†’ confirmation popup opens, user approves)
    const signResult = await window.krayWallet.signMessage(message);
    const pubkey = await window.krayWallet.getPublicKey();
    
    // 4. Call your service API
    const response = await fetch('/api/my-service/action', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            user_address: userAddress,
            service_data: serviceData,
            signature: signResult.signature,
            pubkey: pubkey,
            nonce: nonce
        })
    });
    
    return response.json();
}
๐Ÿš€ START NOW

Ready to Build?

4 steps to launch your earning service on L2 KRAY

1
๐Ÿ”‘

Get Your Address

Create a Taproot address (bc1p...) - this is where your fees go!

Use KRAY Wallet or any Bitcoin wallet
2
๐Ÿ’ก

Define Your Service

Decide what you're building and set your fee structure

๐ŸŽฎ Game: 5 KRAY/match ๐ŸŽจ AI Art: 10 KRAY/gen ๐Ÿ“Š Data: 2 KRAY/query
3
โšก

Implement the Pattern

Copy the code template above and customize for your app

MY_SERVICE_FEE = 10; MY_ADDRESS = "bc1p...";
โœ“
๐Ÿ’ฐ

Start Earning!

Every user action = KRAY in your wallet

100 users ร— 10 KRAY = 1,000 KRAY/day

Join the L2 KRAY developer community

๐Ÿ’ก Service Ideas: What Can You Build?

Here are real examples of services you can create on L2 KRAY. Each one uses the fee pattern above!

๐Ÿš€ More Service Ideas

Copy the fee pattern and adapt for any of these:

๐ŸŽฐ
Lottery / Raffle LIVE
Fee: 10% of pot

Native L2 lottery: winner = BigInt(blockhash) % tickets. Create/buy in KrayWallet PWA.

POST /api/lottery ยท +3 confs
๐Ÿƒ
Card Games
Fee: 2 KRAY/game

Poker, Blackjack, etc. Fixed fee per game session.

const GAME_FEE = 2;
๐Ÿ“ˆ
Staking Pool
Fee: 10% of yields

Users stake KRAY, earn rewards. You take cut of profits.

rewards * 0.10
๐Ÿ”ฎ
Prediction Market
Fee: 1% of bets

Bet on events (sports, crypto, elections). Fee on all bets.

betAmount * 0.01
๐ŸŽŸ๏ธ
NFT Raffle
Fee: 5 KRAY/ticket

Raffle off NFTs. Fixed fee per entry ticket.

const TICKET_FEE = 5;
๐Ÿค–
AI Chat Bot
Fee: 3 KRAY/message

Premium AI assistant. Pay per message or conversation.

const MESSAGE_FEE = 3;
๐Ÿ”
Premium Content
Fee: 10 KRAY/access

Exclusive content, tutorials, signals. Pay to unlock.

const ACCESS_FEE = 10;
โš”๏ธ
Multiplayer Games
Fee: 1 KRAY/match

PvP battles, tournaments. Fee per match entry.

const MATCH_FEE = 1;
๐Ÿ”จ
Auction House
Fee: 3% of sale

Auction NFTs or items. Fee on final sale price.

salePrice * 0.03
๐Ÿ›๏ธ
DAO Voting
Fee: 1 KRAY/vote

Governance voting system. Small fee per vote cast.

const VOTE_FEE = 1;
๐Ÿค
Escrow Service
Fee: 1% of deal

P2P trades with escrow protection. Fee on completion.

dealAmount * 0.01

๐Ÿ“ The Universal Formula

User Pays = YOUR_SERVICE_FEE + 1 KRAY (gas)
YOUR_SERVICE_FEE โ†’ Your Address
1 KRAY โ†’ Treasury

That's it! Set your fee, implement the pattern, and start earning.

๐ŸŽจ NFT L2 SYSTEM

Create NFT Collections on L2 KRAY

Mint NFTs instantly on L2, bridge to Bitcoin when ready

โœ…

Step 1: Become a Verified Project

To create NFT collections on L2 KRAY, your project needs to be verified first. This ensures quality and protects users.

๐ŸŽจ Create unlimited NFTs
โšก Instant minting (no waiting)
๐ŸŒ‰ Bridge to Bitcoin Ordinals
๐Ÿ’ฐ Only 1 KRAY per mint

Send via KrayChat: Project name, your bc1p... address, and brief description.
Already verified? Open NFT Studio to create your collection!

๐Ÿ’ฐ Understanding NFT Fees

GAS Network Fee: 1 KRAY

Cost to create/inscribe NFT on L2. Always goes to Treasury.

You pay this when creating each NFT.

PRICE Mint Price: You Decide

Your product price. Goes to your wallet.

Set 0 for free mint, or any value you want.

Example: You set mint_price = 100 KRAY

Client pays:101 KRAY total
โ†’ 100 KRAYYour wallet (mint_price)
โ†’ 1 KRAYTreasury (network gas)
NFT Status Types
ACTIVE     โ†’ NFT lives on L2, can transfer or bridge
BRIDGED    โ†’ NFT inscribed on Bitcoin L1, disabled on L2
RETURNED   โ†’ NFT came back from Bitcoin to L2
INCUBATOR  โ†’ Inscription exists on Bitcoin, never came to L2

Step 2: Mint NFTs via API

1

Upload to IPFS

Upload your image/video/audio (max 4MB)

Allowed: jpg, png, webp, avif, mp4, mp3, gltf
2

Sign with Wallet

Sign a message to prove you own the project

3

Call Mint API

POST to /api/l2/nfts/mint

4

NFT Created!

Appears in recipient's Inventory

Step 2: Configure Your Collection

1

Set Mint Price

How much clients pay you per NFT

0 = Free mint
2

Set Max Supply

Limit how many can be minted

0 = Unlimited
3

Enable/Disable

Control when clients can mint

NFT L2 API Endpoints
// Get collection info (mint_price, supply, etc)
GET /api/l2/nfts/collection/:id

// Configure your collection (owner only)
POST /api/l2/nfts/collection/config
{
  collection_id: "your_project_id",
  mint_price: "100",      // KRAY - goes to YOU
  max_supply: 1000,       // 0 = unlimited
  mint_enabled: true,     // allow public minting?
  signature: "...",
  nonce: 0,
  pubkey: "..."
}

// List user's NFTs
GET /api/l2/nfts/:address

// Mint new NFT (verified projects only)
// Developer mint: pays 1 KRAY (gas)
// Public mint: pays mint_price + 1 KRAY
POST /api/l2/nfts/mint
{
  collection_id: "your_project_id",
  name: "NFT Name",
  description: "Description",
  file_url: "ipfs://Qm...",
  file_type: "image",
  file_size: 150000,
  metadata: { rarity: "LEGENDARY", ... },
  recipient_address: "bc1p...",
  minter_address: "bc1p...",  // who is paying
  signature: "schnorr_signature",
  nonce: 0,
  pubkey: "your_pubkey"
}

// Transfer NFT on L2
POST /api/l2/nfts/transfer
{
  nft_id: "uuid",
  to_address: "bc1p...",
  signature: "...",
  nonce: 1,
  pubkey: "..."
}

// Inscribe on Bitcoin (NFT born on L2)
POST /api/l2/nfts/:id/inscribe

// Bridge to L1 (NFT already has inscription)
POST /api/l2/nfts/:id/bridge-to-l1

// Bridge from Bitcoin to L2
POST /api/l2/nfts/bridge-to-l2
JavaScript Example: Mint NFT
async function mintNFT() {
  // 1. Get nonce from account
  const account = await fetch(`/api/l2/account/${myAddress}`);
  const nonce = account.nonce || 0;
  
  // 2. Sign message with KrayWallet
  // (financial actions open the confirmation popup โ€” the user approves)
  const message = `Mint NFT to ${recipientAddress} at ${Date.now()}`;
  const signature = await window.krayWallet.signMessage(message);
  const pubkey = await window.krayWallet.getPublicKey();
  
  // 3. Call mint API
  const response = await fetch('/api/l2/nfts/mint', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      collection_id: 'my_project',
      name: 'My NFT #1',
      description: 'An amazing NFT',
      file_url: 'ipfs://QmXxx...',
      file_type: 'image',
      file_size: 150000,
      metadata: { rarity: 'EPIC' },
      recipient_address: recipientAddress,
      signature,
      nonce,
      pubkey
    })
  });
  
  const result = await response.json();
  console.log('NFT minted!', result.nft);
}

After NFT Exists on L2

๐Ÿ”„

Transfer

Send to another address instantly

1 KRAY fee
โšก

Inscribe on Mainnet

Create Ordinal on Bitcoin (first time)

1 KRAY + sats
๐ŸŒ‰

Bridge to L1

Move existing Ordinal back to Bitcoin

1 KRAY + sats

๐ŸŽซ Turn your collection into a Live License

Gate any product (VIP room, game skin, course, club) on current possession of your L2 NFT or L1 inscription. User connects with KRAY Wallet โ†’ your backend proves the key (Schnorr) โ†’ reads GET /api/l2/nfts/:address (only ACTIVE) โ†’ grants access. They sell the NFT โ†’ access moves with it. No new L2 protocol โ€” app-layer pattern only.

Copy the Live NFT License template โ†’

๐Ÿš€ What Can You Build with NFT L2?

NFT L2 is perfect for high-volume, low-cost use cases. Create thousands of NFTs without breaking the bank, then bridge special ones to Bitcoin.

๐ŸŽŸ๏ธ

Tickets & Events

  • Concert & festival tickets
  • Sports game passes
  • Conference badges
  • VIP memberships
Instant transfer, 1 KRAY/ticket
๐ŸŽฎ

Gaming & RPG

  • In-game items (swords, armor)
  • Character skins & avatars
  • Trading cards
  • Achievements & badges
100k+ items, pennies each
๐ŸŽจ

Digital Art & Collectibles

  • 10k+ PFP collections
  • Generative art series
  • POAPs (Proof of Attendance)
  • Limited editions
Bridge rare ones to Bitcoin
๐Ÿ“œ

Certificates & Credentials

  • Course completions
  • Professional licenses
  • Diplomas & degrees
  • Membership cards
Verifiable, transferable
๐Ÿ’ก
The Power of L2 + L1: Create millions of NFTs on L2 for pennies, then bridge the rare/special ones to Bitcoin as Ordinals when they deserve permanence.
โšก DEV SCROLL

Developer Reward System

Create reward-backed tasks, get a scroll_key, and integrate with your website

OVERVIEW

Dev Scroll lets authorized developers create reward-backed tasks on KRAY L2.

How it works:

  1. Open Studio โ†’ Dev Studio tab
  2. Create a scroll (title, reward, max claims)
  3. Sign with your wallet, KRAY goes to ESCROW
  4. Receive a unique scroll_key
  5. Embed scroll_key in your backend
  6. When users complete tasks, call the claim API
  7. ESCROW releases KRAY to the user automatically
API ENDPOINTS
POST /api/dev-scroll/create โ€” Create scroll, get key (supports multi-token + dynamic mode)
POST /api/dev-scroll/claim โ€” User signs, ESCROW pays reward (fixed or dynamic amount)
POST /api/dev-scroll/fund โ€” External funding: anyone adds tokens to scroll pool (sponsor recycling)
POST /api/dev-scroll/edit โ€” Top-up pool / change reward (contract scrolls: top-up only)
POST /api/dev-scroll/remove โ€” Remove scroll, return pool (blocked for contract escrows)
POST /api/dev-scroll/toggle โ€” Enable/disable scroll (signature only)
GET /api/dev-scroll/info/:key โ€” Pool status (includes pool_token, scroll_mode, is_contract)
GET /api/dev-scroll/my-scrolls/:addr โ€” Your scrolls (scroll_key redacted)
POST /api/dev-scroll/my-scrolls โ€” Your scrolls with full keys (signed: scroll_list:addr:ts)
GET /api/dev-scroll/check-claim/:key/:addr โ€” Check claim

Multi-Token + Dynamic Escrow (2026-05-10)

pool_token: KRAY (default), RADIOLA, DOG, DSC โ€” which token the pool holds
scroll_mode: "fixed" (reward_per_claim set at creation) or "dynamic" (amount per claim from API)
escrow_address: Required for non-KRAY tokens. Real taproot (bc1p...) wallet holding the pool
is_contract: Auto-set when escrow_address provided. Immutable terms โ€” funds only leave via claims. No remove/withdraw.
Gas fee: 1 KRAY always charged in KRAY (create, remove, and claim for non-KRAY tokens)
Amounts (create/edit): reward_per_claim, pool_amount, add_to_pool, new_reward_per_claim are HUMAN amounts (e.g. 500 = 500 DOG, decimals allowed). The server applies the rune divisibility automatically. Only the dynamic /claim amount stays in RAW units.

Claim Modes

When creating a scroll, choose how claims are handled:

Open (default)

Same wallet can claim multiple times. Each claim is an independent event. Pool balance and max_claims are the only limits. Perfect for: real-time drops, games, comets, quizzes, recurring rewards.

Single

One claim per wallet. Once a user claims, they cannot claim again from this scroll. Perfect for: one-time rewards, NFT drop access, beta testing, unique airdrops.

Pass claim_mode: "open" or claim_mode: "single" when creating via API. Default is "open". Can be changed later via POST /edit with new_claim_mode.

๐Ÿ›ก๏ธ Security Best Practices

Reward pools are real funds. The platform enforces several anti-abuse layers on every claim โ€” a per-claim 1 KRAY gas fee, a Sybil funding-cluster detector (blocks coordinated multi-wallet farming), an optimistic concurrency lock (no double-spend via races), and hard pool / max_claims caps that bound the maximum possible loss. Follow these to stay safe:

  • Keep your scroll_key server-side only โ€” store it in your backend .env, never in client-side code. The key is the authorization to pay out; anyone who reads it can trigger claims. KRAYโ€ขSPACE never exposes it (public reads are redacted; the full key is only returned to the creator via a signed request).
  • Size max_claims to your budget โ€” it is the hard ceiling on total payout (max_claims ร— reward_per_claim). Nothing can drain more than this, so set it deliberately.
  • Use single claim mode when each user should claim only once โ€” it stops one wallet from claiming repeatedly.
  • Validate the task on your backend before calling /claim โ€” confirm the user actually completed the action (game win, quiz pass, etc.). The signature proves wallet control, not task completion โ€” that check is yours.
  • Optional: restrict to a whitelist โ€” for closed betas / allow-lists, pin the scroll to specific taproot addresses (see below). Even if the scroll_key leaks, only listed addresses can claim.

โœ… Optional Whitelist (off by default โ€” additive, opt-in)

A scroll can OPTIONALLY be locked to an explicit set of taproot (bc1p...) addresses. If you don't set one, the scroll stays open to everyone, exactly as before โ€” nothing changes. When a whitelist is set, any address not on it is rejected with 403 NOT_WHITELISTED before any gas or slot is spent. This is a defense-in-depth layer on top of the gas fee, Sybil detector and max_claims cap: even a leaked scroll_key can't drain the pool to outsiders.

Set at creation

POST /api/dev-scroll/create
{
    ...,                        // all the usual create fields
    "whitelist": [              // OPTIONAL โ€” omit for an open scroll
        "bc1p...addrA",
        "bc1p...addrB"
    ]
}
// Addresses are trimmed, lowercased and de-duplicated server-side.
// Response includes: "whitelist_enabled": true, "whitelist": [...]

Add / remove members later (dynamic, creator-signed)

POST /api/dev-scroll/edit
{
    "scroll_key": "your_scroll_key",
    "address": "bc1p...creator",
    "pubkey": "creator_pubkey_hex",
    "signature": "...",
    "message": "scroll_edit:scroll_key_prefix:address:timestamp",

    "whitelist_add":    ["bc1p...addrC"],   // OPTIONAL โ€” grant access
    "whitelist_remove": ["bc1p...addrA"]    // OPTIONAL โ€” revoke access
}
// Remove is applied first, then add. Removing the last member reverts the
// scroll to OPEN. Whitelist edits are allowed even on contract escrows
// (they change WHO can claim, never the economic terms).

Privacy: GET /info/:key exposes only whitelist_enabled + whitelist_count (never the member list). The full list is returned only to the creator via the signed POST /my-scrolls.

STEP-BY-STEP Complete Flow: Create, Integrate, Manage

PHASE 1 โ€” Create Your Scroll (Dev Studio)

1. Open kray.space/nft-studio and connect your KrayWallet

2. Click the Dev Studio tab (your wallet must be whitelisted in l2_authorized_creators)

3. Fill the form:

Title โ€” Name of your reward (e.g. "Test Body Scanner")
Description โ€” What users need to do
Website URL โ€” Link to your app/feature
Image URL โ€” Logo or preview (optional)
Claim Mode โ€” Choose how claims work:
Open (default) โ€” Same wallet can claim multiple times. For drops, games, recurring events.
Single โ€” One claim per wallet. For one-time rewards, NFT drops, beta access.

Reward per Claim โ€” KRAY each user receives (e.g. 5)
Max Claims โ€” Total claims allowed across all users (e.g. 100)

4. Cost preview: 1 KRAY gas + (reward × claims) = total

5. Click "Create Dev Scroll" โ€” sign with KrayWallet

6. Your KRAY goes to ESCROW. You receive a scroll_key (64 hex chars). Copy it. Keep it secret.

PHASE 2 โ€” Integrate on Your Website

1. Store scroll_key in your backend as an environment variable. NEVER put it in frontend code.

2. Add a "Claim Reward" button on your website where users interact

3. When user clicks the button, call window.krayWallet.signMessageWithConfirmation() (recommended โ€” always shows popup) or signMessage() (auto-signs if unlocked). Both produce the same Schnorr signature.

4. Send the user's signature to your backend (not directly to Kray API)

5. Your backend attaches the scroll_key and forwards to POST /api/dev-scroll/claim

6. ESCROW automatically releases KRAY to the user. L2 transaction recorded. Done.

PHASE 3 โ€” Manage & Monitor (Dev Studio Dashboard)

1. Back in Dev Studio, the "MY SCROLLS" panel shows all your scrolls

2. Each card displays: pool remaining, claims completed, reward per claim, claim mode badge, and your scroll_key

3. Click "Edit" to top-up the pool, change reward amount, add more claim slots, or switch claim mode

4. From your website, call GET /api/dev-scroll/info/:key to show real-time pool status to users

5. Call GET /api/dev-scroll/check-claim/:key/:addr to check if a user already claimed

JAVASCRIPT Integration Example โ€” Frontend (User Signs) + Backend (scroll_key)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// FRONTEND (on developer's website) โ€” User clicks "Claim"
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// User connects KrayWallet on your site, then clicks claim button
async function onClaimButtonClick() {
    // 1. Get the bc1p... taproot address (MUST be the full Bitcoin address)
    const accounts = await window.krayWallet.getAccounts();
    const userAddress = accounts[0]; // e.g. 'bc1pxd5snpe...' (full taproot address)

    // 2. Get x-only public key (hex string, 32 bytes = 64 hex chars)
    //    getPublicKey() returns a plain string, NOT an object
    const userPubkey = await window.krayWallet.getPublicKey();

    // 3. Build message using the bc1p... address (NOT the hex pubkey!)
    const timestamp = Date.now();
    const message = `scroll_claim:reward:${userAddress}:${timestamp}`;

    // 4. User signs with their wallet (Schnorr signature)
    //    Option A (recommended): ALWAYS shows popup โ€” user confirms with password
    const result = await window.krayWallet.signMessageWithConfirmation(message);
    //    Option B: Auto-signs if wallet unlocked (no popup). Use only for non-value actions.
    // const result = await window.krayWallet.signMessage(message);

    // 5. Send to YOUR backend (not directly to Kray API!)
    const resp = await fetch('/api/claim-reward', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            user_address: userAddress,   // bc1p... (full Bitcoin address)
            user_pubkey: userPubkey,     // x-only hex pubkey (64 chars)
            user_signature: result.signature,
            user_message: message
        })
    });
    const data = await resp.json();
    // data = { success: true, tx_id: '...', reward: 5 }
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// BACKEND (your server) โ€” Forwards to Kray API with scroll_key
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

const SCROLL_KEY = 'your_secret_scroll_key'; // Never expose in frontend!

app.post('/api/claim-reward', async (req, res) => {
    const { user_address, user_pubkey, user_signature, user_message } = req.body;

    // Call Kray API with your secret scroll_key + user's signature + pubkey
    const response = await fetch('https://kray.space/api/dev-scroll/claim', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            scroll_key: SCROLL_KEY,
            user_address,
            user_pubkey,
            user_signature,
            user_message
        })
    });

    const result = await response.json();
    // result = { success: true, tx_id: '...', reward: 5, claims_remaining: 94 }
    res.json(result);
});

// Check pool status anytime
async function getPoolStatus() {
    const resp = await fetch(`https://kray.space/api/dev-scroll/info/${SCROLL_KEY}`);
    return await resp.json();
    // { pool_remaining: 470, claims_completed: 6, claims_remaining: 94, ... }
}
SECURITY

scroll_key is SECRET โ€” Never expose it in frontend JavaScript. Always call from your backend.

User signs to claim โ€” The user (not the developer) signs with their KrayWallet to prove wallet ownership. The scroll_key proves developer authorization. Two-factor validation.

One claim per user โ€” Each user can only claim once per scroll (enforced by database constraint).

ESCROW-backed โ€” All KRAY is locked in DEV_ESCROW at creation. Released only on validated claims with Schnorr signature verification.

MANAGE Edit, Top-Up & Monitor Your Scrolls

Dev Scrolls are designed for permanent features on your website. You can always top-up the pool, change the reward, or add more claim slots without creating a new scroll.

Via Dev Studio (UI)

Click the "Edit" button on any scroll card in the MY SCROLLS dashboard. The modal lets you add KRAY to the pool, change reward per claim, and increase max claims.

Via API

POST /api/dev-scroll/edit

{
    "scroll_key": "your_scroll_key",
    "address": "bc1p...your_address",
    "pubkey": "your_public_key_hex",
    "signature": "...",
    "message": "scroll_edit:bc1p...:timestamp",

    // Any combination of these (all optional):
    "add_to_pool": 500,         // Add 500 KRAY to DEV_ESCROW pool
    "new_reward_per_claim": 10, // Change reward from 5 to 10 KRAY
    "add_max_claims": 50,       // Allow 50 more users to claim
    "whitelist_add": ["bc1p..."],    // Optional โ€” grant claim access
    "whitelist_remove": ["bc1p..."]  // Optional โ€” revoke claim access
}

// Response:
{
    "success": true,
    "scroll": { "total_pool": 1000, "pool_remaining": 970, ... }
}

Monitor from Your Website

GET /api/dev-scroll/info/YOUR_SCROLL_KEY

// Response:
{
    "title": "Test Body Scanner",
    "reward_per_claim": 5,
    "max_claims": 100,
    "claims_completed": 23,
    "claims_remaining": 77,
    "pool_remaining": 385,
    "total_pool": 500,
    "claim_mode": "open",
    "is_active": true,
    "whitelist_enabled": false,  // true if the scroll is restricted
    "whitelist_count": 0         // number of allowed addresses (list not exposed publicly)
}

// Check if a user already claimed:
GET /api/dev-scroll/check-claim/YOUR_SCROLL_KEY/bc1p...user_address

// Response:
{ "claimed": false }

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// REMOVE scroll (returns pool to developer, gas fee)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
POST /api/dev-scroll/remove

{
    "scroll_key": "your_scroll_key",
    "address": "bc1p...your_address",
    "pubkey": "your_public_key_hex",
    "signature": "...",
    "message": "dev_scroll_remove:scroll_key_prefix:address:timestamp"
}

// Response:
{
    "success": true,
    "pool_returned": 500,
    "gas_fee": 1,
    "net_returned": 499
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// TOGGLE scroll (enable/disable, signature only)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
POST /api/dev-scroll/toggle

{
    "scroll_key": "your_scroll_key",
    "address": "bc1p...your_address",
    "pubkey": "your_public_key_hex",
    "signature": "...",
    "message": "scroll_edit:toggle_disable:scroll_key_prefix:address:timestamp"
}

// Response:
{ "success": true, "is_active": false }

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// FUND scroll (external top-up, e.g. sponsor recycle)
// Anyone can fund a scroll's pool. Requires sender's
// wallet signature. No gas fee on fund.
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
POST /api/dev-scroll/fund

{
    "scroll_key": "your_scroll_key",
    "from_address": "bc1p...sender_address",
    "amount": 500,
    "pubkey": "sender_public_key_hex",
    "signature": "...",
    "message": "scroll_fund:scroll_key_prefix:amount:sender_address:timestamp"
}

// Response:
{
    "success": true,
    "pool_token": "RADIOLA",
    "amount_funded": 500,
    "new_pool_remaining": 1500,
    "new_total_pool": 2000,
    "tx_id": "hash..."
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// DYNAMIC CLAIM (for scroll_mode: "dynamic")
// Same endpoint as regular claim, but with amount field
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
POST /api/dev-scroll/claim

{
    "scroll_key": "your_scroll_key",
    "user_address": "bc1p...user",
    "user_pubkey": "user_public_key",
    "user_signature": "...",
    "user_message": "scroll_claim:reward:address:timestamp",
    "amount": 42
}

// IMPORTANT: "amount" is in RAW units (human amount ร— 10^divisibility).
// Divisibility per token: KRAY=0, DSC=0, RADIOLA=2, DOG=5, SATSPACE=3.
// e.g. to pay 0.5 DOG send amount: 50000; to pay 5 RADIOLA send amount: 500.

// Gas: every claim pays 1 KRAY.
//   non-KRAY scrolls: charged upfront from the user's KRAY balance.
//   KRAY scrolls: netted from the reward (reward_net = reward - 1),
//   so users with 0 balance can still claim. KRAY rewards must be > 1.
// Response:
{
    "success": true,
    "tx_id": "hash...",
    "reward": 42,
    "reward_net": 42,
    "reward_token": "RADIOLA",
    "gas_fee": 1,
    "claims_remaining": 999999957,
    "pool_remaining": 958
}
โš”๏ธ

The Builder's Oath

I. I create Truth Anchors for every action.

II. I verify signatures before moving value.

III. I derive counters from TXs, not increments.

IV. I protect user keys โ€” they never leave the device.

V. I let the Code speak โ€” it is my sworn oath.

Ready to Build?

Join the Origin Builders and create the future of Bitcoin scaling.