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.
Each user action creates a Truth Anchor (TX L2) that can be verified forever.
Every TX that moves KRAY must be signed by the user. Honor-bound.
Counters and balances are derived from TXs. Never increment directly.
Users can always withdraw to Bitcoin L1. This is a right, not a feature.
Simple, predictable, and fair. No variable gas, no surprises.
bc1p5u00...k3nhv
Click any category to see all transactions
Network fee is separate. Charge whatever you want for your service!
// 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();
Copy-paste examples for everything your app needs.
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 }
getAccounts()
Get all accounts (array)
const accounts = await window.krayWallet.getAccounts();
// Returns ['bc1p...'] โ array with full Bitcoin taproot address
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 }
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);
signMessage(message) USE WITH CAUTION
Auto-sign โ If wallet is already unlocked, signs silently without popup. The user does NOT see or confirm anything. If locked, falls back to popup.
Only for actions that do NOT move KRAY or tokens: login, identity verification, read-only proofs. The user has no chance to review or cancel.
const { signature, address } = await window.krayWallet.signMessage('verify:login:timestamp');
// Returns { signature: '64-byte hex', address: 'bc1p...' }
// โ ๏ธ If wallet is unlocked, signs INSTANTLY โ no popup, no password, no confirmation
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() reuses the cached session password silently, while signMessageWithConfirmation() always requires the user to type their password and confirm. The backend cannot distinguish between them.
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.
sendPayment(invoice)
Pay Lightning invoice
const result = await window.krayWallet.sendPayment('lnbc100n1...');
if (result.success) {
console.log('Paid! Preimage:', result.preimage);
}
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
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);
});
Use Bitcoin L1 for permanence and authenticity, L2 KRAY for fast interactions.
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
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);
});
Building a marketplace, DeFi protocol, or advanced Bitcoin application?
We offer extended API access for approved partners including:
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'
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();
Collectible card game: Cards as Ordinals (L1), game on L2.
Every transaction must follow this 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"
}
All transaction types available on L2 KRAY - use these exact strings in your code
Click any type to copy to clipboard
transfer
deposit
withdrawal
nft_list
nft_unlist
nft_buy
bc1prwz4jegt9l503elk07nq0c2a5mkavq8af40hwa4885g3s6q2h2eqkkdnfn
nft_mint
nft_transfer
nft_bridge_out
nft_bridge_in
create_collection
collection_config
public_ai_mint
ai_generate
bc1pgwfq97qpu06lz5a3xe02dp0t5sw5dl6qcnwftlyyvajdk8cdtcwq7qasux
Build any app on L2 KRAY! Always use tx_type: 'transfer' and put your custom action in the data field as JSON.
transfer
transfer
transfer
transfer
// 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)
});
Every action is signed with Schnorr (BIP-340) — Bitcoin's own Taproot cryptography — and verified twice before becoming permanent.
const message = `${address}:${to}:${amount}:${nonce}:transfer`;
schnorr.sign(sha256(message), privateKey);
{ address, signature, pubkey, message, ...data }
schnorr.verify(sig, sha256(msg), pubkey)
// Every sig re-checked before sealing block
OP_RETURN KRAY + merkle_root_32bytes
The fastest Bitcoin L2. Every transaction is Schnorr-verified, Merkle-sealed, and anchored to L1.
Every Schnorr signature is verified when the action is submitted. Invalid signatures are rejected instantly. Transaction executes and is recorded with signature + pubkey.
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.
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.
If one validator doesn't anchor, another can. The math enforces everything. The system is fully independent and decentralized.
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.
Base URL: https://api.kray.space
/l2/health
Health check
/l2/account/:address
Get balance
/l2/transaction/recent
Recent TXs
/l2/transaction/:id
TX details
/l2/bridge/deposit
Initiate deposit
/l2/bridge/withdraw
Initiate withdrawal
/api/social/feed
Posts feed
/api/social/post/:id/like
Like (1 KRAY)
/api/social/post/:id/repost
Claim reward
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 };
}
Play-to-earn, tournaments, in-game items, achievements
DEX, lending, staking, yield farming
Paid content, creator monetization, tipping
Marketplace, minting, auctions, royalties
DAOs, voting, proposals, treasuries
Certificates, notarization, timestamps
Raffles, jackpots, number draws, prize pools
Sports betting, event outcomes, binary options
Pay-per-view, subscriptions, live donations
Events, concerts, NFT tickets, resale market
Digital goods, memberships, gift cards
Image generation, chatbots, content creation
The 1 KRAY gas is fixed and always goes to Treasury. Developers cannot change this. You only control your service fee.
bc1p5u00mj...k3nhv
bc1p[your_address]
bc1pgwfq97qpu06lz5a3xe02dp0t5sw5dl6qcnwftlyyvajdk8cdtcwq7qasux
bc1prwz4jegt9l503elk07nq0c2a5mkavq8af40hwa4885g3s6q2h2eqkkdnfn
โ set by collection creator
Copy this pattern to create your own L2 KRAY service:
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 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 };
}
// 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
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();
}
4 steps to launch your earning service on L2 KRAY
Create a Taproot address (bc1p...) - this is where your fees go!
Decide what you're building and set your fee structure
Copy the code template above and customize for your app
MY_SERVICE_FEE = 10;
MY_ADDRESS = "bc1p...";
Every user action = KRAY in your wallet
Join the L2 KRAY developer community
Here are real examples of services you can create on L2 KRAY. Each one uses the fee pattern above!
A peer-to-peer betting system where users can bet KRAY against each other. The developer takes a 3% fee from the pot when bets are resolved.
| Daily Bets | Avg Pot | Fee 3% | Daily Earnings |
|---|---|---|---|
| 50 bets | 100 KRAY | 3 KRAY | 150 KRAY/day |
| 100 bets | 200 KRAY | 6 KRAY | 600 KRAY/day |
| 500 bets | 500 KRAY | 15 KRAY | 7,500 KRAY/day |
// KRAY Bet Configuration
const KRAY_BET_ADDRESS = 'bc1p[your_bet_service_address]';
const BET_FEE_PERCENT = 3; // 3% of pot
async function resolveBet(betId, winnerId, signature, pubkey) {
const bet = await getBet(betId);
const pot = BigInt(bet.player_a_amount) + BigInt(bet.player_b_amount);
// Calculate fees
const serviceFee = pot * BigInt(BET_FEE_PERCENT) / 100n;
const winnerReceives = pot - serviceFee;
const winnerAddress = winnerId === 'a' ? bet.player_a : bet.player_b;
// 1. Credit winner (194 KRAY)
await creditAccount(winnerAddress, winnerReceives);
// 2. Credit YOUR address (6 KRAY - 3% fee)
await creditAccount(KRAY_BET_ADDRESS, serviceFee);
// 3. Credit Treasury (1 KRAY gas)
await creditAccount(TREASURY_ADDRESS, GAS_FEE);
return { success: true, winner_receives: winnerReceives };
}
Copy the fee pattern and adapt for any of these:
Users buy tickets, random winner takes pot minus your fee.
ticketPrice * totalTickets * 0.05
Poker, Blackjack, etc. Fixed fee per game session.
const GAME_FEE = 2;
Users stake KRAY, earn rewards. You take cut of profits.
rewards * 0.10
Bet on events (sports, crypto, elections). Fee on all bets.
betAmount * 0.01
Raffle off NFTs. Fixed fee per entry ticket.
const TICKET_FEE = 5;
Premium AI assistant. Pay per message or conversation.
const MESSAGE_FEE = 3;
Exclusive content, tutorials, signals. Pay to unlock.
const ACCESS_FEE = 10;
PvP battles, tournaments. Fee per match entry.
const MATCH_FEE = 1;
Auction NFTs or items. Fee on final sale price.
salePrice * 0.03
Governance voting system. Small fee per vote cast.
const VOTE_FEE = 1;
P2P trades with escrow protection. Fee on completion.
dealAmount * 0.01
User Pays = YOUR_SERVICE_FEE + 1 KRAY (gas)
That's it! Set your fee, implement the pattern, and start earning.
Mint NFTs instantly on L2, bridge to Bitcoin when ready
To create NFT collections on L2 KRAY, your project needs to be verified first. This ensures quality and protects users.
Send via KrayChat: Project name, your bc1p... address, and brief description.
Already verified? Open NFT Studio to create your collection!
Cost to create/inscribe NFT on L2. Always goes to Treasury.
You pay this when creating each NFT.
Your product price. Goes to your wallet.
Set 0 for free mint, or any value you want.
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
Upload your image/video/audio (max 4MB)
Allowed: jpg, png, webp, avif, mp4, mp3, gltf
Sign a message to prove you own the project
POST to /api/l2/nfts/mint
Appears in recipient's Inventory
How much clients pay you per NFT
0 = Free mint
Limit how many can be minted
0 = Unlimited
Control when clients can mint
// 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
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
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);
}
Send to another address instantly
1 KRAY feeCreate Ordinal on Bitcoin (first time)
1 KRAY + satsMove existing Ordinal back to Bitcoin
1 KRAY + satsNFT L2 is perfect for high-volume, low-cost use cases. Create thousands of NFTs without breaking the bank, then bridge special ones to Bitcoin.
Create reward-backed tasks, get a scroll_key, and integrate with your website
Dev Scroll lets authorized developers create reward-backed tasks on KRAY L2.
How it works:
scroll_keyPOST /api/dev-scroll/create โ Create scroll, get keyPOST /api/dev-scroll/claim โ User signs, ESCROW pays rewardPOST /api/dev-scroll/edit โ Top-up pool / change rewardPOST /api/dev-scroll/remove โ Remove scroll, return pool to dev (gas fee)POST /api/dev-scroll/toggle โ Enable/disable scroll (signature only)GET /api/dev-scroll/info/:key โ Pool statusGET /api/dev-scroll/my-scrolls/:addr โ Your scrollsGET /api/dev-scroll/check-claim/:key/:addr โ Check claim
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.
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:
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
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 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, ... }
}
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.
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
}
// 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
}
// 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 }
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.
Join the Origin Builders and create the future of Bitcoin scaling.