Overview
Qwai is a proof-of-stake layer 1 blockchain built without elliptic-curve cryptography. Every account and validator signs with post-quantum keys, blocks are final as soon as they are made, and smart contracts are Rust programs run on a RISC-V virtual machine.
| Coin | QWAI. Six decimals: 1 QWAI is 1,000,000 smallest units. |
| Supply | 1,000,000,000 QWAI, fixed. No inflation. |
| Consensus | Proof of stake, Byzantine fault tolerant. One block to finality. |
| Signatures | ML-DSA-65 (FIPS 204). Optional second key: SLH-DSA (FIPS 205). |
| Hashing | BLAKE3. |
| Contracts | Rust, compiled to RISC-V (RV32IM), gas metered. |
| Addresses | 32 bytes, written as 64 hex characters, no prefix. |
| Public node | https://rpc.qwai.io/api |
| Explorer | explorer.qwai.io |
Get started
- Install Qwail. Download the wallet from qwail.org and add it to Chrome.
- Create a wallet. Qwail shows 24 recovery words. Write them down and keep them offline. Anyone with the words controls the wallet, and nobody can restore it without them.
- Receive QWAI. Press Receive and share your address.
- Use apps. On Qwaix or any Qwai app, press Connect. Every request from an app is shown to you in Qwail before anything is signed.
Accounts and keys
An account is not one key. It is a rule: "any k of these n keys must sign". The address is a hash of the first rule the account was made with, and it never changes afterwards.
Two-key protection
An account can hold a lattice key (ML-DSA) and a hash-based key (SLH-DSA) and require both on every transaction. The two rest on unrelated mathematics, so an attacker has to break both. Turn it on in Qwail under Settings. Two-key transactions are about three times larger and take about a second to sign.
Changing keys
Keys can be replaced or added at any time with one transaction. The address, balance, tokens and NFTs stay exactly where they are. If a signature scheme is ever weakened, accounts move to new keys without moving funds.
Fees
A transaction pays one smallest unit of QWAI per byte, plus gas if it runs a contract (1,000 gas per smallest unit). A plain transfer costs about 0.0034 QWAI; a token trade about 0.004. Fees go to the validator that proposes the block.
Validators and staking
Validators take turns proposing blocks in proportion to their stake, and a block is final once validators holding more than two thirds of all stake have signed it.
Run a validator
- Download the Qwai Node app from Run a node and open it. It joins the network and verifies the whole chain.
- In the app, create or import a wallet holding at least 10,000 QWAI.
- Press Stake. Your computer starts signing blocks in the next block.
| Smallest stake | 10,000 QWAI |
| Rewards | The fees of every block you propose. |
| Withdrawing | Stake unlocks 14 days after you stop validating. |
| Going offline | After 200 blocks in a row without your signature, your validator is set aside. Your stake is untouched; press Rejoin in the app. |
| Signing twice | Signing two different blocks at one height burns 5% of the stake and removes the validator for good. Never run the same validator on two computers. |
Stake behind a validator
You do not need to run a computer to stake. Any amount from 1 QWAI can be staked behind a validator that takes delegations. Your coins add to its voting power and earn a share of the fees from every block it proposes. Rewards are added to your stake automatically.
- Open the Validators page and connect Qwail.
- Press Stake next to a validator, enter an amount and approve it in the wallet.
- To leave, press Unstake. The coins can be collected 14 days later.
| Smallest amount | 1 QWAI |
| Rewards | Your share of the validator's block fees, less its commission. Qwai creates no new coins, so rewards come only from fees. |
| Commission | Set by the validator. It can be lowered at any time and raised by at most 1% a day. |
| Unstaking | 14 days. Coins that are unstaking earn nothing. |
| Risk | If the validator signs two different blocks at one height, 5% of everything staked behind it is burned, yours included. Coins still unstaking at that moment are included. |
| If the validator leaves | Your coins stop earning and are free when the validator's own are. Press Unstake to take them back. |
Validator owners open their validator to delegators, and set the commission, in the Qwai Node app.
Burn address
000000000000000000000000000000000000000000000000000000000000dead
No key can ever exist for this address. Coins, tokens and NFTs sent to it are gone for good. Coins held there are counted as burned and taken off the supply shown by the chain.
Live validators and their shares are on the Validators page.
Tokens and Qwaix
Every token launched on Qwaix runs the same program: a fixed supply of 1,000,000,000 with six decimals, no owner, and no way to make more.
Launching
A launch puts the whole supply into a pool against QWAI. The creator holds nothing they did not buy. The price follows a curve; when the pool has raised its target, it opens as an ordinary pool, its starting liquidity is locked for good, and anyone can add liquidity.
Tax
A creator can set a tax on every buy and sell, fixed forever at launch, split three ways:
| To holders | Paid in QWAI to everyone holding the token, in proportion to what they hold. Holders press Claim. |
| To the creator | Paid in QWAI on every trade. |
| Buy-back and burn | Spent buying the token from its own pool and destroying it. |
The three together can be at most 25%. Sending a token from one wallet to another is never taxed.
Fees
While a pool is launching, 1% of each trade is a fee. After it opens, 0.3% of each trade stays in the pool for liquidity providers.
NFTs
An NFT collection is one contract running the standard NFT program. Each item has a number, one owner, and a description stored on the chain: a name, a picture, text and traits. Wallets and explorers recognise every collection automatically because they all run the same program.
A collection is either closed (only its creator mints) or open (anyone mints by paying a price that goes to the creator), with an optional limit on how many items can ever exist. A collection can ask for a creator's share of every sale, up to 10%. See Standards for the calls.
Marketplace
NFTs are created, bought and sold on Qwaix. Selling never moves your item out of your wallet: you let the market move it, set a price, and keep holding it until someone pays. Offers hold the bidder's coins in the market contract until they are accepted or taken back. Each sale pays the creator's share and a 1% market fee; the seller receives the rest.
A listing ends for good if the item moves or you take the market's permission away. It does not come back if the item returns or permission is given again.
Traits
Give each item a traits object in its description, such as {"Background": "Night", "Level": 3}. Marketplaces build their filters and rarity ranks from it.
Smart contracts
A contract is an ordinary Rust program (#![no_std], #![no_main]) compiled for RISC-V. It is not Solidity and there is no EVM. The developer kit has the toolkit, a starter contract and the real token, NFT and exchange contracts.
Build
curl https://sh.rustup.rs -sSf | sh # install Rust once
./build.sh # in the kit folder; output: build/<name>.elf
A complete contract
#![no_std]
#![no_main]
use contract_sdk as chain;
const TOTAL: [u8; 32] = [1; 32];
fn main() {
let mut input = [0u8; 64];
let len = chain::input(&mut input);
if chain::is_deploy() { return; } // one-time setup goes here
match input[..len].split_first() {
// 01 + amount: add to the total
Some((1, args)) if args.len() == 8 => {
let mut now = [0u8; 8];
chain::storage_get(&TOTAL, &mut now);
let add = u64::from_le_bytes(args.try_into().unwrap());
let Some(next) = u64::from_le_bytes(now).checked_add(add) else { chain::revert(b"too large") };
chain::storage_set(&TOTAL, &next.to_le_bytes());
chain::emit(&chain::caller());
}
// 02: read the total
Some((2, [])) => { let mut now = [0u8; 8]; chain::storage_get(&TOTAL, &mut now); chain::ret(&now) }
_ => chain::revert(b"unknown call"),
}
}
chain::entry!(main);
How a contract runs
- It runs from the top each time it is called. The first input byte chooses the call; the rest are its arguments, fixed-width and little-endian.
- is_deploy() is true for the one run when the contract is first put on the chain.
- Memory is one megabyte, zeroed at the start of every call. There is no heap; use fixed-size arrays.
- revert (or a panic) fails the call and undoes everything it did, including coins it was sent.
- Release builds do not check arithmetic for overflow. Use checked_add and friends.
- A contract cannot be changed after it is deployed.
What a contract can ask of the chain
| input(&mut buf) | Copies the call's input; returns its full length. |
| ret(&bytes) | Ends the call successfully with an output. |
| revert(message) | Ends the call and undoes everything it did. |
| caller() | Who called: a wallet or another contract. |
| this() | This contract's own address. |
| value() | Coins sent with this call. |
| storage_get(&key, &mut out) | Reads a stored value. Keys are 32 bytes. |
| storage_set(&key, &value) | Stores up to 4,096 bytes. An empty value deletes. |
| transfer(&to, amount) | Sends coins the contract holds. Runs no code at the receiver. |
| balance(&address) | Coin balance of any address. |
| emit(&bytes) | Records an event, up to 1,024 bytes, 64 per transaction. |
| hash(&bytes) | BLAKE3. Use it to make storage keys. |
| block() | Height and time (milliseconds) of the current block. |
| call(&contract, value, &input, &mut out) | Calls another contract, up to 8 deep. If it fails, its effects are undone. |
| code_id(&address) | Identity of the program at an address, so a contract can accept only known programs. |
| is_deploy() | True during the deploying run. |
Gas and limits
| Per instruction | 1 gas |
| Storage write | 20,000 gas + 50 per byte |
| Storage read | 2,000 gas |
| Calling another contract | 30,000 gas |
| Sending coins | 10,000 gas |
| One transaction | up to 5,000,000 gas |
| One block | up to 50,000,000 gas |
| Price | 1,000 gas per smallest unit of QWAI |
Habits that keep contracts safe
- Check every input length exactly and revert on anything unexpected.
- Refuse coins on calls that do not use them, or they are stuck in the contract.
- Keep your own count of what the contract holds. Never decide anything from the contract's balance, which anyone can change by sending coins.
- Update your stored state before calling another contract, and guard against being called back mid-call.
- Round in the contract's favour.
Standards
Calls are a selector byte followed by fixed-width arguments. Amounts and item numbers are 8 bytes, little-endian. Addresses are 32 bytes.
Token
| 01 | transfer: to, amount |
| 02 | balance: of → amount |
| 03 | supply → amount |
| 04 / 05 / 06 | name, symbol, decimals |
| 07 | info → settings, totals, name, symbol |
| 08 | details → what the creator stored (picture, links) |
| 09 | owed: of → QWAI waiting for a holder |
| 0a | claim: of. Pays a holder what they are owed |
| 0b | burn: amount |
NFT collection
Program id 6aa367b3c8e72a23f5c006c501ea1ab403753325e34106530150c59edb7c5201
| 01 | transfer: to, item |
| 02 | owner: item → address |
| 03 | supply → made, existing, most |
| 04 / 05 | name, symbol |
| 08 | details → the collection's own description |
| 09 | item: item → owner, then its description (JSON) |
| 0a | mint: to, description → item number |
| 0b | burn: item |
| 0c | balance: owner → count |
| 0d | owned: owner, first, count (1 byte) → item numbers |
| 0e | approve: item, account. Lets that account move it once |
| 0f | approved: item → address |
| 10 | take: item, to. By an account allowed to move it |
| 11 | allow: account, yes (1 byte). Lets that account move any of your items until undone |
| 12 | allowed: owner, account → 1 byte |
| 13 | holder: item → owner, times moved |
| 14 | standing: item, account → owner, times moved, two permission counts, allowed, approved |
Deploy input: most items (8, 0 for no limit), mint price (8), open to all (1), creator's share in hundredths of a percent (2, at most 1000), then name, symbol and details, each after its length.
An item's description is JSON: {"name", "image", "about", "traits"}. The image is a data:image/...;base64, picture or an https:// address. Descriptions are at most 3,800 bytes.
NFT market
| 01 | list: collection, item, price, expiry |
| 02 | unlist: collection, item |
| 03 | buy: collection, item, price. Send the price |
| 04 | offer: collection, item (0 for any), price each, how many, expiry. Send the total |
| 05 | withdraw: collection, item, bidder |
| 06 | accept: collection, item, bidder, the offer's item (or 0), least price |
| 07 / 08 / 09 | listing, listings (a page), offers (a page) |
| 0a / 0b | totals for a collection, settings |
Times are milliseconds; an expiry of 0 means never. The market only trades collections running the standard NFT program.
Token lock
Program id 687e0605495ba0a96d24db29e62ee9ebec50cea430520a1b9cb34f5ed9e6747f
One lock is one contract. Whatever is sent to it, of one token and of QWAI, is released to one receiver on one schedule. Nobody, its creator included, can take anything back or change the schedule.
| Deploy input | token (32, zeros for QWAI only), receiver (32), start, first release, end (8 each, milliseconds) |
| 01 | release. Anyone may call; pays the receiver what has unlocked |
| 02 | info → settings, paid, held, releasable now |
| 03 | pass on: new receiver. By the receiver |
| 04 | add coins |
Create and release locks on Qwaix.
API
Every node answers over HTTPS with JSON. No key and no sign-up; any website may call it. Amounts are text in whole coins. Contract input and output are hex. There are no push updates: ask /status every couple of seconds and fetch what is new. The explorer has a live reference where every call can be tried.
| GET /status | Height, supply, staking totals. |
| GET /account/ADDRESS | Balance and next transaction number. |
| GET /blocks, /block/HEIGHT | Blocks and their transactions. |
| GET /tx/ID, /receipt/ID | A transaction and what it did. |
| GET /address/ADDRESS/transactions | History of an address. |
| GET /validators | Validators, stake, shares, commission and delegated coins. |
| GET /delegations/ADDRESS | What an account has staked behind validators. |
| GET /nft/collections, /nft/collection?id= | NFT collections with floor, volume and traits. |
| GET /nft/items?collection=&traits= | Items, filtered by trait, price, status or owner, and sorted. |
| GET /nft/item, /nft/offers, /nft/activity, /nft/account | One item, a collection's offers, recent activity, one account's NFTs. |
| GET /contract/ADDRESS | Whether a contract is there, and its program id. |
| GET /contracts?program=ID | Every contract running one program. |
| GET /events?contract=&about= | A contract's events, newest first. |
| POST /query | Runs a contract read-only. Body: {"contract", "input"}. |
| POST /queries | Up to 25 of those at once. |
| POST /tx | Submits a signed transaction. |
| GET /dex/tokens, /dex/token, /dex/holders | Qwaix tokens, one token, its holders. |
| GET /proof/ADDRESS | A balance with a proof against the signed state. |
Connecting the wallet
Qwail gives every page window.qwail. Your site asks, the user approves in the wallet, and the wallet signs and sends. Your code never touches a key.
if (!window.qwail) { /* send the user to https://qwail.org */ }
const { address } = await window.qwail.connect();
// Call a contract. input is hex: selector byte, then arguments.
// value is QWAI to send along, as text in whole coins.
const result = await window.qwail.signAndSend({
kind: 'call', contract: '<address>', input: '01' + to + u64(amount), value: '0',
});
// Resolves once it is in a block: { id, ok, output, created }
// Rejects if the user declines or the contract reverts.
// Put a contract on the chain. code is the .elf file as hex.
const made = await window.qwail.signAndSend({ kind: 'deploy', code, input });
// A plain payment.
await window.qwail.signAndSend({ kind: 'transfer', to: '<address>', amount: '2.5' });
Encoding helpers
// A number as 8 little-endian bytes of hex.
const u64 = (n) => { let v = BigInt(n), s = '';
for (let i = 0; i < 8; i++) { s += (v & 255n).toString(16).padStart(2, '0'); v >>= 8n; }
return s; };
// The reverse, from a hex reply.
const readU64 = (hex) => (hex.match(/../g) || [])
.reduceRight((n, h) => (n << 8n) | BigInt(parseInt(h, 16)), 0n);Building with AI
Qwai is new, so an AI assistant will not know it by default. Give it the guide and the kit and it has everything it needs.
- Download the developer kit and open the folder in your AI coding tool.
- Point it at QWAI-DEV-GUIDE.md, sdk/ and examples/. The guide is also available as plain text.
- Describe what you want.
Using QWAI-DEV-GUIDE.md, the SDK in sdk/ and the contracts in examples/
as the reference for style and safety, write a Qwai contract that
[what it should do]. Use the first input byte as the call selector,
fixed-width little-endian arguments, checked arithmetic, and revert on
any unexpected input. Then write a single-page site that reads it
through POST /api/queries and sends transactions through window.qwail.