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.frametx import FrametxCLIfrom web3_8141.signing.mldsa import MldsaSigner
active_seed = "0x" + secrets.token_bytes(32).hex()backup_seed = "0x" + secrets.token_bytes(32).hex()
active = MldsaSigner(active_seed) # .public_key is 1,952 bytesbackup = MldsaSigner(backup_seed)
# Assembling the rotatable account's initcode is the reference CLI's job;# the derivation itself is offline either way.derived = FrametxCLI().address( account_type="v2", active_seed=active_seed, backup_seed=backup_seed,)
print("Your address:", derived["address"])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": "…", "algorithm": "ML-DSA-44", "difficulty": 12,# "pool_bits": 12, "escalation_bits": 0, "prior_claims": 0,# "expires_at": 1790254544,# "rule": "SHA3-256(pk || token) must have 12 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": "…" }Read difficulty from the response rather than hardcoding it. The base
(pool_bits) rises as the faucet’s daily budget is drawn down, and each prior
claim by the same address or subnet in the last 24 hours adds a bit
(escalation_bits); the token stops being accepted at expires_at.
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, 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({ transaction: tx, signer: active });const hash = await client.sendFrameTransaction({ transaction: signed });
const receipt = await client.waitForFrameReceipt({ hash });console.log(receipt.status, receipt.frameReceipts); // status 1 on successfrom web3 import Web3, HTTPProviderfrom web3_8141 import attachfrom web3_8141.frametx import FrametxCLI
w3 = attach(Web3(HTTPProvider("https://rpc.frostfi.net")))
# The first send is the one shape web3-8141 does not build natively: it needs# a DEPLOY frame carrying the initcode. The CLI builds and signs it.first = FrametxCLI().send( active_seed, None, 0, # sender is derived; nonce is 0 variant="mldsa65", deploy=True, account_type="v2", backup_seed=backup_seed, recipient=recipient, value=10**15,)
tx_hash = w3.eth.send_raw_transaction(first["txs"]["approve"]["raw"])receipt = w3.eth.wait_for_transaction_receipt(tx_hash)assert receipt["status"] == 1Every later transaction is built natively — see Send a transaction.
Look 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.