Integrate QuaiAxe
All examples use quais, Quai's SDK. Every address below is
checksummed before use — the Quai RPC rejects lowercase addresses. Addresses and constants are the
live mainnet ones from the overview.
import { quais } from 'quais';
const RPC = 'https://rpc.quai.network/cyprus1';
const provider = new quais.JsonRpcProvider(RPC, undefined, { usePathing: false });
const FACTORY = quais.getAddress('0x0000c6ae51b3698a2945d68696b59cf52dbb3a94');
const VAULT = quais.getAddress('0x000e8fa7cfa09827f0a0786ce3e96ac5e4e9f46c');
const LENS = quais.getAddress('0x00472f13e471b97429a45bf1890187a8ab850e8f');
const CURVE = quais.getAddress('0x004bc407903a51506bcf0b1ab423958c5991c237');
const INIT_CODE_HASH = '0x93921cd2500d1f18799ed6c2728cfd6983ac9afde51abc838963cbfea5597dea';1. Derive a miner's deposit address off-chain
This mirrors quaiaxe/src/lib/deposit-address.js (the site's own dependency-free implementation)
using quais's primitives instead of a hand-rolled keccak256:
const coder = quais.AbiCoder.defaultAbiCoder();
const ZERO_ADDRESS = quais.ZeroAddress;
const MAX_GRIND = 4096; // matches factory.MAX_GRIND() and findAddress's own cap
function saltOf(payout, burnBps, nonce) {
// payout is forced to the zero address at 100% burn — there is no payout wallet to name.
const effectivePayout = burnBps === 10000 ? ZERO_ADDRESS : payout;
return quais.keccak256(coder.encode(['address', 'uint256', 'uint256'], [effectivePayout, burnBps, nonce]));
}
function addressOf(payout, burnBps, nonce) {
const salt = saltOf(payout, burnBps, nonce);
return quais.getAddress(
'0x' + quais.keccak256(quais.concat(['0xff', FACTORY, salt, INIT_CODE_HASH])).slice(26),
);
}
// Cyprus-1 shard (first byte 0x00) AND the Quai ledger (top bit of byte 2 clear) — the same rule
// the factory's isMiningAddress / findAddress enforce.
function isMiningAddress(addr) {
const hex = addr.slice(2);
return hex.slice(0, 2) === '00' && (parseInt(hex.slice(2, 4), 16) & 0x80) === 0;
}
function deriveDepositAddress(payout, burnBps) {
for (let nonce = 0; nonce < MAX_GRIND; nonce++) {
const address = addressOf(payout, burnBps, nonce);
if (isMiningAddress(address)) return { address, nonce };
}
throw new Error('no address found within MAX_GRIND tries');
}
// 60% burn, the rest paid out to a miner's own wallet:
const payout = quais.getAddress('0x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); // example only
const { address, nonce } = deriveDepositAddress(payout, 6000);
console.log(address, nonce);Cross-check against the chain
Never trust a derivation you can't verify. findAddress is the on-chain source of truth and
grinds the identical rule:
const FACTORY_ABI = [
'function addressOf(address payout, uint256 burnBps, uint256 nonce) view returns (address)',
'function findAddress(address payout, uint256 burnBps, uint256 start) view returns (address depositAddress, uint256 nonce)',
];
const factory = new quais.Contract(FACTORY, FACTORY_ABI, provider);
const [onChainAddress, onChainNonce] = await factory.findAddress(payout, 6000, 0);
if (onChainAddress !== address || Number(onChainNonce) !== nonce) {
throw new Error('off-chain derivation disagrees with the chain — do not show this address to a miner');
}2. Build stratum settings
QuaiAxe deposit addresses are plain payout addresses as far as any miner or pool is concerned. The recommended path is StratumX solo (0% fee, non-custodial — the protocol pays the address directly):
function buildMinerConfig({ address, nickname, lock = 0, algorithm = 'sha256', region = 'eu' }) {
if (!/^0x00[0-9a-fA-F]{38}$/.test(address)) throw new Error('address must be a Cyprus-1 address (0x00…)');
if (!/^[A-Za-z0-9_-]{2,20}$/.test(nickname)) throw new Error('nickname must be 2-20 chars, [A-Za-z0-9_-]');
if (![0, 1, 2, 3].includes(lock)) throw new Error('lock must be 0, 1, 2 or 3');
const ports = { sha256: 3333, scrypt: 3334, kawpow: 3335 };
// StratumX solo serves SHA-256 only. Scrypt/KawPoW need your own go-quai node on the port above.
const host = algorithm === 'sha256' ? `mining-${region}.stratumx.org` : 'localhost';
const port = ports[algorithm];
return {
url: `stratum+tcp://${host}:${port}`,
// The pool splits on the FIRST dot: everything before it is the payout address.
user: `${address}.QAXE_${nickname}`,
password: `lock=${lock}`,
};
}
console.log(buildMinerConfig({ address, nickname: 'rig1' }));
// { url: 'stratum+tcp://mining-eu.stratumx.org:3333',
// user: '0x00….QAXE_rig1', password: 'lock=0' }lock=0 is Quai's mandatory minimum lock (14 days, ~241,920 blocks) — it is not a QuaiAxe
choice and cannot be made shorter. 3/6/12 months) earn a network reward boost that
decays over the first year, at the cost of a longer wait before the address's balance is real.lock=1/2/3 (
StratumX's SHA-256 endpoint is on port 3333; Scrypt and KawPoW are only available against your own go-quai node's built-in stratum, on ports 3334 and 3335 respectively.
3. Check a rig without waiting 14 days
A reward is locked before it's credited, so a misconfigured payout address looks identical to a correct one for two weeks unless you check workshares directly — they appear within minutes:
async function checkRig(address, { blocks = 30 } = {}) {
const addr = address.toLowerCase();
const latest = await provider.send('quai_getBlockByNumber', ['latest', false]);
const head = Number(BigInt(latest.woHeader.number));
let seen = false, shares = 0;
for (let i = 0; i < blocks; i++) {
const n = head - i;
if (n < 0) break;
const b = i === 0 ? latest : await provider.send('quai_getBlockByNumber', ['0x' + n.toString(16), false]);
if (!b || !b.woHeader) continue;
const coinbases = (b.workshares || []).map((w) => w.primaryCoinbase);
if (b.woHeader.primaryCoinbase) coinbases.push(b.woHeader.primaryCoinbase);
for (const c of coinbases) if (c && c.toLowerCase() === addr) { shares++; seen = true; }
}
return { seen, shares, scannedBlocks: Math.min(blocks, head + 1) };
}
console.log(await checkRig(address));seen: true means the address is genuinely receiving workshares; seen: false after a few minutes
of mining means something in the miner config is wrong — check the address, the port, and the
worker string before waiting two weeks to find out the hard way.
4. Collect a deposit
const DEPOSIT_FACTORY_ABI = [
'function collect(address payout, uint256 burnBps, uint256 nonce, address refundTo) returns (uint256 gross)',
];
const factoryWrite = new quais.Contract(FACTORY, DEPOSIT_FACTORY_ABI, wallet); // wallet = quais.Wallet
const tx = await factoryWrite.collect(payout, 6000n, BigInt(nonce), await wallet.getAddress());
const receipt = await tx.wait();
console.log('collected in', receipt.hash);Anyone can call collect on anyone's deposit address — collection just moves the balance into the
vault's accounting under that deposit address's own ledger entry, it never redirects who the
principal or payout belongs to. See deposit-factory for collectMany /
collectAndSweep when batching.
5. Preview and execute a sweep, with a real slippage floor
previewSweep()'s buyValue is what actually reaches the curve — not the slice, because a
portion of the slice is reserved for the caller's gas refund. Quote against buyValue, not the
slice, and remember curve.quoteBuy expects the net amount (after the curve's own fee), so back
that out first:
const VAULT_ABI = [
'function previewSweep() view returns (uint256 slice, uint256 fee, uint256 refundReserve, uint256 buyValue)',
'function canSweep() view returns (bool)',
'function sweep(uint256 minTokensOut, address refundTo) returns (uint256 quaiSpent, uint256 burned)',
];
const CURVE_ABI = [
'function feeBps() view returns (uint256)',
'function quoteBuy(uint256 quaiIn) view returns (uint256 tokensOut)',
];
const vault = new quais.Contract(VAULT, VAULT_ABI, provider);
const curve = new quais.Contract(CURVE, CURVE_ABI, provider);
if (!(await vault.canSweep())) throw new Error('not ready — cooldown or below minimum');
const { buyValue } = await vault.previewSweep();
const feeBps = await curve.feeBps();
const net = buyValue - (buyValue * feeBps) / 10000n; // the curve's OWN fee comes off first
const quoted = await curve.quoteBuy(net);
const minTokensOut = (quoted * 99n) / 100n; // 1% slippage floor — tune to your risk tolerance
const vaultWrite = vault.connect(wallet);
const tx = await vaultWrite.sweep(minTokensOut, await wallet.getAddress());
const receipt = await tx.wait();
console.log('swept in', receipt.hash);sweep's floor is enforced against the vault's actual post-buy QAXE balance (donations included),
not just what the curve reports buying — see burn-vault for the exact
mechanics.
6. Read everything in one call with QaxeLens
const LENS_ABI = [
'function read(address vault, address[] addrs) view returns (uint256[] balances, uint256[] received, uint256[] contributed, uint256[] kept, uint256[] pending)',
];
const lens = new quais.Contract(LENS, LENS_ABI, provider);
const addrs = [address /* , more deposit addresses */];
const [balances, received, contributed, kept, pending] = await lens.read(VAULT, addrs);
addrs.forEach((a, i) => {
console.log(a, {
uncollectedWei: balances[i].toString(),
lifetimeReceivedWei: received[i].toString(),
lifetimeContributedToBurnWei: contributed[i].toString(),
lifetimeKeptWei: kept[i].toString(),
pendingWithdrawWei: pending[i].toString(),
});
});balances[i] is the raw wallet balance sitting at the deposit address, uncollected. pending[i]
is what's waiting in step 7 below.
7. withdrawPending for a miner whose push failed
The miner-leg payout is pushed with only 30,000 gas. If the payout address can't accept a plain
transfer within that (a hostile or gas-hungry contract, or simply out of gas), the amount falls
back to pending[payout] instead of reverting the whole collection. The affected address pulls it
themselves, any time:
const VAULT_WRITE_ABI = ['function withdrawPending()', 'function pending(address) view returns (uint256)'];
const vaultAsPayout = new quais.Contract(VAULT, VAULT_WRITE_ABI, payoutWallet); // payoutWallet = quais.Wallet
const owed = await vaultAsPayout.pending(await payoutWallet.getAddress());
if (owed > 0n) {
const tx = await vaultAsPayout.withdrawPending();
await tx.wait();
}