Hartii developer docs

Games

Contract reference

All addresses live on Quai mainnet (Cyprus-1, chain 9), confirmed via quai_getCode.

MonthlyGamePayouts — one instance per game, the ranked-play prize pool

Ten independent deployments, one per game (addresses on index). Identical bytecode/ABI per instance. This is the whole trust model for ranked play: the chain only escrows, allocates and pays out — scoring, pricing and the win-split math are computed off-chain and only ever read into setMonthlyWinState.

solidity
function enterRanked() external payable;          // pay a ranked entry; funds join the pool
receive() external payable;                        // bare transfers also fund the pool

function setMonthlyWinState(                        // onlyOwner
    uint256 month,                                   // YYYYMM (UTC), must exceed lastSettledMonth
    address[] calldata recipients,
    uint256[] calldata amounts
) external;

function claimPayout() external;                    // pull-based, anyone with a claimable balance

function withdrawUnallocatedFunds(                   // onlyOwner — the platform's own cut
    uint256 amount,
    address payable to
) external;

function transferOwnership(address newOwner) external; // onlyOwner, one-step (no accept step)

// views
function owner() external view returns (address);
function totalFundsLocked() external view returns (uint256);   // sum of unclaimed winner allocations
function lastSettledMonth() external view returns (uint256);
function claimableBalances(address) external view returns (uint256);
function unallocatedBalance() external view returns (uint256); // balance - totalFundsLocked

Events: FundsDeposited(address indexed sender, uint256 amount), RankedEntry(address indexed player, uint256 amount, uint256 timestamp), WinStateSet(uint256 indexed month, uint256 recipientCount, uint256 totalAllocated), PayoutWithdrawn(address indexed recipient, uint256 amount), UnallocatedWithdrawn(address indexed to, uint256 amount), OwnershipTransferred(address indexed previousOwner, address indexed newOwner).

Reverts: "No value" (zero-value enterRanked), "Month already settled" (non-increasing month), "Length mismatch" / "Empty recipients" / "Zero recipient" / "Zero amount" (bad setMonthlyWinState input), "Insufficient balance" (would allocate more than the contract actually holds), "Nothing to claim" (empty claimPayout), "Transfer failed" / "Withdraw failed" (payout call reverted on the receiving side), "Exceeds unallocated" (owner trying to withdraw more than the unclaimed remainder), "Reentrant" (the nonReentrant guard), "Zero owner" / "Zero to".

Trust model — read this before assuming ranked play is custodial in the usual sense:

  • owner is a single address, intended by design to be "a secure / hardware / multisig wallet, reviewing an off-chain-computed winner set" (source comment) — a one-step transferOwnership, not two-step, so a typo'd new-owner address is a real risk on this contract specifically (unlike the Gallery marketplace contracts, which use two-step admin transfer).
  • setMonthlyWinState is the only way funds become claimable, and it's cumulative-add with a strictly-increasing month guard: re-calling it for an already-settled month reverts rather than double-allocating, and it can never allocate more than the contract's actual balance (require(address(this).balance >= totalFundsLocked)). The owner decides who gets how much — there is no on-chain scoring check inside this call; correctness of the winner set depends entirely on the off-chain settlement process feeding it.
  • Winners pull, the contract never pushes. claimPayout() is the only way a winner's balance leaves the contract, and it's nonReentrant with checks-effects-interactions (balance zeroed before the external call). No single winner's address can brick anyone else's payout — that's an explicit design goal, not an accident.
  • The owner's own cut (withdrawUnallocatedFunds) is bounded to what's not committed to a winnerunallocated = balance - totalFundsLocked, and the function reverts if you ask for more than that. The owner cannot touch a claimable balance that's already been allocated to a player.
  • No pause, no fee setter, no upgrade path. The entire admin surface is: allocate winnings once a month, withdraw the un-allocated remainder, hand off ownership. AGENTS.md states the operational policy on top of this: "Money is owner-signed... Never a hot key, never automated."

HartiiToken — HARTII (ERC-20)

0x00356B9bc20Ea80C654D77655Da53ca07547c216

Standard ERC-20 (transfer, approve, transferFrom, balanceOf, allowance) plus a public burn(uint256 value). Fixed supply, minted once to the owner at deploy — there is no mint function anywhere in the contract, so total supply can only ever go down (via burn), never up.

Events: Transfer, Approval, Burn(address indexed from, uint256 value).

Trust model: no owner/admin role at all on the token itself — once the initial supply is minted, nobody (not even the deployer) has a privileged function on this contract.

HartiiSale — QUAI → HARTII at a fixed rate

0x00483bE6EAaA8015130b216e06d8f70723dF822D

solidity
function buy() external payable;                       // or a bare transfer — same effect
function quote(uint256 quaiIn) external view returns (uint256);
function rate() external view returns (uint256);        // HARTII per 1 QUAI, both 18dp
function setRate(uint256 newRate) external;              // onlyOwner
function withdrawQuai(uint256 amount, address payable to) external; // onlyOwner
function withdrawHartii(uint256 amount, address to) external;       // onlyOwner
function transferOwnership(address newOwner) external;   // onlyOwner, one-step

Events: Bought(address indexed buyer, uint256 quaiIn, uint256 hartiiOut), RateChanged, QuaiWithdrawn, HartiiWithdrawn, OwnershipTransferred.

Reverts: "No value", "Sale under-funded" (contract doesn't hold enough HARTII to fill the buy — the buy simply fails rather than partially filling), "Transfer failed", "Zero rate", "Zero to", "Exceeds balance", "Reentrant".

Trust model: buying is fully trustless per-transaction — you get the quoted HARTII in the same tx or it reverts, no backend involved. The owner can change rate at any time (no cap enforced in code — this is a bare onlyOwner, unlike the Gallery marketplaces' hard-capped fees), can pull all accrued QUAI revenue, and can pull unsold HARTII back out. There's no mechanism forcing the owner to keep the sale funded or the rate stable — buy at your own assessment of the current rate()/quote() at call time.

HartiiCosmetics — ERC-1155 cosmetics

0x007fDe17CbA1Fe1750906F08f64fEf44195702eA

Standard ERC-1155 (balanceOf, balanceOfBatch, setApprovalForAll, isApprovedForAll, safeTransferFrom, safeBatchTransferFrom, uri) plus:

solidity
function buy(uint256 typeId) external;                  // pays in HARTII, partially burned
function mintWinnerEdition(                               // signature-gated tournament reward claim
    address winner, uint256 nonce, uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external;
function setCosmetic(uint256 typeId, uint256 price, uint256 maxSupply, bool active) external; // onlyOwner
function grant(address to, uint256 typeId, uint256 amount) external;    // onlyOwner
function setBaseURI(string calldata newBase) external;                  // onlyOwner
function setTreasury(address t) external;                               // onlyOwner
function setAuthorizedSigner(address signer) external;                  // onlyOwner
function transferOwnership(address newOwner) external;                  // onlyOwner, one-step

Events: CosmeticSet(uint256 indexed typeId, uint256 price, uint256 maxSupply, bool active), CosmeticBought(address indexed buyer, uint256 indexed typeId, uint256 pricePaid), WinnerEditionMinted(address indexed winner, uint256 indexed typeId, uint256 amount), plus standard ERC-1155 TransferSingle/TransferBatch/ApprovalForAll/URI and OwnershipTransferred.

Reverts: "Reserved id range" (owner trying to setCosmetic inside the winner-edition ID range), "Not for sale", "No price", "Sold out", "Payment failed" (HARTII transferFrom failure), "Treasury xfer failed", "Not the winner", "Voucher expired", "No signer", "Voucher used" (replay protection — each signed voucher digest can only be redeemed once), "Bad signature" (ecrecover mismatch against authorizedSigner), "Zero to", "Zero", "Reentrant".

Trust model: buy(typeId) pays in HARTII at the owner-set price; part of that payment is burned and the rest routed to treasury (the exact split is read from contract state at buy time, not a fixed constant — check setCosmetic's recorded price and the burn-vs-treasury split in source before assuming a ratio). mintWinnerEdition is how tournament rewards reach a player's wallet: it requires an ecrecover-verified signature from authorizedSigner (an off-chain, owner-controlled signing key — separate from the contract owner's own key) over a (winner, nonce, deadline) voucher, each digest usable exactly once. Owner powers: set cosmetic price/supply/active flag, grant cosmetics for free (e.g. promos), change the base metadata URI, change treasury, and rotate the authorized signer. Owner cannot mint an arbitrary ERC-1155 balance to itself through any path other than grant (which is on-chain, event-logged, and attributable).

HartiiMarketplace (cosmetics resale)

0x0034D51cc6162E27bCfDce5E276d92Eb563bb73C

Peer-to-peer resale of HartiiCosmetics items, priced and paid in HARTII (not QUAI).

solidity
function list(uint256 typeId, uint256 unitPrice, uint256 quantity) external returns (uint256 listingId);
function buy(uint256 listingId, uint256 quantity) external;
function updatePrice(uint256 listingId, uint256 newUnitPrice) external;
function cancel(uint256 listingId) external;
function setFees(uint256 _feeBps, uint256 _royaltyBps) external;   // onlyOwner
function setTreasury(address t) external;                          // onlyOwner
function transferOwnership(address newOwner) external;             // onlyOwner, one-step

Events: Listed, PriceUpdated, Cancelled, Sold(uint256 indexed listingId, address indexed buyer, address indexed seller, uint256 typeId, uint256 quantity, uint256 totalPaid), FeesChanged(uint256 feeBps, uint256 royaltyBps), OwnershipTransferred.

Trust model: feeBps defaults to 500 (5% house fee) and setFees is capped in the function itself — require(_feeBps <= 1000 && _royaltyBps <= 1000, "Too high"), a hard 10% cap on each, enforced on-chain. Owner can change the fee/royalty split (within that cap) and the treasury recipient, and nothing else — no pause, no ability to touch an individual listing.

Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.