> For the complete documentation index, see [llms.txt](https://docs-v4.nftx.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-v4.nftx.io/gacha/how-it-works.md).

# How It Works

How a gacha machine prices a pull, draws a winner with Chainlink VRF, and settles it safely through a commit/reveal flow.

A gacha machine has three moving parts: a **price** that emerges from the machine's inventory, a **draw** decided by verifiable randomness, and a **commit/reveal** flow that keeps the two honest across the blocks between them. This page walks through each.

{% hint style="warning" %}
**Testnet only.** Gacha is live today only on **Ethereum Sepolia** (chain `11155111`) as a rehearsal — there is no mainnet deployment yet.
{% endhint %}

## The house is the depositors

Every NFT in a machine was put there by a depositor, and it lives in the gacha vault's own custody — not in the shared [Locker](/contracts/locker.md).

That separation is deliberate. `Locker.swap` is a *free 1:1 NFT swap*: any desirable NFT sitting unlisted in the Locker can be taken by anyone for the cost of one floor NFT of the same collection. A prize pool held in the Locker would be sniped before the wheel ever turned. So a machine's inventory is bucketed by collection and held by the [NFTXGachaVault](/contracts/gacha-vault.md), where only the machine can draw from it.

Because a floor NFT is worth exactly 1.0 collection token by construction, the machine never has to appraise anything. Every unit of a collection is the same prize, valued at that collection's live floor. The only variance is *which collection* you draw.

## Pricing: expected value plus the vig

A spin is priced from the machine's live inventory, every single time. Two numbers drive it:

* Each collection `c` has a floor price `p_c` (its collection token's ETH price, read from the pool's TWAP) and a number of drawable units `n_c`.
* An asset's **draw weight is inverse to its price** — `1/p_c` per unit — so the cheaper a collection, the likelier it is to drop. Bucket `c` therefore carries weight `n_c / p_c`.

From those, the machine computes its expected value and price:

```
D     = Σ_c n_c / p_c
N     = Σ_c n_c                                (total drawable units)
EV    = N / D                                  (the machine's expected payout per pull)
price = EV · (10,000 + vigBps) / 10,000        (recomputed on every quote and spin)
```

`EV` is a harmonic mean over the assets, and `price` is that EV marked up by the **vig** — the house edge, in basis points (default `2000`, i.e. 20%). Because the price is always EV plus a positive vig, **a pull costs floor value or better**: you never pay above the machine's own expected payout by more than the vig, and that vig is what pays the depositors.

{% hint style="info" %}
**The invariant that makes it fair:** every unit in the machine contributes exactly `1/D` to the expected payout, regardless of its own price. A cheap unit is likelier to drop but pays little; a dear unit rarely drops but pays a lot. The two cancel, so inventory drains uniformly in *value*.
{% endhint %}

### A worked example

Say a machine holds nine floor Miladys at 1 ETH each and one Pudgy Penguin worth 100 ETH — a jackpot. The harmonic mean barely moves for the jackpot:

|                             |            |
| --------------------------- | ---------- |
| Machine EV                  | \~1.11 ETH |
| Price at a 20% vig          | \~1.33 ETH |
| Chance of drawing the Pudgy | \~0.11%    |

The jackpot raises the spin price by at most `1/n` however expensive it is — here, a couple of percent. It's cheap to *advertise* a jackpot and the odds of hitting it are honestly tiny; the variance it creates lands on the depositors, not the price. That trade-off is spelled out in [For depositors](/gacha/for-depositors.md).

### Machines are quality bands

The harmonic mean is dominated by its smallest element, so one very cheap collection can crater a machine's price. Add a single 1-ETH unit to a machine of 40-ETH units and the EV can collapse by over 90% — turning a 40-ETH machine into a near-giveaway. The operator curates a machine's price band, and structural guards checked at snapshot time keep a machine from being quietly taken over by one cheap bucket:

| Guard                      | Default                  | What it bounds                                     |
| -------------------------- | ------------------------ | -------------------------------------------------- |
| Max bucket weight share    | `20%`                    | Any one collection's share of the draw             |
| Minimum machine EV         | \~50% of launch EV       | A fail-closed tripwire; spins stop if EV collapses |
| Per-collection unit cap    | `100`                    | How many units one bucket can hold                 |
| Inventory floor at request | `N ≥ max(10, 5 × pulls)` | A batch can't be sold against near-empty custody   |

The takeaway: a machine is a *quality band*, curated by its operator. The protocol doesn't cap how wide the band may be — a machine can deliberately span cheap floors and rare grails — but the EV tripwire and weight-share cap still refuse to price a spin against a machine one cheap bucket has run away with.

## The draw: one VRF word, expanded

Randomness comes from **Chainlink VRF v2.5**. A spin requests a single 256-bit random word; the machine expands it, with domain-separated hashing, into every draw it needs — one bucket pick and one token-ID pick per pull, plus a redraw seed for the empty-bucket case. Selection walks a cumulative-weight array, so the odds match the pricing weights exactly. Within the chosen bucket, the specific token ID is uniform — every unit is the same prize anyway.

Using the request/callback split (below) also makes the draw **flash-loan immune**: no capital moved inside a single transaction can influence an outcome that was sealed in an earlier block.

## Commit / reveal: snapshot, resolve, claim

VRF is a request-then-callback oracle: you pay at request, the draw resolves a few blocks later. That gap has to be handled carefully, because the fulfillment is a public transaction whose contents determine the outcome.

```
spin()  ──►  PENDING  ──(VRF callback)──►  RESOLVED  ──(claim)──►  CLAIMED
   │  price paid + inventory snapshot          random word          prizes + change
   │  hashed and committed                      lands
```

At **request time**, the machine freezes everything a spin depends on — the per-bucket prices, unit counts, vig, and fee — into a *snapshot*, hashes it, and commits the hash on-chain. The spinner's payment is held in per-request escrow. Nothing that happens afterwards — new deposits, config changes — can reprice a pending spin. The one sanctioned deviation is the **empty-bucket cascade**: if the drawn bucket happens to have emptied by claim time (other spins or withdrawals drained it), the machine deterministically redraws over the snapshot's remaining non-empty buckets, using the same random word, so everyone sees the same outcome.

At **claim time**, the claimer supplies the snapshot back (its hash must match), the protocol fee is skimmed, the remainder is distributed to the depositors, and the prizes transfer last. Claims are resumable and can be batched, and anyone can claim on a spinner's behalf for a small tip — a house bot does this within minutes so nobody's prize sits waiting.

{% hint style="warning" %}
**A timeout never refunds ETH — it re-randomises.** If the price were refundable on a slow fulfillment, a spinner could watch the mempool, compute their result, and cancel their losses while keeping their wins. Since a pull pays out less than its price most of the time (that's what a vig *is*), a refund path would invert the house edge. Instead, a stuck request can be re-requested for a new random word against the *same* snapshot and the *same* price — expected-value-neutral, so there's no free option. Only after a long escape window (7 days), during which no fulfillment ever arrived, can the full price be refunded — and by then there's no outcome to game.
{% endhint %}

## Where the money goes

When a spin is claimed, its price splits three ways:

```
price  →  protocol fee (default 5%)  +  vig/revenue → depositors  +  the prize's value stays in inventory
```

The revenue left after the protocol fee streams to the machine's depositors, pro-rata by the live value of their stake, through a rewards accumulator. A depositor keeps earning on a unit even *after* it's won — that's **residual yield**, and it's the heart of the [depositor model](/gacha/for-depositors.md).

There's no internal buyback and no restock loop: a winner keeps their NFT or sells it through the [Zap](/contracts/zap.md) at floor, and the only thing that refills a machine is fresh deposits chasing the yield.
