Appearance
Minting
Reaching the unlock time makes a deposit eligible to mint. It does not mint it. Between eligibility and a token existing sits a gate that re-proves, from scratch, every fact the mint depends on.
This page is that gate, in order.
The gate, step by step
ELIGIBLE_TO_MINT
│
▼
┌─────────────────────────────────────────────────────────┐
│ 1. Lock the deposit row │
│ everything below runs inside that lock │
└─────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 2. Trade record: was this rolled back? │
│ reversed → freeze the account's deposits, no mint │
│ ambiguous → hold for review, no mint │
└─────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 3. Fresh custody re-verification │
│ the item must be where it is supposed to be, now │
│ not where it was when the deposit was accepted │
└─────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 4. Category resolved from real Steam tags │
│ unresolvable → hold for review, never guess │
└─────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 5. Allocate the tokenId (once) and pin it to the deposit│
└─────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 6. Idempotency check against the CHAIN, then mint │
└─────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 7. Confirmation-based finalization │
└─────────────────────┬───────────────────────────────────┘
▼
MINTED1. Row lock
The deposit row is locked for the duration of the gate. Two workers cannot evaluate the same deposit concurrently, and no other process can advance it mid-decision. Every check below reads state that cannot change underneath it.
2. The trade record outranks everything
The first substantive question is not "do we hold the item" but "was the trade that delivered it rolled back". Steam's trade record answers it authoritatively and continues to answer it after the protection window closes.
- Reversed → the deposit does not mint. Every open deposit on that Steam account is frozen and re-verified, because a reversal is account-scoped, not trade-scoped.
- Rollback started but not completed → held for review. The item is probably still in custody, but "probably" is not a basis for minting, and it is certainly not a basis for destroying the deposit either.
- Held → continue.
The trade record outranks an inventory reading in both directions: a rollback is a rollback even if the item is momentarily visible, and an item's absence during protection is normal rather than evidence of anything.
3. Fresh custody re-verification
By mint time the protection window has closed, so the item should be observable in custody. This check is deliberately performed now, inside the lock, rather than trusting the verification done days earlier when the deposit was accepted. State from a week ago is not evidence about the present.
If the item is not observable but the trade record shows no rollback, that is an anomaly — the two sources disagree. The deposit is held for review. It is not minted, and it is not declared reversed. Declaring a reversal that the trade record denies would destroy a live deposit on the strength of a single ambiguous reading.
4. Category
Category is packed into the token id and can never be corrected — no upgrade, no admin call, no re-mint. So the gate refuses to guess.
The category pinned at deposit time (from the item's live Steam type tag) is preferred. If none was pinned and the stored tags still yield no definite answer, the deposit is held for review rather than minted with a name-based guess. A wrong guess here is permanent; a hold costs a person a look.
See Token IDs for the encoding and Deposit lifecycle for why it is pinned early.
5. Token id allocation
The id is allocated once — from the category and a per-category sequence — and pinned to the deposit. A retry reuses the pinned id rather than allocating a second one, so a transient failure cannot burn ids or produce two tokens for one item.
6. Idempotency: existence is not success
Before sending a mint transaction, the minter reads ownerOf(tokenId) on chain and branches on who owns it, not merely on whether it exists:
ownerOf(tokenId) | Interpretation | Action |
|---|---|---|
| reverts (no such token) | Not minted yet | Send the mint |
| the intended recipient | Our own earlier attempt landed | Skip the send, finalize |
| any other address | Collision | Release the id, fast-forward, retry |
The third row is the important one, and the reason the check is written this way.
The sequence counter lives off-chain; the chain is the source of truth for which ids exist. A database restored from a backup, or rebuilt, has a counter that is behind the chain and will confidently hand out ids that are already minted. If the minter treated "this token already exists" as "our earlier attempt succeeded", it would mark the deposit complete and record the depositor as the owner of an NFT that belongs to a stranger — their real skin consumed, nothing delivered.
So a collision is handled as a collision:
- Un-pin the id from the deposit.
- Probe the chain for a free sequence and fast-forward the counter past the whole occupied run, rather than advancing one id per attempt. A counter a few thousand mints behind would otherwise exhaust its retries and park a perfectly good deposit in review.
- Retry; the next pass allocates a free id.
The counter only ever moves forward. It is never rewound and gaps are never reclaimed, so no concurrent mint can be handed the same id twice. Gaps in the sequence are normal and carry no meaning.
7. Confirmation-based finalization
The mint transaction is sent with a per-transaction gas estimate — never a hardcoded limit, because Robinhood Chain is an Arbitrum Nitro L2 with its own fee model. The deposit is finalized only after the transaction confirms, not when it is broadcast.
Finalization writes the token record and its metadata blob in one transaction, re-reading the deposit at that moment so that enrichment data which landed during the mint is not clobbered by a stale snapshot.
If the send path errors after broadcast — an RPC hiccup, a duplicate retry losing a race — the worker reconciles from chain state before doing anything else, and applies the same owner check as above. "The token exists" is not proof it was our send, on this path either.
Mint authority
Minting is gated to an allow-listed minter role:
solidity
mapping(address => bool) public isMinter;
modifier onlyMinter() {
if (!isMinter[msg.sender]) revert NotMinter();
_;
}
function mint(address to, uint256 tokenId) external onlyMinter;
function mintBatch(address to, uint256[] calldata tokenIds) external onlyMinter;Two properties worth stating plainly:
The minter role is separate from contract ownership. The owner administers the contract (base URI, royalty, fees, upgrades) and controls the minter set, but ownership alone does not confer the ability to mint. They are distinct permissions on distinct addresses.
Both are transferable. transferOwnership moves the admin role; updateMinter(address, bool) adds and removes minters. The design intent is that ownership moves to a multisig and the minter stays a segregated key with no admin power. Anyone can read isMinter(address) and owner() on chain to see the current state.
mintBatch mints many ids to one recipient in a single transaction, bounded by MAX_BATCH (50). An empty array reverts with EmptyBatch; an oversized one with BatchTooLarge.
Every mint also validates the token id's category byte and reverts on a duplicate id. See Token IDs.
What mint authority means for you
An allow-listed minter can create tokens. Nothing in the contract requires a real item to exist behind one — that binding is an operator assertion recorded off-chain, and it is a trust assumption. Stated in full on Trust & security.
What the minter role cannot do: move, freeze, or burn a token that already exists. Those require the token's owner.
Anything unprovable is held, not minted
Collecting the review branches from the whole gate:
| Condition | Outcome |
|---|---|
| Trade rollback started but not completed | Held for review |
| Item not observable in custody, trade shows no rollback | Held for review |
| Category not determinable from real Steam tags | Held for review |
| Two indistinguishable items could satisfy the deposit | Held for review |
| Unrecognised revert from the mint transaction | Held for review |
| Confirmed reversal | Not minted; deposit reversed |
| Token id occupied by someone else | Id released, counter advanced, retried |
| Transient failure (network, rate limiting, nonce) | Retried with backoff |
Nothing is ever silently dropped. A deposit that cannot mint is visible in a state a person acts on, not lost.
Related
- Deposit lifecycle — how a deposit reaches the gate.
- Custody model — why the trade record is authoritative.
- LootSkins (ERC-721) — the full contract interface.
- Admin & upgrades — who holds which role.