# XCHandles FAQ

Frequently Asked Questions about XCHandles

### What is XCHandles?

XCHandles is, at its core, a decentralized address book. Users can register human-friendly handles such as @yak, which can then be associated with an XCH address, profile picture, display name, and much more. XCHandles is the Chia equivalent of the Ethereum Name Service and the first fully decentralized registry of this kind.

### How much does a handle cost?

New handles are priced based on length. Handles containing at least one number have a 50% discount. The current [pricing puzzle](/technical-manual/pricing-puzzles#factor-pricing) has a base price of 5.0 wUSDC.b, which translates to the costs below:

<table><thead><tr><th width="100">Length</th><th width="130" data-type="checkbox">Has Numbers?</th><th width="100">Factor</th><th width="173">Example Handle</th><th>Cost</th></tr></thead><tbody><tr><td>3</td><td>false</td><td>128</td><td>@abc</td><td>640 wUSDC.b per year</td></tr><tr><td>3</td><td>true</td><td>64</td><td>@ab1</td><td>320 wUSDC.b per year</td></tr><tr><td>4</td><td>false</td><td>64</td><td>@abcd</td><td>320 wUSDC.b per year</td></tr><tr><td>4</td><td>true</td><td>32</td><td>@abc1</td><td>160 wUSDC.b per year</td></tr><tr><td>5</td><td>false</td><td>16</td><td>@abcde</td><td>80 wUSDC.b per year</td></tr><tr><td>5</td><td>true</td><td>8</td><td>@a1234</td><td>40 wUSDC.b per year</td></tr><tr><td>6+</td><td>false</td><td>2</td><td>@example</td><td>10 wUSDC.b per year</td></tr><tr><td>6+</td><td>true</td><td>1</td><td>@example1</td><td>5 wUSDC.b per year</td></tr></tbody></table>

### What happens when a handle expires?

Immediately after a handle expires, an [expiration auction](/technical-manual/pricing-puzzles#exponential-premium) begins. The handle is valued at the normal price plus a premium. The premium begins at \~100.000.000 wUSDC.b and decreases continuously each second, halving each day. The first user to pay the current auction price will receive the handle. The premium becomes 0 28 days after an auction started, allowing anyone to register the handle at its normal price if no bids have been made.

### How should I refer to XCHandles?

When speaking, you can refer to XCHandles simply as "handles." If you think the underlying blockchain is not shared knowledge, you may also say "X-C-H handles"/"Chia handles." In writing (e.g., messages, UIs, etc.), please write "XCHandles". The only exception to the previous code is Rust code, where "Xchandles" may be used to conform to variable naming standards.

### Who controls pricing?

A price singleton has the ability to update the payment CAT for handles. The singleton also has control over the pricing and expired pricing puzzles, enabling it to change the pricing strategy of XCHandles if needed. The pricing singleton is currently a 6-of-10 multisig controlled by [warp.green validators](https://docs.warp.green/#who-are-the-validators).

### How do I know that XCHandles was properly deployed? <a href="#how-do-i-know-xchandles-was-properly-deployed" id="how-do-i-know-xchandles-was-properly-deployed"></a>

Anyone can use the [slot-machine CLI](https://github.com/Yakuhito/slot-machine/) to verify a deployment by running:

```bash
cargo r xchandles verify-deployment --launcher-id <hex> [--testnet11]
```

On testnet11, the command checks the on-chain registry against trusted CSV files bundled with slot-machine (`xchandles_premine_testnet11.csv` and `xchandles_price_schedule_testnet11.csv`). See the [CLI reference](/technical-manual/cli/xchandles#verify-deployment) for details.

To verify the deployment of the root registry on mainnet, you can run:

```bash
cargo r --release xchandles verify-deployment --launcher-id 6f314f69a9cf776311e1c7781a3013e4daae3696c71d8a04d049a5af1530f050 --premine xchandles_premine_mainnet.csv
```


# API

Behold, the public API

The listener API is a read-only JSON HTTP service used to resolve handles, look up data needed for registration, and get information about the protocol.

| Network | Base URL                    |
| ------- | --------------------------- |
| Mainnet | <https://api.xchandles.com> |

All listed routes are **GET** (HEAD is also supported), and CORS allows any origin. IDs in responses are 64-character lowercase hex **without** `0x`. Handles are 3-63 lowercase ASCII letters and digits with no normalization.

On most routes, omitting `launcher_id` selects the root registry, whose launcher is the default. If the index is too stale, important reads will return `503` with `"code": "index_stale"` instead of a normal response.

***

## `GET /healthz`

Liveness probe; returns an empty `200`.

***

## `GET /handle/{handle}`

Returns a live (unspent) slot plus information about the resolved singleton (if available, name NFT data as well) for a given handle. The data can be used to construct the coin id of the latest unspent handle slot and the latest NFTs, which can be checked on-chain to exist but not be spent for confirmation. `slot_parent_id` is the parent of that slot coin; `slot_parent_parent_id` and `slot_parent_inner_puzzle_hash` are its compact lineage proof (parent amount is the registry singleton amount, typically `1`). Together they are enough to spend the slot trustlessly. `pending_transfer` is `null` when no transfer is executable, or the same object as `GET /handle/{handle}/pending-transfer`.

| Param                            | Required | Description                                                         |
| -------------------------------- | -------- | ------------------------------------------------------------------- |
| `{handle}`                       | yes      | handle (in path)                                                    |
| `launcher_id`                    | no       | Registry launcher id (64-char hex)                                  |
| `include_metadata`               | no       | If `true`, include NFT metadata CLVM as hex (when available)        |
| `bypass_expiration_safety_check` | no       | If `true`, return the proof even after expiration (`410` otherwise) |

```json
{
  "registry_launcher_id": "<hex32>",
  "handle": "alice",
  "slot": {
    "counter": 0,
    "handle_hash": "<hex32>",
    "neighbors": { "left_value": "<hex32>", "right_value": "<hex32>" },
    "expiration": 4102444800,
    "owner_launcher_id": "<hex32>",
    "resolved_launcher_id": "<hex32>"
  },
  "slot_parent_id": "<hex32>",
  "slot_parent_parent_id": "<hex32>",
  "slot_parent_inner_puzzle_hash": "<hex32>",
  "slot_confirmation_height": 90,
  "resolved_singleton": { "...same shape as GET /singletons/{launcher_id}..." },
  "indexed_peak_height": 116,
  "pending_transfer": null
}
```

***

## `GET /handle/{handle}/pending-transfer`

Returns a performable pending ownership transfer for the handle, or `204` if none is executable. The same payload is also embedded as `pending_transfer` on `GET /handle/{handle}` (`null` instead of `204`). This should be queried and asserted to return 204 (or `pending_transfer: null`) when an offer involving a handle NFT is accepted.

| Param         | Required | Description          |
| ------------- | -------- | -------------------- |
| `{handle}`    | yes      | handle (in path)     |
| `launcher_id` | no       | Registry launcher id |

`200`:

```json
{
  "new_owner_launcher_id": "<hex32>",
  "new_resolved_launcher_id": "<hex32>",
  "update_confirmation_height": 100,
  "minimum_execution_height": 150,
  "initiator_coin_id": "<hex32>",
  "executor_coin_id": "<hex32>"
}
```

***

## `GET /singletons/{launcher_id}`

Returns information about a followed singleton. The API tracks all owner and resolved singletons for all handles registered in followed registries.

| Param              | Required | Description                                                  |
| ------------------ | -------- | ------------------------------------------------------------ |
| `{launcher_id}`    | yes      | 64-char hex launcher id                                      |
| `include_metadata` | no       | If `true`, include NFT metadata CLVM as hex (when available) |

```json
{
  "launcher_id": "<hex32>",
  "parent_coin_id": "<hex32>",
  "amount": 1,
  "inner_puzzle_hash": "<hex32>",
  "confirmation_height": 100,
  "melted": false,
  "melt_height": null,
  "nft": {
    "metadata_treehash": "<hex32>",
    "metadata_updater_puzzle_hash": "<hex32>",
    "current_owner": null,
    "royalty_puzzle_hash": "<hex32>",
    "royalty_basis_points": 500,
    "p2_puzzle_hash": "<hex32>"
  },
  "indexed_peak_height": 116
}
```

`nft` is `null` for non-NFT singletons. `nft.metadata` is omitted unless `include_metadata=true`.

***

## `GET /registrations/{handle}`

Returns the latest confirmed register or expire action for a handle.

| Param         | Required | Description          |
| ------------- | -------- | -------------------- |
| `{handle}`    | yes      | handle (in path)     |
| `launcher_id` | no       | Registry launcher id |

```json
{
  "handle": "alice",
  "registration_secret": "<hex32>",
  "action_kind": "register",
  "protocol_fee": 1000,
  "confirmation_height": 90,
  "indexed_peak_height": 116
}
```

`action_kind` is `"register"` or `"expire"`. Fees are mojos of the current payment CAT.

***

## `GET /recent-registrations`

Returns the newest register/expire events, plus the running count of registered handles. This is used for the front page of XCHandles.

| Param         | Required | Description                         |
| ------------- | -------- | ----------------------------------- |
| `launcher_id` | no       | Registry launcher id                |
| `limit`       | no       | Page size; default and maximum `50` |

```json
{
  "items": [
    { "handle": "carol", "action_kind": "expire", "confirmation_height": 110 }
  ],
  "total_registered": 2,
  "indexed_peak_height": 116
}
```

***

## `GET /expiring`

Returns a cursor-paginated directory of handles that are in an expiration auction or about to expire.

| Param         | Required | Description                                                         |
| ------------- | -------- | ------------------------------------------------------------------- |
| `view`        | yes      | `active` (in the 28-day auction) or `soon` (expires within 30 days) |
| `cursor`      | no       | Obtained from a previous page; used for pagination                  |
| `limit`       | no       | Page size; default and maximum `50`                                 |
| `launcher_id` | no       | Registry launcher id                                                |

`view=active`:

```json
{
  "items": [{
    "handle": "bob",
    "expiration": 1798272000,
    "projected_pricing_timestamp": 1800000420,
    "current_premium": 94830,
    "total_registration_fee": 734830,
    "base_registration_fee": 640000,
    "reaches_base_at": 1800691200
  }],
  "next_cursor": "v1.1798272000.bob",
  "indexed_peak_height": 116,
  "confirmed_timestamp": 1800000000
}
```

`view=soon`:

```json
{
  "items": [{
    "handle": "dan",
    "expiration": 1800604800,
    "base_registration_fee": 640000
  }],
  "indexed_peak_height": 116,
  "confirmed_timestamp": 1800000000
}
```

`next_cursor` is omitted on the last page. Fees are mojos for a 1-year registration. Quotes use the **effective** base: the last generation whose `activation_timestamp` is at or before `confirmed_timestamp` (what a buyer pays after unrolling). The directory does not include the generation list or remaining unrolls.

***

## `GET /price`

Live Price Singleton snapshot: committed (pre-unroll) base, remaining executable unrolls, and index freshness. Optional `launcher_id` selects a registry; omitting it uses the default.

| Param         | Required | Description          |
| ------------- | -------- | -------------------- |
| `launcher_id` | no       | Registry launcher id |

```json
{
  "indexed_peak_height": 116,
  "confirmed_timestamp": 1786935600,
  "current_base_price": 1,
  "unrolls": [
    { "activation_timestamp": 1786885200, "base_price": 9 },
    { "activation_timestamp": 1786892400, "base_price": 8 }
  ]
}
```

`current_base_price` is the committed registry / pricing puzzle (before unrolling). `unrolls` are remaining generations from the current scheduler generation onward, in order. A row with `activation_timestamp <= confirmed_timestamp` is **due** (a wallet can unroll now). The payable base after unrolling is the last due unroll’s `base_price`, or `current_base_price` if nothing is due. Before the first generation row that value is launch price `1`. Stale index: `503` `index_stale`.

***

## `GET /schedule`

The full static generation list baked into the Price Singleton launcher, including already-activated rows. This list does not change; the last row’s `base_price` holds after that timestamp. There are no peak or timestamp freshness fields. Optional `launcher_id` is the same as on other routes.

| Param         | Required | Description          |
| ------------- | -------- | -------------------- |
| `launcher_id` | no       | Registry launcher id |

```json
{
  "generations": [
    { "activation_timestamp": 1786885200, "base_price": 9 },
    { "activation_timestamp": 1787022000, "base_price": 1 }
  ]
}
```

Clients use `/schedule` for future window math (reminder crossings, home tables). Do not treat a missing later row as a vault-era fallback.

***

## `GET /neighbors`

Given a handle hash, returns the neighboring handle slots, each slot's parent coin id, and its compact lineage proof (`parent_parent_id` and `parent_inner_puzzle_hash`; parent amount is always `1`). This endpoint alone (plus on-chain data) is enough to enable trustless registrations for light clients.

| Param         | Required | Description                    |
| ------------- | -------- | ------------------------------ |
| `launcher_id` | yes      | Registry launcher id           |
| `handle_hash` | yes      | handle tree hash (64-char hex) |

```json
{
  "left_handle_hash": "<hex32>",
  "right_handle_hash": "<hex32>",
  "left_left_handle_hash": "<hex32>",
  "left_expiration": 0,
  "left_counter": 0,
  "left_owner_launcher_id": "<hex32>",
  "left_resolved_launcher_id": "<hex32>",
  "left_parent_id": "<hex32>",
  "left_parent_parent_id": "<hex32>",
  "left_parent_inner_puzzle_hash": "<hex32>",
  "right_right_handle_hash": "<hex32>",
  "right_expiration": 0,
  "right_counter": 0,
  "right_owner_launcher_id": "<hex32>",
  "right_resolved_launcher_id": "<hex32>",
  "right_parent_id": "<hex32>",
  "right_parent_parent_id": "<hex32>",
  "right_parent_inner_puzzle_hash": "<hex32>"
}
```

***

## Errors

Chain-backed reads other than `/neighbors` use:

```json
{
  "code": "handle_not_found",
  "message": "No live handle slot is indexed for this handle",
  "request_id": "<uuid>"
}
```

| Status | `code`                                                                                                      |
| ------ | ----------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_handle`, `invalid_launcher_id`, `invalid_view`                                                     |
| `404`  | `handle_not_found`, `registry_not_followed`, `singleton_not_followed`                                       |
| `410`  | `handle_expired`                                                                                            |
| `503`  | `index_stale`, `singleton_incomplete`, `singleton_mismatch`, `resolution_incomplete`, `resolution_mismatch` |


# Premine

How XCHandles allocates handles before open registration

The premine is the set of handles reserved before open mainnet registration and assigned to specific receivers. It exists so people who already held names in earlier Chia naming systems - and contributors who helped the ecosystem - receive a fair starting allocation, while everyone else still has a fair shot at registering remaining handles when XCHandles launches.

Mainnet launch is scheduled for **August 20, 2026 at 09:00 UTC**. Alongside the premine, the first two weeks after launch use a declining global pricing factor so early public registrations cost more than the long-term price. That launch pricing period is described in [CHIP-0054](https://github.com/Chia-Network/chips/pull/192): handle prices are multiplied by a factor that steps down each day and eventually reaches **1x** (normal price). The factor schedule is:

| Time   | Pricing Factor |
| ------ | -------------- |
| Day 1  | 1000x          |
| Day 2  | 750x           |
| Day 3  | 500x           |
| Day 4  | 250x           |
| Day 5  | 100x           |
| Day 6  | 75x            |
| Day 7  | 50x            |
| Day 8  | 33x            |
| Day 9  | 25x            |
| Day 10 | 15x            |
| Day 11 | 10x            |
| Day 12 | 5x             |
| Day 13 | 3x             |
| Day 14 | 2x             |

After day 14, the factor settles at **1x**. Normal length-based pricing (including the discount for handles that contain numbers) still applies underneath the global factor - see [How much does a handle cost?](/#how-much-does-a-handle-cost).

Together, the premine and the launch pricing period aim at the same outcome: desirable handles should go to people who want them most, rather than whoever can script the fastest claim at a flat low price.

## Two sources

The published premine has two parts:

1. **Base premine (CNS + NamesDAO)** - handles derived from viable [Chia Name Service](https://mintgarden.io/collections/chia-name-service-col10r992w4cvasaxjs7ldc0n5hlhl5dklc3x3l2tp405ra6adzczqksnw49f2) and [NamesDAO](https://mintgarden.io/collections/.xch-namesdao-names-col1u9pemm2avjcz8t9emhga4vys5knugsfnctpkk2jyx05jc8d6ch2swe4qvm) legacy registrations as of the **migration cutoff** (**July 20, 2026 at 09:00 UTC**). After that instant, ownership and registration changes on those systems no longer affect the base premine.
2. **Contribution premine** - handles reserved for Chia ecosystem contributors, collected separately and merged into the same published file before launch.

You can look up whether a handle is reserved on the XCHandles launch site. The canonical published file is [`premine.csv`](https://github.com/Yakuhito/nfts/blob/master/premine.csv).

## Base premine (CNS and NamesDAO)

At a high level:

* A legacy name can become a handle if, after taking the substring before the first `.xch` when present (so `scott.xch 8722634` becomes `scott`; names without `.xch` are unchanged), it is already a valid XCHandles handle (**3-63** characters, `a-z` and `0-9` only), or it becomes valid after stripping `-` and `_`.
* CNS is resolved first. NamesDAO only fills handles that CNS did not claim.
* When several records compete for the same handle, deterministic rules pick a single winner (exact spellings beat stripped ones; active registrations beat expired ones; earlier mint time wins remaining ties).
* The premine recipient is the NFT’s owner address at the migration cutoff - specifically the inner puzzle hash encoded as an XCH address - not a forwarding address from metadata.
* Each base-premine handle expires at `max(legacy expiration, launch instant) + 122 days`, so holders keep a meaningful registration window after launch even if their legacy name was near expiry.

Allocation type in the CSV is `cns` or `namesdao`. The allocation explanation links to the winning NFT’s MintGarden page.

## Contribution premine

Handles reserved for ecosystem contributors appear in the published premine with allocation type `contributor`. All contributor premine entries are found in [`contributor-premine.csv`](https://github.com/Yakuhito/nfts/blob/master/contributor-premine.csv)

Contribution requests are messages to [@yakuhito](https://x.com/yakuhito) on X that include a receive address, the requested handle or handles, and a short description of contributions. The contribution deadline is **August 3, 2026 at 09:00 UTC** - requests after that instant are not considered. Confirmed allocations are merged into [`premine.csv`](https://github.com/Yakuhito/nfts/blob/master/premine.csv) before launch. Contribution-premine handles expire on **August 20, 2027 at 09:00 UTC** (one year after launch).

If a handle that is already present in the base premine is requested, the recipient will NOT be changed, but the expiration will be updated to **August 20, 2027 at 09:00 UTC** (one year after launch). All handles updated as such can be found in [`contribuor-extensions.csv`](https://github.com/Yakuhito/nfts/blob/master/contributor-extensions.csv).

## Full selection details

This section is for readers who want the precise base-premine rules. Contribution allocations remain a manual review process; they are not produced by the algorithm below.

### Eligibility

1. Start from CNS NFTs under creator address `xch1zdfcemh4cvcglzx03qu0czlaurt800agghz86c0m5uez4p30dvls8zjc8l` and NamesDAO NFTs under DID `did:chia:13myvry7hmp6nwpa00lqexczka652xkyujyjsecplge8c65rtdl4qd0yya7`.
2. Read the legacy name from off-chain metadata (hash-verified). If the name contains `.xch`, keep only the substring before the first `.xch` and discard the rest (for example `scott.xch 8722634` → `scott`). Names without `.xch` are left unchanged.
3. Classify the result:
   * **Exact** - already a valid handle (`a-z` / `0-9`, length 3-63).
   * **Stripped** - contains `-` or `_`, and removing those characters yields a valid handle.
   * Otherwise the record is skipped (recorded in the warnings CSV when generating from chain data).
4. Derive **legacy expiration** from metadata (not from whether the NFT coin is spent):
   * **CNS** - calendar date `YYYY-MM-DD` interpreted as **23:59:59 UTC** that day.
   * **NamesDAO** - `Expiry` block height projected to UNIX time with a fixed formula: heights below `9_000_000` map to expiration `0`; otherwise `ceil((height - 9_000_001) × 18.75) + 1_783_933_174` (the UNIX time of block `9_000_001` is `1_783_933_174`).
5. A registration is **active** at the migration cutoff if its legacy expiration is strictly after `2026-07-20 09:00:00 UTC`; otherwise it is **expired**. Both active and expired records can still win under the rules below.

**Registration time** is the timestamp of the block in which the NFT’s launcher coin was spent (eve mint), not a MintGarden listing date.

### Per-source resolution (CNS, then NamesDAO)

For each source independently:

1. **Succession** - group records that share the same original legacy string. Prefer the latest **active** registration time; if none are active, prefer the latest expired registration time. Identical timestamps break ties toward the lexicographically smallest NFT id.
2. **Collisions** - group the surviving records by the derived handle. Prefer **Exact** over **Stripped**, then active over expired, then earliest registration time, then lexicographically smallest NFT id.
3. After CNS is fully resolved, run the same process for NamesDAO, but **skip any handle already claimed by CNS**.

Burned NFTs are still eligible: the recipient is whatever inner puzzle hash the NFT coin had at the migration cutoff (including a burn puzzle hash), encoded as an XCH address.

### Expiration written at launch

* Base premine: `max(legacy expiration, 2026-08-20 09:00:00 UTC) + 122 days`.
* Contribution premine: `2027-08-20 09:00:00 UTC`.

To combat hoarding, wallets with more than 10 allocated handles at launch will only be given `122 days` before the handles expire.

### Published CSV columns

```
handle,recipient,expiration,allocation_type,allocation_explanation
```

* `allocation_type` - `cns`, `namesdao`, or `contributor`
* `allocation_explanation` - for base premine, the MintGarden NFT URL for the winning registration (`https://mintgarden.io/nfts/{nft_id}`); for contributors, a URL documenting the allocation
* `expiration` - integer UNIX timestamp (UTC)

Generation from a local chain snapshot also writes a companion warnings CSV (`reason,source,nft_id,original_name,metadata_urls`) for records that could not be turned into candidates (for example missing name or expiration metadata).

## Verifying the CNS / NamesDAO premine

Anyone can independently rebuild and check the **base** premine (CNS + NamesDAO). Contribution rows are maintained separately and merged into the published file; do not expect a base-only rebuild to match [`premine.csv`](https://github.com/Yakuhito/nfts/blob/master/premine.csv) row-for-row unless you filter to `cns` and `namesdao` only.

The open-source tooling lives in the [`nfts`](https://github.com/Yakuhito/nfts) repository. After the migration cutoff, a trustworthy run looks like:

```bash
rm -f nfts.db nfts.db-shm nfts.db-wal

cargo run -- --db nfts.db add \
  'xch1zdfcemh4cvcglzx03qu0czlaurt800agghz86c0m5uez4p30dvls8zjc8l,did:chia:13myvry7hmp6nwpa00lqexczka652xkyujyjsecplge8c65rtdl4qd0yya7'

cargo run -- --db nfts.db sync

cargo run -- --db nfts.db premine generate \
  --output base-premine.csv \
  --warnings premine-warnings.csv

cargo run -- premine confirm base-premine.csv
```

`premine generate` rebuilds the base premine from a fresh local snapshot. `premine confirm` reconstructs the expected set from MintGarden collection and event APIs and compares it exhaustively to the CSV - it never modifies the input file, and exits nonzero on any mismatch.

Compare your confirmed `cns` / `namesdao` rows to the published [`premine.csv`](https://github.com/Yakuhito/nfts/blob/master/premine.csv). After launch, you can also use the [slot-machine](https://github.com/Yakuhito/slot-machine/) `xchandles verify-deployment` command to check that on-chain premine registrations match the trusted CSV bundled with that deployment - see [How do I know that XCHandles was properly deployed?](/#how-do-i-know-xchandles-was-properly-deployed).

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Jul 19th, 2026.*


# Pricing Puzzles

"That will be 7 XCH, sir."

Different registries might have different pricing rules - one registry might to ask for the same amount of payment CATs for any handle, while others might implement dynamic pricing based on custom rules. To accommodate this need, XCHandles was build to have two puzzles: one for determining the price of a handle during registration (and renewal/registration extension), and one for determining the price of an expired handle (during an 'expiry auction').

The way pricing puzzles work was mainly informed by observations from Ethereum Name Service's history. The pricing strategy of a registry might need to change during its lifetime as the registry controllers discovers new data about what's working and what's not. For example, ENS experimented with 2 different ways to handle name expiration before implementing the reverse exponential auction (see 'Exponential Premium' for more details on the technique). This led to the decision to include the two puzzles in the registry's state rather than making them curried arguments. The pricing strategy is set by the price singleton, which can also set a new [CAT Maker](https://docs.catalog.cat/technical-manual/other-useful-concepts#cat-makers). This functionality can always be turned off by melting the price singleton (or by using a custom inner puzzle that restricts what state updates are allowed).

### *Factor Pricing*

*Note*: The factor pricing puzzle can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/default_puzzles/factor_pricing.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/default_puzzles/factor_pricing.clsp)).

This 'factor' pricing strategy allows handles to be priced similar to [ENS domains](https://docs.ens.domains/faq#what-does-it-cost-to-register-a-eth-domain). The price is determined as the base price (a curried in parameter) multiplied by a factor which depends on two variables: the handle's length (shorter handles will be more expensive) and whether the handle contains numbers (handles containing numbers cost half of what their only-characters counterparts do). The table below describes the prices for handle registration/renewal **per year** (to get the price for three years, for example, one would have to multiply the values in the table by 3 - there is no discount).

<table><thead><tr><th width="100">Length</th><th width="130" data-type="checkbox">Has Numbers?</th><th width="100">Factor</th><th width="173">Example Handle</th><th>Cost</th></tr></thead><tbody><tr><td>3</td><td>false</td><td>128</td><td>@abc</td><td>640 wUSDC.b per year</td></tr><tr><td>3</td><td>true</td><td>64</td><td>@ab1</td><td>320 wUSDC.b per year</td></tr><tr><td>4</td><td>false</td><td>64</td><td>@abcd</td><td>320 wUSDC.b per year</td></tr><tr><td>4</td><td>true</td><td>32</td><td>@abc1</td><td>160 wUSDC.b per year</td></tr><tr><td>5</td><td>false</td><td>16</td><td>@abcde</td><td>80 wUSDC.b per year</td></tr><tr><td>5</td><td>true</td><td>8</td><td>@a1234</td><td>40 wUSDC.b per year</td></tr><tr><td>6+</td><td>false</td><td>2</td><td>@example</td><td>10 wUSDC.b per year</td></tr><tr><td>6+</td><td>true</td><td>1</td><td>@example1</td><td>5 wUSDC.b per year</td></tr></tbody></table>

The table assumes a **base price** of 5.00 wUSDC.b (5000 when translated to CAT mojos). Note that the payment token is determined by the CAT maker - wUSDC.b is the token registrations are planned to be priced in after mainnet launch. One year is the normal registration period, but other subregistries may choose other periods - DIG handles, for example, plans to price registrations per week.

More generally, pricing puzzles return `(price . registration_delta_seconds)`, where `price` is the amount of payment CATs to be paid and `registration_delta_seconds` is a positive integer representing the number of seconds the registration should be extended for. Pricing puzzles have a solution that always starts with three truths:

* `Buy_Time`: this is a timestamp verified to be in the past. Not used in the factor pricing puzzle, but added so the solution format is consistent with the exponential premium puzzle solution.
* `Current_Expiration` , which is the verified current expiration of the handle whose price is being quoted. This will only be '0' if the handle is being registered. The factor pricing puzzle does not use this truth.
* `Handle` , a string containing the value being registered. The pricing puzzle is expected to perform validation for this handle. For example, the factor pricing puzzle ensures a handle is 3-63 characters long and only contains 0-9a-z characters.

The two truths may be followed by an arbitrary solution. For the factor pricing puzzle, this solution is simply `num_periods` (formerly `num_years`), which allows users to register a handle for more than 1 period at a time.

### *Exponential Premium*

*Note*: The exponential premium puzzle can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/default_puzzles/exponential_premium.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/default_puzzles/exponential_premium.clsp)).

After a handle expires, determining a good 'next owner' is a difficult problem to solve. A clever solution is currently being used by ENS: whoever wants the handle the most would be willing to pay more for it, so auctioning the handle is a good way to go. The price starts at a high value and then decreases each second - giving everyone the chance to register it at the price they consider 'fair.' Linear pricing (e.g., the price decreases $5/hour) is unlikely to work because starting at a price no one would be willing to pay (e.g., $100 million) would result in an auction that is either too long or too short (the price decreases very rapidly). Instead, XCHandles implements an 'exponential premium' pricing strategy, where the auction starts with a very high value and the premium halves every 24 hours. The table below shows how the premium evolves over a 28-day auction when it starts at 100,000,000 wUSDC.b:

| Time            | Approx. Premium     |
| --------------- | ------------------- |
| Day 0, Hour 0   | 99999999.63 wUSDC.b |
| Day 0, Hour 1   | 97153878.78 wUSDC.b |
| Day 0, Hour 12  | 70710677.75 wUSDC.b |
| Day 1, Hour 0   | 49999999.63 wUSDC.b |
| Day 1, Hour 12  | 35355338.69 wUSDC.b |
| Day 2, Hour 0   | 24999999.63 wUSDC.b |
| Day 3, Hour 0   | 12499999.63 wUSDC.b |
| Day 7, Hour 0   | 781249.628 wUSDC.b  |
| Day 14, Hour 0  | 6103.143 wUSDC.b    |
| Day 21, Hour 0  | 47.311 wUSDC.b      |
| Day 27, Hour 0  | 0.373 wUSDC.b       |
| Day 28, Hour 22 | 0.033 wUSDC.b       |
| Day 28, Hour 23 | 0.019 wUSDC.b       |
| Day 28, Hour 24 | 0.008 wUSDC.b       |

Note that the exponential premium puzzle approximates the floating value of `start_premium / 2**(time_passed /auction_start)` , with an auction taking exactly 28 days. The overall effect is that the premium halves every day, but it also takes intermediary values throughout the day. The staring premium is calculated such that the premium value after the last second is rounded down to 0. The hourly evolution of the premium can be seen in the table below:

<figure><img src="https://1020188380-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FA5k0eLdixiAUrwidcrSf%2Fuploads%2Fgit-blob-002641415fe5d1560a712aed692fc94272514f0f%2Fimage.png?alt=media" alt=""><figcaption><p>Premium decreases exponentially until day 28, when it reaches 0</p></figcaption></figure>

More generally, auction puzzles will have the same return as a normal pricing puzzle, namely `(premium . registration_delta_seconds)`. They will also accept the same three truths:

* `Buy_Time`: this is a verified timestamp that can be treated as 'time now.' The registry makes sure that the timestamp happened before the last transaction block's timestamp, but the user is financially motivated to provide a value as big (late) as possible since the auction puzzle would lower its premium for bigger timestamps.
* `Expiration`: the current expiration of the name, which was checked to be in the past. Note that expiration can't normally be 0.
* `Handle`: string containing the handle being auctioned off. Normally, the handle has already been validated by the other pricing puzzle.

The auction pricing puzzle may then accept an arbitrary solution. The exponential premium puzzle uses the order of truths to run the normal pricing in order to determine the 'true price' of a handle. The first two truths can be prepended to `pricing_program_solution`, which can then be used to evaluate the normal pricing puzzle (curried in via `BASE_PROGRAM` ). This pattern allows the exponential premium puzzle to add the time-dependent premium to a handle's base price, making sure the price at the end of auction is the same as the price if the handle were registered the first time (effectively 'ending' the auction).

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025.*


# State Scheduler

Deterministic state changes

*Note*: The state scheduler puzzle can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/singleton/state_scheduler.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/singleton/state_scheduler.clsp)).

If you're launching a handle registry, one thing to copy from the DNS world is the initial handle auction. During the launch period, handles should be more expensive than normal, with price decreasing until it reaches the terminal ('normal') value. This ensures that people that really want a handle have a chance to register it by paying a premium, while also discouraging a single actor coming in and registering all 'rare' handles.

The state scheduler is a small puzzle intended to exist inside the price singleton of the registry during the launch period. It simply sends a message to the registry, making it update its state to a new one. `INNER_PUZZLE` is used to generate the rest of the coin's conditions - usually, these are a re-creation condition (`CREATE_COIN`) and one that asserts a block height has passed. During the launch period, the price controller singleton is expected to contain a series of state schedulers, with the last one giving control to the 'terminal' inner puzzle hash (e.g., a multisig).

Note that singletons with the state scheduler code can be spent by anyone. Because the price is expected to go down with every update, it is in the user's best interest to update the state before registering a new handle (assuming that's a possibility).

When the price singleton's first generations will contain state schedulers, the `kv_list` argument of the singleton launcher will hint it. Particularly, the format for the variable (defined [here](https://github.com/Yakuhito/chia-wallet-sdk/blob/main/crates/chia-sdk-driver/src/primitives/action_layer/state_scheduler_info.rs)) is `(price_singleton_launcher_id registry_singleton_id final_puzzle_hash state_schedule . final_puzzle_memos)`, where the final puzzle hash specifies the 'custody' inner puzzle hash the price singleton will eventually have and the state schedule is a cons-box list where each item's first value is the relative block height and the second is the updated state. The last argument can be used to describe the final puzzle in more detail (it may contain multisig data, for example).

### Price Schedule CSV

The slot-machine CLI loads a price schedule from CSV when launching and verifying deployments. Format:

```
block_height,asset_id,registration_price,registration_period
```

After launch, run `xchandles unroll-state-scheduler` to apply each scheduled state transition on-chain.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 21st, 2025. Updated Jul 2026.*


# XCHandles

A decentralized address book for the Chia blockchain

From a technical standpoint, XCHandles is, at its core, a `HashMap<bytes32, bytes32>` that transforms the hash of a handle (i.e., `sha256tree` of the handle string) to a singleton launcher id. Entries usually point to a name NFT launcher id, which contains details such as receiver address (resolved as the owner of the NFT), as well as other info in their metadata (e.g., display name and image). Each entry can only be updated or transferred by its owner. Entries also have expiration dates - after registration, anyone may extend the validity of a handle. After the handle expires, it enters an auction process handled by another puzzle (set to the 'exponential premium' puzzle at launch).

Before proceeding, it's highly recommended that you familiarize yourself with the primitives in the [CATalog docs](https://docs.catalog.cat/technical-manual/slots), as XCHandles uses them in more complex ways.

Note that the XCHandles registry also includes a [delegated state action](https://docs.catalog.cat/technical-manual/other-useful-concepts#the-delegated-state-action).

Generally, announcements and messages from the main registry are sent using a one-byte prefix followed by a hash of the actual message contents. To prevent collisions, the prefixes and message structures are defined in a single file ([here](https://github.com/Yakuhito/chia-wallet-sdk/blob/main/crates/chia-sdk-driver/src/primitives/action_layer/xchandles_registry_prefix.rs)).

### Registry State

The registry singleton's inner state (`XchandlesRegistryState`) contains three puzzle hashes:

| Field                                | Purpose                                                                |
| ------------------------------------ | ---------------------------------------------------------------------- |
| `cat_maker_puzzle_hash`              | Puzzle that returns the full payment CAT puzzle hash for registrations |
| `pricing_puzzle_hash`                | Puzzle that quotes registration/renewal prices (and periods)           |
| `expired_handle_pricing_puzzle_hash` | Puzzle that quotes post-expiry auction prices                          |

These are curried at launch and can be updated later via the [delegated state action](https://docs.catalog.cat/technical-manual/other-useful-concepts#the-delegated-state-action).

### Registry Constants

Immutable per-deployment values (`XchandlesConstants`):

| Field                          | Purpose                                     | Default (CLI)                |
| ------------------------------ | ------------------------------------------- | ---------------------------- |
| `launcher_id`                  | Registry singleton launcher id              | Set at launch                |
| `precommit_payout_puzzle_hash` | Where precommit refunds are sent            | `--payout-address` at launch |
| `relative_block_height`        | Blocks between precommit and register       | **32**                       |
| `price_singleton_launcher_id`  | Launcher id of the price multisig singleton | Set at launch                |

### Action Merkle Tree

The registry action layer contains eight puzzles (in Merkle tree order):

1. Expire
2. Extend
3. Oracle
4. Register
5. Initiate Update
6. Execute Update
7. Refund
8. Delegated State

### Announcement and Message Prefixes

**Created announcements**:

| Prefix | Action   | Payload                            |
| ------ | -------- | ---------------------------------- |
| `r`    | Register | Precommit coin full puzzle hash    |
| `$`    | Refund   | Precommit coin full puzzle hash    |
| `e`    | Extend   | `sha256tree(total_price . handle)` |
| `x`    | Expire   | Precommit coin full puzzle hash    |
| `o`    | Oracle   | Slot value hash                    |

**Received messages**:

| Prefix | Purpose                       | Payload                           |
| ------ | ----------------------------- | --------------------------------- |
| `s`    | Update state                  | New `XchandlesRegistryState` hash |
| `a`    | Register owner                | Precommit coin full puzzle hash   |
| `b`    | Register resolved             | Precommit coin full puzzle hash   |
| `e`    | Expire owner                  | Precommit coin full puzzle hash   |
| `f`    | Expire resolved               | Precommit coin full puzzle hash   |
| `i`    | Initiate update               | Update slot value hash            |
| `u`    | Execute update (old owner)    | Update slot value hash            |
| `o`    | Execute update (new owner)    | Update slot value hash            |
| `r`    | Execute update (new resolved) | Update slot value hash            |

### Slots

Handle slots contain data about registered handles and use a nonce of **1**. Slots tracking initiated updates use a nonce of **2**. More information about the data held in these slots can be found under [The Register Action](/technical-manual/xchandles/the-register-action) and [The Initiate Update Action](/technical-manual/xchandles/the-initiate-update-action).

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025. Updated Jul 2026.*


# The Register Action

Where it all starts

*Note*: The register action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/register.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/register.clsp)).

To create an entry in XCHandles, users have to register a handle. While only holding a key inside a slot (and associated neighbors) works for CATalog, XCHandles requires a more complex value structure:

```
(counter . ((handle_hash . (left . right)) . (expiration . (owner . resolved))))
```

In Rue, this is defined as follows:

```rs
export struct HandleSlotNeighbors {
    left_value: Bytes32,
    ...right_value: Bytes32,
}

export struct HandleSlotValueA {
    handle_hash: Bytes32,
    ...neighbors: HandleSlotNeighbors,
}

export struct HandleSlotData {
    owner_launcher_id: Bytes32,
    ...resolved_launcher_id: Bytes32,
}

export struct HandleSlotValueB {
    expiration: Int,
    ...data: HandleSlotData,
}

export struct XchandlesHandleSlotValue {
    counter: Int,
    a: HandleSlotValueA,
    ...b: HandleSlotValueB,
}
```

The values in a slot have the following meaning:

* **counter**: the number of times the slot coin has been spent (used to prevent re-play/forgery)
* **value**: the `sha256tree` of a handle, which can also be computed as `(sha256 1 handle)`
* **left\_value** & **right\_value**: also known as the neighbor struct (or cons-box, to be more clvm-correct), these values 'point to' the left and right neighbors in the doubly-linked list. The rule that enforces uniqueness for handles is simply **left\_value** < **right\_value**
* **expiration**: timestamp after which the handle is considered expired
* **owner**: the launcher id of the singleton that can update the entry (slot value). More precisely, the owner can freely update the **owner** of a slot (i.e., transfer a handle to a new owner) and set the **resolved** (i.e., change the singleton that the handle is resolved to)
* **resolved**: the launcher id of the singleton that the handle resolves to. This will usually be the same as **owner** and point to an NFT with an 'owner-can-update-any-field' metadata updater, but separating the two allows for more flexibility (in custody, for example).

The register action behaves very similarly to the [CATalog register action](https://docs.catalog.cat/technical-manual/catalog/the-register-action), with the mention that it does NOT launch a singleton to represent the name. Instead, it is assumed that the resolved launcher id used during registration corresponds to a valid name NFT that was launched separately. Uniqueness prelaunchers are not used, as names can expire but their associated NFTs cannot be clawed back (and creating a uniqueness prelauncher for the same handle twice defeats its purpose).

At registration, the owner and resolved singleton(s) need to send messages to the registry to confirm the user owns them.

The precommitment coin for XCHandles also holds more values. The reveal of its overall value will be the hash of `(c (c (c cat_maker_hash cat_maker_solution) (c pricing_puzzle_hash pricing_puzzle_solution)) (c (c handle secret) (c owner_launcher_id resolved_launcher_id)))` where:

* **cat\_maker\_details** and **pricing\_puzzle\_details** form the refund sub-struct. These values are confirmed to correspond to those of the current registry state - otherwise, the commitment coin is considered invalid and can be clawed back using the refund action.
* **handle** is the string representing the handle being registered (which will be validated)
* **secret** is used so that the handle registration is impossible to guess or brute-force before the transaction that claims the handle is sent to the mempool
* **owner\_launcher\_id** & **resolved\_launcher\_id** correspond to the new handle (i.e., the one being registered)

Registration time is not stored in the precommit value directly. It comes from the pricing puzzle solution's `buy_time` field (see `HandlePricingSolution` in `xchandles.rue`).

This translates to the following Rue struct:

```rs
export struct PuzzleDetails {
    puzzle_hash: Bytes32,
    ...puzzle_solution: Any,
}

export struct PrecommitValueA {
    cat_maker_details: PuzzleDetails,
    ...pricing_puzzle_details: PuzzleDetails,
}

export struct PrecommitHandleAndSecret {
    handle: String,
    ...secret: Any,
}

export struct PrecommitValueB {
    handle_and_secret: PrecommitHandleAndSecret,
    ...data: HandleSlotData,
}

export struct PrecommitValue {
    a: PrecommitValueA,
    ...b: PrecommitValueB,
}
```

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the register action, the announcement prefix, `r` , is concatenated to the precommit coin's full puzzle hash .

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025. Updated Jul 2026.*


# The Refund Action

Where registrations gone wrong are fixed

*Note*: The refund action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/refund.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/refund.clsp)).

This action is very similar to the [CATalog refund action](https://docs.catalog.cat/technical-manual/catalog/the-refund-action), except that the pricing puzzle might either be a 'normal' or 'expiry auction' one. A precommit coin can be clawed back in the following cases:

* The CAT maker puzzle hash has changed
* The payment CAT amount is wrong
* The pricing puzzle in used has changed
* The handle has already been registered (and is still valid - i.e., has not expired)

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the refund action, the announcement prefix, `$` , is concatenated to the precommit coin's full puzzle hash.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025.*


# The Extend Action

This handle is awesome - may I please have it for longer?

*Note*: The extend action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/extend.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/extend.clsp)).

Most users will want to extend the time their handle registrations. Extending is simpler than a registration because the slot has already been created, meaning some checks (e.g., left vs. right neighbor) can be skipped. Moreover, extending doesn't require a commit-reveal pattern, as the handle is already known and belongs to an owner. The process can thus be compressed to one transaction using offer announcements to assert payment.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the extend action, the announcement prefix, `e`, is concatenated to `(sha256tree (total_price . handle))` .

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025.*


# The Expire Action

Someone didn't renew this awesome handle - let's get it!

*Note*: The expire action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/expire.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/expire.clsp)).

When handles expire, they're auctioned off by using a second pricing puzzle. Usually (and by default), the other pricing puzzle will use the time when the handle expired and the registration time to calculate a premium, which is added to the base price. The premium decreases (exponentially in the puzzle used at launch) as more time passes - essentially creating a public 'auction' where it's in the potential owners' best interest to pay their maximum price for the handle to secure it.

Because the handle is changing owners, precommitment coins are used to solve the concern of potential mempool snipers (malicious farmers). The value of a precommitment coin will be the same as that for registrations. Note that the pricing puzzle will now describe the auction puzzle, not the 'standard one.'

At expiration, the owner and resolved singleton(s) need to send messages to the registry to confirm the user owns them.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the expire action, the announcement prefix, `x`, is concatenated to the precommit coin's full puzzle hash.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025.*


# The Oracle Action

This oracle sees things that are currently true on-chain, not the future...

*Note*: The oracle action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/oracle.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/oracle.clsp)).

CATalog registrations are forever, which allows one to use uniqueness prelauncher to allow quick on-chain and off-chain validation that an NFT indeed represents the entry for a given CAT. XCHandles, on the other hand, has expiring handles - everyone is free to set the resolved launcher id to any singleton, and entries can expire. The oracle is a very simple action that will announce the value of a given slot on-chain. The announcement prefix, `o`, is concatenated with the slot's value hash. It's simple as that!

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025.*


# The Initiate Update Action

This will be your new owner

*Note*: The initiate update action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/initiate_update.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/initiate_update.clsp)).

This action handles owner-specific update abilities for every entry. Namely, owners can transfer ownership of their entry or set it to resolve to another singleton. To do so, the owner singleton must create a coin-puzzle message with the contents of `(sha256tree (c handle_hash (c new_owner_launcher_id new_resolved_launcher_id)))`.

To prevent certain attacks, an update cannot be completed instantly. Rather, the owner must initiate the update, wait for a 'safe' number of blocks (32 - enough to make blockchain re-orgs extremely unlikely), and then complete the update by having the child of the coin that initiated the update send another message to the registry.

The singleton keeps track of initiated updates via update slots (nonce **2**). An update slot's value is:

```
((initiator_coin_id . min_execution_height) . (handle_hash . (new_owner . new_resolved)))
```

In Rue:

```rs
export struct UpdateSlotValueA {
    initiator_coin_id: Bytes32,
    ...min_execution_height: Int,
}

export struct UpdateSlotValueB {
    handle_hash: Bytes32,
    ...new_data: HandleSlotData,
}

export struct XchandlesUpdateSlotValue {
    a: UpdateSlotValueA,
    ...b: UpdateSlotValueB,
}
```

`min_execution_height` is an absolute block height (not a relative block count).

This puzzle does not create a puzzle announcement, as the owner can be assured the update is initiated only if the message sent to the singleton is consumed. An initiated update is canceled simply by spending the child of the owner singleton coin that initiated it.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 15th, 2025. Updated Jul 2026.*


# The Execute Update Action

This is your new owner

*Note*: The execute update action code can be found [here](https://github.com/Yakuhito/slot-machine/blob/master/rue-puzzles/actions/xchandles/execute_update.rue) ([Chialisp](https://github.com/Yakuhito/slot-machine/blob/master/puzzles/actions/xchandles/execute_update.clsp)).

Once an update has been [initiated](/technical-manual/xchandles/the-initiate-update-action) and a minimum number of blocks has passed, the owner may execute the update by calling this action. In doing so, messages must also be sent from the new singleton(s) that the handle will be owned by and point to.

This puzzle does not create a puzzle announcement, as the owner can be assured the update is executed only if the message sent to the singleton is consumed.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 2nd, 2026.*


# CLI

Command-line interface for XCHandles

The [slot-machine](https://github.com/Yakuhito/slot-machine) repository provides a CLI for interacting with XCHandles on-chain. Puzzle source lives in slot-machine; spend construction uses [chia-wallet-sdk](https://github.com/Yakuhito/chia-wallet-sdk) drivers.

### Invocation

```bash
cargo r xchandles <subcommand> [flags]
```

Or, after building: `slot-machine xchandles <subcommand> [flags]`.

### Prerequisites

* **Sage wallet** - the Sage RPC should be running locally.
* **Coinset RPC** - used for chain sync and broadcasting (mainnet or testnet11).
* **Neighbor API** - register, extend, and expire commands need left/right slot neighbors. By default the CLI queries `https://api.xchandles.com`; use `--local` to query a local indexer started with `xchandles listen`.

### Common Flags

| Flag          | Default  | Description                                              |
| ------------- | -------- | -------------------------------------------------------- |
| `--testnet11` | `false`  | Use testnet11 consensus constants and coinset            |
| `--fee`       | `0.0025` | Transaction fee in XCH                                   |
| `--local`     | `false`  | Use local SQLite index (`data.db`) instead of remote API |
| `--log`       | `false`  | Write spend bundle debug output to `sb.debug`            |
| `--debug`     | `false`  | Debug signing mode (prompt for private key)              |

### License

slot-machine is licensed under the [Prosperity Public License](https://prosperitylicense.com/). Commercial use requires a license after the trial period.

### Further Reading

* [XCHandles Commands](/technical-manual/cli/xchandles) — full command reference


# XCHandles Commands

XCHandles CLI command reference

All commands are invoked as `cargo run -- xchandles <command> [flags]`.

***

## Launch

### initiate-launch

Creates the XCHandles registry singleton, price singleton (medieval vault), state scheduler, and premine payment CAT.

```bash
cargo run -- xchandles initiate-launch \
  --pubkeys <comma-separated-hex-pubkeys> \
  -m <threshold> \
  --payout-address <xch/txch address> \
  [--relative-block-height 32] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--fee 0.0025]
```

| Flag                      | Default    | Description                                    |
| ------------------------- | ---------- | ---------------------------------------------- |
| `--pubkeys`               | (required) | Comma-separated price singleton signer pubkeys |
| `-m`                      | (required) | Multisig threshold                             |
| `--payout-address`        | (required) | Where precommit refunds are sent               |
| `--relative-block-height` | `32`       | Blocks between precommit and register          |
| `--registration-period`   | `31557600` | Registration period in seconds (\~1 year)      |

Outputs: registry `launcher_id`, price singleton id, premine payment asset id.

### continue-launch

Registers premine handles from CSV and mints name NFTs.

```bash
cargo run -- xchandles continue-launch \
  --launcher-id <hex> \
  --payment-asset-id <hex> \
  --royalty-address <address> \
  --handles-per-spend <n> \
  [--start-time <unix>] \
  [--skip 0] \
  [--royalty-basis-points 1000] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--fee 0.0025]
```

Reads `xchandles_premine_testnet11.csv` (or mainnet equivalent) from the slot-machine repo root.

### unroll-state-scheduler

Applies block-height-scheduled price changes from the price schedule CSV on-chain.

```bash
cargo run -- xchandles unroll-state-scheduler \
  --launcher-id <hex> \
  [--testnet11] \
  [--local] \
  [--fee 0.0025]
```

***

## verify-deployment

Validates an on-chain deployment against trusted CSV files.

```bash
cargo run -- xchandles verify-deployment \
  --launcher-id <hex> \
  [--testnet11]
```

Checks:

* Registry launcher solution and initial state
* Premine handles match `xchandles_premine_testnet11.csv` (or mainnet CSV)
* Price schedule matches `xchandles_price_schedule_testnet11.csv` (or mainnet CSV)
* Price singleton multisig configuration

***

## Register

Two-phase registration: creates a precommit coin, then (after `relative_block_height` blocks) spends the registry to claim the handle.

```bash
cargo run -- xchandles register \
  --launcher-id <hex> \
  --handle <name> \
  --nft <nft1...> \
  --payment-asset-id <hex> \
  --payment-cat-base-price <amount> \
  [--num-periods 1] \
  [--refund-address <address>] \
  [--secret <hex>] \
  [--start-time <unix>] \
  [--refund] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--local] \
  [--log] \
  [--fee 0.0025]
```

**Phase 1** (first run): creates the precommit coin. The CLI prints a follow-up command.

**Phase 2** (after block delay): completes registration with the printed command.

**Refund path**: add `--refund` to claw back a precommit coin via the refund action (e.g., wrong payment, handle already taken).

***

## extend

Extends a handle's registration period in a single transaction.

```bash
cargo run -- xchandles extend \
  --launcher-id <hex> \
  --handle <name> \
  --payment-asset-id <hex> \
  --payment-cat-base-price <amount> \
  [--num-periods 1] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--local] \
  [--fee 0.0025]
```

***

## initiate-update

Starts a two-phase handle transfer to a new NFT.

```bash
cargo run -- xchandles initiate-update \
  --launcher-id <hex> \
  --handle <name> \
  --new-nft <nft1...> \
  [--min-height <block>] \
  [--testnet11] \
  [--local] \
  [--fee 0.0025]
```

`--min-height` defaults to current peak + 32 blocks.

***

## execute-update

Completes a previously initiated handle update after the minimum block height.

```bash
cargo run -- xchandles execute-update \
  --launcher-id <hex> \
  --handle <name> \
  --new-nft <nft1...> \
  [--testnet11] \
  [--local] \
  [--fee 0.0025]
```

***

## expire

Re-registers an expired handle via the expiry auction pricing puzzle.

```bash
cargo run -- xchandles expire \
  --launcher-id <hex> \
  --handle <name> \
  --nft <nft1...> \
  --payment-asset-id <hex> \
  --payment-cat-base-price <amount> \
  [--expire-time <unix>] \
  [--num-periods 1] \
  [--refund-address <address>] \
  [--secret <hex>] \
  [--refund] \
  [--committed-expiration <unix>] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--local] \
  [--fee 0.0025]
```

Uses the same two-phase precommit pattern as register.

***

## listen

Runs a local neighbor API and syncs registry spends.

```bash
cargo run -- xchandles listen \
  --launcher-ids <id1>,<id2> \
  [--testnet11]
```

Serves `http://localhost:3000` with a `GET /neighbors?launcher_id=&handle_hash=` endpoint (same contract as `https://api.xchandles.com`).

***

## view

Prints current registry state.

```bash
cargo run -- xchandles view \
  --launcher-id <hex> \
  [--payment-asset-id <hex>] \
  [--payment-cat-base-price <amount>] \
  [--registration-period 31557600] \
  [--testnet11]
```

Payment hints are optional overrides for price display.

***

## sign-state-update

A price singleton signer produces a partial signature for a proposed registry state change.

```bash
cargo run -- xchandles sign-state-update \
  --launcher-id <hex> \
  --new-payment-asset-id <hex> \
  --new-payment-cat-base-price <amount> \
  [--new-registration-period 31557600] \
  --my-pubkey <hex> \
  --multisig-launcher-id <hex> \
  [--payment-asset-id <hex>] \
  [--payment-cat-base-price <amount>] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--debug]
```

***

## broadcast-state-update

Collects multisig signatures and broadcasts the state update.

```bash
cargo run -- xchandles broadcast-state-update \
  --launcher-id <hex> \
  --new-payment-asset-id <hex> \
  --new-payment-cat-base-price <amount> \
  [--new-registration-period 31557600] \
  --multisig-launcher-id <hex> \
  --signatures <hex,...> \
  [--payment-asset-id <hex>] \
  [--payment-cat-base-price <amount>] \
  [--registration-period 31557600] \
  [--testnet11] \
  [--fee 0.0025]
```

See the [CATalog delegated state action docs](https://docs.catalog.cat/technical-manual/other-useful-concepts#the-delegated-state-action).


# Reward Distributors

Efficient continuous payouts

First detailed in [DIP-0002](https://github.com/DIG-Network/DIPS/blob/main/DIPs/dip-0002.md) and later in [CHIP-0051](https://github.com/Chia-Network/chips/pull/165), the Reward Distributor can be seen as an efficient way to distribute rewards at a constant, per-epoch rate among a dynamic group of participants. Reward distributors come in three main flavors.

<table><thead><tr><th width="95.5">Type</th><th>Description</th></tr></thead><tbody><tr><td>Managed</td><td>A manager sets and removes entries with varying weights. In DIG's use, the manager sets and removes active mirrors, each with a corresponding number of shares. During 1-week epochs, rewards are continuously allocated to active mirrors based on their shares.</td></tr><tr><td>NFT</td><td>This mode allows users to stake NFTs to earn rewards. <strong>Collection NFT Reward Distributors</strong> allow any NFT minted by a particular DID to be staked, assigning a weight of one to each. <strong>Curated NFT Reward Distributors</strong> operate based on a whitelist containing the launcher ids of stakeable NFTs, as well as their distribution weight. Such reward distributors may be <strong>refreshable</strong> (if the NFT's weight on the whitelist is changed, anyone may restake it to make the NFT earn rewards proportional to its new weight) or <strong>non-refreshable</strong> (an NFT's weight is set when the NFT is staked and cannot be updated unless the NFT is unstaked by its owner and staked again). For all NFT distributors, owners may unstake their NFTs at any time, reclaiming the NFT while stopping the accumulation of rewards.</td></tr><tr><td>CAT</td><td>Similar to NFT types, CAT Reward Distributors allow anyone owning a particular CAT to stake it to earn proportional rewards. Owners may, at any time, unstake their CATs, which stops reward accumulation.</td></tr></tbody></table>

In all variations of the distributor, anyone can commit funds to be distributed during current and future epochs. For future epochs, funds may also be clawed back - however, a fee will be incurred (to disincentivized 'fake' commitments). This ensures that, should a manager misbehave (i.e., falsely report one or more mirror statuses on-chain), farm incentive providers have a way of stopping the majority of manager fees and changing to another farm. Manager fees are proportional to committed funds and are paid out at the start of each epoch. Payouts may be initiated by anyone or solely by the recipient via a message - this behavior is set at distributor launch and may not be changed.

The DIG Reward Distributor (manager mode) was mainly designed by [Michael Taylor ](https://github.com/MichaelTaylor3D)and implemented in Chialisp by [yakuhito](https://github.com/yakuhito). The NFT mode (DID/collection) was later added for community use. The first UI, [rewards.fireacademy.io](https://rewards.fireacademy.io/), powered the alpha deployment of the first version of distributors, allowing [DataLayer Minions](https://mintgarden.io/collections/datalayer-minions-col1k86fjeaje70hhy46yp4c2jfuhddlt66zcd6mw86zzy2d4egage3sa302t4) NFT holders to stake their minions for DIG tokens. A [precision issue](https://blog.fireacademy.io/p/too-discrete-to-handle-how-low-cat) was identified as a result of the alpha. The bug was fixed with the introduction of the latest (V2) version of the distributor, which also added the other two modes, allowing curated NFT and CAT reward distributors.

Generally, announcements and messages from the main registry are sent using a one-byte prefix followed by a hash of the actual message contents. To prevent collisions, the prefixes and message structures are defined in a single file ([here](https://github.com/Yakuhito/chia-wallet-sdk/blob/main/crates/chia-sdk-driver/src/primitives/action_layer/reward_distributor_prefix.rs)).

### State

Unlike XCHandles and CATalog, the DIG Reward Distributor holds a complex state that may not be arbitrarily updated by an external singleton. Instead, each action can update the dynamic state according to well-defined rules. The state of the reward distributor is `(c total_reserves (c active_shares (c round_reward_info round_time_info)))` . The first argument is used to keep track of all the funds the DIG Reward Distributor holds. The second argument tracks the number of currently active shares, which is important for reward calculation. `round_reward_info` is defined as `(cumulative_payout . remaining_rewards)` and tracks the cumulative payout per share since launch and the remaining amount of rewards in the current epoch, respectively. Lastly, `round_time_info` is defined as `(last_update . epoch_end)` and tracks the time when the reward distribution (cumulative payout) was last calculated, as well as the timestamp when the epoch ends. In Rue, the state is defined as follows:

```rs
export struct RewardInfo {
    cumulative_payout: Int,
    ...remaining_rewards: Int,
}

export struct RoundTimeInfo {
    last_update: Int,
    ...epoch_end: Int,
}

export struct RewardDistributorState {
    total_reserves: Int,
    active_shares: Int,
    reward_info: RewardInfo,
    ...round_time_info: RoundTimeInfo,
}
```

### Reserve

Another difference when compared to XCHandles/CATalog is that the DIG Reward Distributor manages funds. Specifically, a CAT determined at launch is used for rewards. This means that the reward distributor uses a single-reserve finalizer, as described [here](https://docs.catalog.cat/technical-manual/action-layer). The finalizer automatically detects output conditions starting with the special opcode `-42`and makes the reserve output them instead of the singleton - this is useful for facilitating payouts. Moreover, the finalizer automatically re-creates the reserve, ensuring it stores an amount of `total_reserves` mojos (taken from the latest state).

### Slots

The DIG reward distributor is the first app to use different types of slots. As mentioned [here](https://docs.catalog.cat/technical-manual/slots), differentiating slots can be done by wrapping the 1st curried puzzle by a nonce layer and using different nonces for different kinds of slots. The slots of a reward distributor are defined as follows:

* **Nonce 1**: Reward slots. These hold `(epoch_start next_epoch_initialized . rewards)`. The epoch start timestamp is considered the slot's 'key' and should be unique. The second argument is 1 if the next epoch has been initialized - i.e., a slot exists with a key of `epoch_start + EPOCH_SECONDS`. The last value in the slot keeps track of the total amount of rewards committed to the epoch, including the manager fee (which will be deducted when the epoch starts). Note that a slot will no longer be accurate when the epoch starts, as anyone can add rewards to the current epoch (which does not modify 'rewards'). Moreover, undistributed rewards from previous epochs (i.e., 'change' that cannot be distributed to mirrors) will also be added to the rewards of the epoch. For example, if there are 7 shares, it's possible that a change of 5 mojos might exist, which will be 'carried over' the next epoch. Lastly, the manager will be paid out \~x% of the rewards, where x is a constant defined at reward distributor launch.
* **Nonce 2**: Commitment slots. These hold `(epoch_start clawback_ph . rewards)`and function as 'tickets' that allow users that committed funds to a future epoch to claw part of their funds back.
* **Nonce 3**: Entry slots. These hold `(payout_puzzle_hash initial_cumulative_payout . shares)` . The first argument identifies an entry (e.g., mirror) by its payout puzzle hash. Shares describe how much an entry should be rewarded (approx. `shares / total_shares * rewards_per_second` for every second). The cumulative payout describes how much a theoretical entry holding 1 share that has been active since registry launch should be paid. By calculating the difference between the current value and the value when the entry was added, we can say how much a share needs to be paid. Since all shares are paid equally, multiplying the difference by the amount of shares yields the payout amount for a given recipient. In V2, stake/unstake puzzles allow stakes to be consolidated, allowing multiple staked entries (NFTs/CATs) to be 'stored' inside a single entry slot - thus ensuring payouts only use one coin.
* **Nonce 4**: Deposit slots. These hold `(payout_puzzle_hash shares . launcher_id_or_cat_amount)`. The locking puzzle creates one at stake; the matching unlocking puzzle spends it at unstake. Refresh spends the old deposit slot and creates a new one. The hint is the custody/payout puzzle hash, same as entry slots. For CATs, both `shares` and `launcher_id_or_cat_amount` are the CAT amount. For NFTs, `shares` is the credited weight and the third field is the NFT launcher id. Unlike entry and reward slots, deposit slots have no counter - identical triples share one puzzle hash. In Rue:

```rs
export struct DepositSlotValue {
    payout_puzzle_hash: Bytes32,
    shares: Int,
    ...launcher_id_or_cat_amount: Bytes32 | Int,
}
```

Note that nonce 0/None (i.e., no nonce) is not used for the Reward Distributor.

### Available Actions

For non-manager modes, the [Add Entry](/technical-manual/reward-distributors/managed-reward-distributor/the-add-entry-action) and [Remove Entry](/technical-manual/reward-distributors/managed-reward-distributor/the-remove-entry-action) actions are replaced by [Stake](/technical-manual/reward-distributors/other-reward-distributors/the-stake-action) and [Unstake](/technical-manual/reward-distributors/other-reward-distributors/the-unstake-action) actions, each containing a specific locking puzzles (as detailed on the stake action page). Moreover, the [Initiate Payout](/technical-manual/reward-distributors/the-initiate-payout-action) action can be set to one of two puzzles, depending on whether the recipient needs to approve a payment or not.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Aug 28th, 2026.*


# The Sync Action

Where the interesting math is found

*Note*: The sync action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/tree/main/rue-puzzles/actions/reward_distributor/sync.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/sync.clsp)).

The sync action is used to distribute rewards since the last reward distribution. The cumulative payout grows by `(/ (* remaining_rewards (- update_time last_update)) (* active_shares (- epoch_end last_update)))` . In essence, `(/ (- update_time last_update) (- epoch_end last_update))` represents the fraction of time passed compared to the remaining time in the round. Multiplying this by the remaining rewards amount yields the rewards to be distributed. Dividing the results by the number of active shares will return the change in cumulative payout, which is always defined for one share. Note that this result is 'rounded down' by division operations, and that the 'change' is stored in the new state's `remaining_rewards`.

Syncing can only be done for the current epoch - when the epoch ends (i.e., `last_update = epoch_end`), the 'new epoch' action must be called.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the sync action, the announcement prefix, `s`, is concatenated to `(sha256tree (update_time . epoch_end))`.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# The New Epoch Action

Transitions to a new reward period

*Note*: The new epoch action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/new_epoch.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/new_epoch.clsp)).

Because reward rates might change, the Reward Distributor uses the concept of 'epochs', which are simply one-week periods. The rewards committed to each epoch may be different. Inside each round, rewards are distributed linearly (e.g., \~25% of the rewards will be distributed after 25% of the time in the round has passed).

Starting a new epoch requires a 'slot oracle' - spending and recreating a reward slot with the same value. This ensures that the correct rewards are allocated. The slot either:

* Represents the new epoch, in which case it contains the reward amount
* Represents a previous epoch but indicates that the next slot is not initialized, in which case the reward amount for the epoch to be initialized is 0

When a new epoch starts, the committed rewards, which were previously stored as CAT units (i.e., 1 = one mojo), are multiplied by PRECISION (usually `u64::MAX`), allowing a much more precise reward allocation to all entries.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the new epoch action, the announcement prefix, `e`, is concatenated to `(sha256tree epoch_end)`.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# The Add Incentives Action

Current epoch rocks - let's add some rewards to it

*Note*: The add incentives action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/add_incentives.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/add_incentives.clsp)).

This action simply adds incentives to the current round. The new rewards will be continuously distributed in a linear fashion from `last_update` until `epoch_end`. They cannot be clawed back.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the add incentives action, the announcement prefix, `i`, is concatenated to `(sha256tree (amount . last_update))`.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# The Commit Incentives Action

"I'll probably incentivize this future round"

*Note*: The commit incentives action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/commit_incentives.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/commit_incentives.clsp)).

Unlike adding incentives, commitments are not necessarily binding. Rewards committed to a future epoch can be clawed back (minus a % fee) until the corresponding epoch starts.

This action can affect slots in two different ways. First, if the reward slot corresponding to the epoch the commitment is made for exists, the action will simply spend the slot and re-create it with higher rewards. Second, if the slot is not initialized, the user will provide the last initialized slot. In that case, the action will initialize all slots between the lastly initialized reward slot and the target one. A newly-initialized slot reward is 0 unless it's the target epoch, in which case the rewards will be set to the commitment amount.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the commit incentives action, the announcement prefix, `c`, is concatenated to `(sha256tree (epoch_time next_epoch_time . total_rewards))` , which is also the value hash of the commitment slot that is created.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# The Withdraw Incentives Action

I'll take my funds elsewhere

*Note*: The withdraw incentives action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/withdraw_incentives.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/withdraw_incentives.clsp)).

Users that committed incentives to a future epoch can call this action to 'claw back' their commitment. This is done by sending a message from the `clawback_ph` specified when the rewards were committed, which is assumed to be a custody puzzle (hence, the refund will be sent to the same puzzle).

The action does not create a puzzle announcement. Instead, the user must send a puzzle-puzzle (18) message to the Reward Distributor with no contents. If the user has multiple commitments, they should assert that the correct commitment slot is spent - commitment slots are only spent when commitments are clawed back.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# The Initiate Payout Action

I know the tokens are mine - I would like them now, please. \~ Entry

*Note*: The initiate payout action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/initiate_payout_with_approval.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/initiate_payout_with_approval.clsp)) (with approval) & [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/initiate_payout_without_approval.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/initiate_payout_without_approval.clsp)) (without approval).

This action initiates a payout without requiring an entry to be removed (or assets to be unstaked). Its behavior may be changed at setup, where the deployer determines whether the payout requires approval by the payout/custody puzzle hash or not. If there is no approval needed, the initiate payout action is public, and can be called by anyone to trigger a payout transaction to an entry's puzzle hash. If an approval is needed, the entry puzzle hash must send a specific message to the reward distributor to claim the payout.

To prevent wallet dusting, the accumulated amount must be at least `PAYOUT_THRESHOLD`, which is set at reward distributor launch.

While an entry's rewards are calculated using `cat_unit * PRECISION`, payouts can only be done in CAT units. For every payout, the difference between `cat_payout_amount * PRECISION` (the amount paid out in CAT units expressed with `PRECISION`) and the entry's total amount to be paid out, which is guaranteed to correspond to less than a mojo, is set to be distributed to everyone for the remainder of the period - in some sense, the payout initiator frees the entry's claim to that amount.

The action also creates a puzzle announcement that can be asserted to ensure the dApp singleton is running the right action with the right parameters. For the initiate payout action, the announcement prefix, `p`, is concatenated to `(sha256tree (c entry_payout_puzzle_hash entry_payout_amount))` .

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# Managed Reward Distributor

Let me talk to your manager

Initially, reward distributors were only available in the managed mode. This attribute describes how entries are added to the distributor. In this case, an external coin - the manager singleton - sends a message to the reward distributor to add or remove entries at any time. The manager may also set custom distribution weights (shares) to entries. The two actions in this group are *only* present in the action merkle tree of a managed reward distributor.


# The Add Entry Action

Welcome, fren

*Note*: The add entry action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/manager/add_entry.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/manager/add_entry.clsp)).

This action adds an entry to the active entries of the reward distributor. The entry immediately starts earning rewards in the active epoch.

The action does not create a puzzle announcement. Instead, the manager must send a puzzle-puzzle (18) message to the Reward Distributor with the contents of `a` + `(sha256tree (entry_payout_puzzle_hash . entry_shares))`.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# The Remove Entry Action

Mirror is offline - remove it from the active entry list

*Note*: The remove entry action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/manager/remove_entry.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/manager/remove_entry.clsp)).

This action is called by the manager to stop paying rewards to a particular entry. The entry is automatically paid out the remaining amount (i.e., the rewards the owner did not claim). Given the amount to be paid out is calculated in `PRECISION` units, the difference between `cat_payout_amount * PRECISION` and the total to be paid out, which is guaranteed to be lower than 1 CAT unit, is re-allocated to be redistributed in the current or future epochs.

The action does not create a puzzle announcement. Instead, the manager must send a puzzle-puzzle (18) message to the Reward Distributor with the contents of `r` + `(sha256tree (entry_payout_puzzle_hash . entry_shares))`.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Feb 16th, 2025.*


# Other Reward Distributors

Here's the one for the NFT/CAT fans

The NFT mode of the reward distributor allows anyone holding an NFT (from a particular collection or on a particular curated list) to stake it in order to create an entry. The CAT modes allows anyone to stake a CAT with a particular asset id (set at launch) to the distributor to create an entry with a proportional number of shares. The first two actions in this group are *only* present in the action merkle tree of an NFT or CAT reward distributor. The third/last action is only available for refreshable curated NFT distributors.


# The Stake Action

NFT/CAT: locked.

*Note*: The stake action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/staking/stake.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/staking/stake.clsp)). The code for locking puzzles can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/tree/main/rue-puzzles/actions/reward_distributor/staking/locking_puzzles) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/tree/main/puzzles/actions/reward_distributor/staking/locking_puzzles)).

This action adds an entry to the active entries of the reward distributor. The entry immediately starts earning rewards in the active epoch. A general locking puzzle asserts the appropriate asset with the appropriate amount has been deposited to the distributor - locked to the distributor's `p2_singleton` (`my_p2`), not a nonce wrapper around it - by using a settlement payments announcement. The settlement memo/hint is `tree_hash((custody_puzzle_hash, my_p2))`. Each locking puzzle also creates a deposit slot proving `(custody_puzzle_hash, shares, launcher_id or cat_amount)`.

In general, a locking puzzle returns the number of newly locked shares and a list of conditions, which will become part of the condition list returned by the action. The stake action also allows slot consolidation: if the user already has an entry slot, they can specify it so all their shares are represented by a single slot instead of multiple ones. This means that, at payout, accumulated rewards are paid out in a single coin.

There are three locking puzzles for the reward distributor, detailed below.

### NFTs From DID

This locking puzzle is used for collection NFT distributors. To prove the NFT belongs to a collection, a proof is used to calculate the launcher id starting with the minter DID inner puzzle hash (and going through all the intermediary coins). The NFT with the resulting launcher id is then checked to be sent to a singleton-controlled inner puzzle. With this locking puzzle, every locked NFT is worth exactly one share - in other words, no NFT in the collection is allocated more than any other NFT from the collection in a given second. The deposit slot records that one share and the NFT launcher id.

Because collections are usually part of the off-chain metadata instead of on-chain attributes, this locking puzzle actually proves NFTs have been minted by a particular DID. The puzzle assumes that the given DID only minted ONE collection - any NFT minted by the DID set at launch can be staked, which means the DID owner needs to be trusted not to maliciously mint many NFTs and stake all of them to collect future rewards. In general, while this category of NFT distributors is more trustless than managed reward distributors, it may still require trust. The next locking puzzle, for example, assumes the DataStore is operated as expected and not compromised. Lastly, the CAT reward distributor trusts the CAT's TAIL and its issuer (should they have the ability to increase the CAT supply).

### NFTs From DL

DL stands for DataLayer. Curated NFT reward distributors allow any NFT on a whitelist to be staked, with the weight of a given NFT also being specified on the said list. The list is stored in a Merkle tree inside a DataLayer store, with the trusted launcher id being set at distributor launch. The locking puzzle requires `nft_shares > 0` and writes that weight plus the NFT launcher id into the deposit slot. If the weight of the NFT in the list is changed by the store owner, the NFT may or may not be restaked via the [Refresh Action](/technical-manual/reward-distributors/other-reward-distributors/the-refresh-action).

### CAT

As the name suggests, the CAT locking puzzle allows a CAT with a certain asset id to be locked in exchange for a number of shares equal to its amount. The amount must be greater than 0. Both `shares` and `launcher_id_or_cat_amount` on the deposit slot are that amount.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Aug 28th, 2026.*


# The Unstake Action

I would like that NFT back, please.

*Note*: The unstake action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/staking/unstake.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/staking/unstake.clsp)). The code for unlocking puzzles can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/tree/main/rue-puzzles/actions/reward_distributor/staking/unlocking_puzzles) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/tree/main/puzzles/actions/reward_distributor/staking/unlocking_puzzles)).

This action removes one or more entries from the active entries list of the reward distributor while returning the corresponding locked asset to its owner. Because slots may be consolidated for the same payout puzzle hash (i.e., represent more than one locked asset) and this action has code shared between all non-manager types of reward distributors, an unlocking puzzle is used to give the unstake action specificity. The unlocking puzzle returns the number of shares the unstake operation should remove for the given slot, as well as a list of conditions that assert the owner is trying to unstake (usually via messages) and unlock the assets (usually by sending a message containing an appropriate delegated puzzle to the locked coins).

The unstake action triggers a payout for the whole slot, ensuring it is synced before entries are removed. There are only two unlocking puzzles, one for NFTs and one for CATs. Each spends the matching deposit slot - `(custody_puzzle_hash, shares, launcher_id or cat_amount)` - and sends a delegated puzzle to the locked coin at `my_p2`. The CAT unlocker requires `cat_shares > 0`. The NFT unlocker requires `nft_shares >= 0`. The generic unstake action only checks that `shares_to_remove` is at most the entry slot's shares. Extra unlock behavior (e.g., a lockup period) would need new locking and unlocking puzzles; the stake and unstake actions could remain unchanged.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Aug 28th, 2026.*


# The Refresh Action

Because everyone needs NFTs with dynamic weights, for some reason

*Note*: The refresh action code can be found [here](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/rue-puzzles/actions/reward_distributor/staking/refresh_nfts_from_dl.rue) ([Chialisp](https://github.com/DIG-Network/reward-distributor-clsp/blob/main/puzzles/actions/reward_distributor/staking/refresh_nfts_from_dl.clsp)).

The refresh action is the newest addition to the reward distributor repertoire. It was added to address a particular short-coming of the curated NFT distributor in some cases. Particularly, after an NFT is staked, the DataLayer store owner may update the NFT's weight. The *non-refreshable* curated NFT reward distributor behavior is that the NFT's weight in the distributor (number of shares) is not changed until the owner unstakes the NFT. This has advantages in some cases, but not all.

The refresh action allows curated NFT reward distributors to be refreshable. Anyone can trigger this action to essentially restake a set of NFTs whose weights mismatch those in the DataLayer store. Presumably, the owner themselves would call it after the store is updated - if that is not the case, a third party observer may.

The NFT's weight lives in its deposit slot, not in a nonce on the `p2_singleton` lock. Refresh proves the new DataLayer leaf, spends the old deposit slot (`old_shares`), creates a new deposit slot (`new_shares`), and restakes the NFT still at `my_p2` - the delegated puzzle recreates the locked coin there with the same settlement hint. The share delta must be nonzero; both new and old shares must be `>= 0`. This thus allows curated NFT reward distributors to respond to updates of dynamic lists, where weights (e.g., an NFT's rarity) may be changed at any time by the store owner.

*Written by* [*yakuhito*](https://x.com/yakuhito) *from* [*FireAcademy.io*](https://fireacademy.io/) *on Aug 28th, 2026.*


