Quickstart
This takes you from nothing to a funded post-quantum account that has sent a transaction on the live network.
What you will do
Section titled “What you will do”-
Generate a keypair. A 32-byte seed, which is your secret key.
-
Derive your address offline. No transaction, no gas, no deployment. The address exists as a mathematical fact before it exists on chain.
-
Fund it from the faucet. Funds arrive at an address with no code. This is normal.
-
Send. Your first transaction deploys your account and makes the payment, paid for out of the balance already sitting there.
1. Install an SDK
Section titled “1. Install an SDK”npm install viem# plus viem-8141, from source — see /build/sdks/viem-8141/uv add web3# plus web3-8141 with the mldsa extra — see /build/sdks/web3-8141/2. Generate a key and derive your address
Section titled “2. Generate a key and derive your address”The seed is the secret key. Thirty-two bytes, generated from a secure random source, is the whole thing — the ML-DSA keypair is derived from it deterministically.
import { randomBytes } from 'node:crypto';import { bytesToHex } from 'viem';import { createMldsaSigner, counterfactualAddress } from 'viem-8141';
const active = createMldsaSigner({ seed: bytesToHex(randomBytes(32)) });const backup = createMldsaSigner({ seed: bytesToHex(randomBytes(32)) });
const account = counterfactualAddress({ activePublicKey: active.publicKey, backupPublicKeyHash: backup.publicKeyKeccak,});
console.log('Your address:', account);import secretsfrom web3_8141.account import ML_DSA_65, account_salt, create2_address, FACTORY_ADDRESSfrom web3_8141.signing.mldsa import MldsaSigner
active = MldsaSigner(secrets.token_bytes(32))backup = MldsaSigner(secrets.token_bytes(32))
# See /build/create-a-wallet/ for building the rotatable initcode.print("Your address:", account)3. Fund it
Section titled “3. Fund it”Open faucet.frostfi.net, paste your address, and request funds. There is no captcha — your browser proves work by grinding ML-DSA-44 keypairs until one hashes with enough leading zero bits, which is a pleasingly on-theme way to rate-limit a post-quantum chain.
The faucet is also scriptable:
# 1. Get a challengecurl -s "https://faucet.frostfi.net/api/challenge?address=0xYOUR_ADDRESS"# → { "token": "…", "difficulty": 15, "algorithm": "ML-DSA-44",# "rule": "SHA3-256(pk || token) must have 15 leading zero bits" }
# 2. Grind an ML-DSA-44 keypair matching the rule, sign# "frost-faucet-v1" || token || address with it, then:curl -s -X POST https://faucet.frostfi.net/api/claim \ -H 'content-type: application/json' \ --data '{"address":"0x…","token":"…","pk":"<hex>","sig":"<hex>"}'# → { "tx_hash": "0x…", "amount_wei": "…" }Confirm it arrived — with any Ethereum tooling, since reading is ordinary:
curl -s https://rpc.frostfi.net -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance", "params":["0xYOUR_ADDRESS","latest"]}'Your account still has no code at this point. eth_getCode returns 0x. That is
expected — the address is counterfactual.
4. Send your first transaction
Section titled “4. Send your first transaction”The first send is special: it carries a DEPLOY frame that materialises the account, and pays for that deployment out of the balance you just received.
import { createPublicClient, defineChain, http, parseEther } from 'viem';import { buildFirstSendTransaction, frameActions, frost, serializeFrameTransaction, signFrameTransactionWitness,} from 'viem-8141';
const client = createPublicClient({ chain: frost, transport: http('https://rpc.frostfi.net'),}).extend(frameActions());
const tx = buildFirstSendTransaction({ activePublicKey: active.publicKey, backupPublicKeyHash: backup.publicKeyKeccak, recipient: '0xRECIPIENT', value: parseEther('0.001'),});
// Build → hash → sign → attach. The sig-hash excludes the witness bytes,// so attaching the signature does not change it.const signed = await signFrameTransactionWitness(tx, active);const hash = await client.sendFrameTransaction(serializeFrameTransaction(signed));
const receipt = await client.waitForFrameReceipt(hash);console.log(receipt.successful, receipt.frameReceipts);from web3 import Web3, HTTPProviderfrom web3_8141 import attachfrom web3_8141.codec import encode, sig_hash
w3 = attach(Web3(HTTPProvider("https://rpc.frostfi.net")))
tx = build_first_send_tx(...) # see /build/create-a-wallet/tx.signatures[0].signature = active.sign(sig_hash(tx))
tx_hash = w3.eip8141.send_frame_transaction(encode(tx))receipt = w3.eip8141.wait_for_frame_receipt(tx_hash)assert receipt.successfulLook it up on the explorer. Your account now has code,
and its nonce is 3 — CREATE2 set it to 1, the key-pointer deployment bumped
it to 2, and payment approval bumped it to 3. Always read
eth_getTransactionCount; never assume.
What just happened
Section titled “What just happened”Three things that have no equivalent on Ethereum:
- Your address existed before your account did. It is a pure function of your public keys, derived through the CREATE2 factory. See Accounts and keys.
- Your account deployed itself and paid its own way. No sponsor, no bundler, no separate deployment step.
- A lattice signature authorised it. The account’s VERIFY frame called the
0x14precompile with your signature and its own embedded public key, and only approved when it returned 1.
- Create a wallet — the same flow with every option explained.
- Rotate a key — what to do when the active key is lost.
- Send a transaction — ordinary spends, contract calls, and gas.