Skip to content

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                      │
  └─────────────────────┬───────────────────────────────────┘

                     MINTED

1. 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)InterpretationAction
reverts (no such token)Not minted yetSend the mint
the intended recipientOur own earlier attempt landedSkip the send, finalize
any other addressCollisionRelease 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:

  1. Un-pin the id from the deposit.
  2. 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.
  3. 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:

ConditionOutcome
Trade rollback started but not completedHeld for review
Item not observable in custody, trade shows no rollbackHeld for review
Category not determinable from real Steam tagsHeld for review
Two indistinguishable items could satisfy the depositHeld for review
Unrecognised revert from the mint transactionHeld for review
Confirmed reversalNot minted; deposit reversed
Token id occupied by someone elseId 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.

LootFi is not affiliated with, endorsed by, or sponsored by Valve Corporation. Counter-Strike and Steam are trademarks of Valve Corporation.