> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ton.org/llms.txt
> Use this file to discover all available pages before exploring further.

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.ton.org/feedback

```json
{
  "path": "/standard/tokens/nft/reference",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# NFT: reference implementation

The NFT standard ([TEP-62](https://github.com/ton-blockchain/TEPs/blob/0d7989fba6f2d9cb08811bf47263a9b314dc5296/text/0062-nft-standard.md)) describes the required messages and get-methods. The NFT royalty standard ([TEP-66](https://github.com/ton-blockchain/TEPs/blob/0d7989fba6f2d9cb08811bf47263a9b314dc5296/text/0066-nft-royalty-standard.md)) describes a standard way to implement royalty information for NFT sales. The token data standard ([TEP-64](https://github.com/ton-blockchain/TEPs/blob/0d7989fba6f2d9cb08811bf47263a9b314dc5296/text/0064-token-data-standard.md)) describes available formats for metadata of tokens. A widely used [reference implementation](https://github.com/ton-blockchain/token-contract/tree/1182ad99413242f09925d50e70ccb7e0e09f94d4/nft) follows these standards for simple NFTs with only the essential mechanics. This page explains the reference implementation, what you can extend, and what must remain unchanged.

The [nft-contract repository](https://github.com/ton-blockchain/nft-contract) contains multiple smart contracts written in [FunC](/languages/func/overview) language, including additional ones for NFT sales. This page describes the collection and item smart contracts, as they are the only necessary ones per standard.

## Collection

The full source code is in [nft-collection.fc](https://github.com/ton-blockchain/token-contract/blob/1182ad99413242f09925d50e70ccb7e0e09f94d4/nft/nft-collection.fc) file.

The TEP-62 standard only requires the implementation of get‑methods: `get_collection_data()`, `get_nft_address_by_index(int index)`, and `get_nft_content(int index, cell individual_content)`. The TEP‑66 standard also adds the `royalty_params()` get‑method, a `get_royalty_params` message for requesting royalty parameters on‑chain, and a `report_royalty_params` message for the response containing these parameters.

All other behavior is implementation‑specific. This includes any kind of modifications to the logic of minting new items, managing ownership of the collection, changing metadata, and other features. The reference implementation, for example, implements the ownership management feature and two ways of minting items: singular and batched. Both of these are outside the scope of the standards but are common and needed in most cases. These features are optional and can be removed or replaced.

| Capability                                                           | Requirement                                    |
| -------------------------------------------------------------------- | ---------------------------------------------- |
| `get_collection_data` (get-method)                                   | Required by TEP-62                             |
| `get_nft_address_by_index` (get-method)                              | Required by TEP-62                             |
| `get_nft_content` (get-method)                                       | Required by TEP-62, data format follows TEP-64 |
| `royalty_params` (get-method)                                        | Required by TEP-66                             |
| `get_royalty_params` (inbound internal message)                      | Required by TEP-66                             |
| `report_royalty_params` (outbound internal message)                  | Required by TEP-66                             |
| Deploy single item, `op=1` (inbound internal message, owner-only)    | Optional                                       |
| Deploy batch of items, `op=2` (inbound internal message, owner-only) | Optional                                       |
| Change owner, `op=3` (inbound internal message, owner-only)          | Optional                                       |

Here, `op` means a 32‑bit operation code placed at the start of an internal message body. Contracts use it to dispatch messages to different handlers. The numeric values are local to this contract; they just need to be unique within its dispatcher. See the [Messages overview](/foundations/messages/overview) for more background.

### Storage

The storage scheme itself is not defined by these standards, so it can be arbitrary. The reference collection contract contains 5 fields: `owner_address`, `next_item_index`, `content`, `nft_item_code`, and `royalty_params`. Some of them are self‑explanatory, while `content` and `royalty_params` need additional explanation below.

Storage loading and saving is implemented with simple functions `load_data` and `save_data`:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
(slice, int, cell, cell, cell) load_data() inline {
  var ds = get_data().begin_parse();
  return
    (ds~load_msg_addr(), ;; owner_address
     ds~load_uint(64), ;; next_item_index
     ds~load_ref(), ;; content
     ds~load_ref(), ;; nft_item_code
     ds~load_ref()  ;; royalty_params
     );
}

() save_data(slice owner_address, int next_item_index, cell content, cell nft_item_code, cell royalty_params) impure inline {
  set_data(begin_cell()
    .store_slice(owner_address)
    .store_uint(next_item_index, 64)
    .store_ref(content)
    .store_ref(nft_item_code)
    .store_ref(royalty_params)
    .end_cell());
}
```

The `content` [cell](/foundations/serialization/cells) in this contract contains 2 cells in [references](/foundations/serialization/cells): `collection_content` containing metadata of the collection itself, and `common_content` containing the common prefix for individual items' metadata.

The `royalty_params` cell contains 3 values: `royalty_factor` which is a numerator, `royalty_base` which is a denominator, and `royalty_address` which is the address where royalties are sent.

### Child contracts

The collection and item contracts implement a classic [parent–child pattern](/contract-dev/contract-sharding), where the collection acts as the parent and items are children. Therefore, the collection implements functionality to deploy its children, and the contract keeps the `nft_item_code` field in storage.

The contract uses helper functions to compose [`StateInit`](/foundations/messages/deploy) — roughly, the package that holds a contract’s initial code and data used to derive its address and deploy it — calculate addresses, and send deploy messages for NFT items.

First, `calculate_nft_item_state_init` composes a `StateInit` cell of an NFT item with a given `item_index`. It composes a data cell following the expected storage schema of the item contract. Then the data and code cells are placed in a final `StateInit` cell following the format required by the TON blockchain. It uses the `store_dict` builder to encode optional references; it does not store dictionaries. This can be replaced with a more self‑explanatory [`store_maybe_ref`](/languages/func/stdlib#store-maybe-ref) builder which does the same thing. It stores a single bit `0` in a builder if the cell is null, otherwise bit `1` and a cell as a reference.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
cell calculate_nft_item_state_init(int item_index, cell nft_item_code) {
  cell data = begin_cell().store_uint(item_index, 64).store_slice(my_address()).end_cell();
  return begin_cell().store_uint(0, 2).store_dict(nft_item_code).store_dict(data).store_uint(0, 1).end_cell();
}
```

The `calculate_nft_item_address` function takes `state_init` composed by `calculate_nft_item_state_init`, along with `wc` (the [workchain](/foundations/glossary#workchain) for the resulting address). It calculates the address by hashing the `StateInit` and composing a [`MsgAddressInt`](/foundations/addresses/overview#internal-addresses) slice according to the format required by the TON blockchain.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
slice calculate_nft_item_address(int wc, cell state_init) {
  return begin_cell().store_uint(4, 3)
                     .store_int(wc, 8)
                     .store_uint(cell_hash(state_init), 256)
                     .end_cell()
                     .begin_parse();
}
```

`deploy_nft_item` builds the `StateInit`, derives the address, and sends the deploy message. It calls `calculate_nft_item_state_init` to compose the `StateInit`, then `calculate_nft_item_address` with `workchain()` to derive the contract's workchain (reference implementation uses `0` for [basechain](/foundations/addresses/overview#workchain-id)), and finally composes and sends the deploy message with the `StateInit` and content (using [`send_raw_message`](/languages/func/stdlib#send-raw-message)).

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() deploy_nft_item(int item_index, cell nft_item_code, int amount, cell nft_content) impure {
  cell state_init = calculate_nft_item_state_init(item_index, nft_item_code);
  slice nft_address = calculate_nft_item_address(workchain(), state_init);
  var msg = begin_cell()
            .store_uint(0x18, 6)
            .store_slice(nft_address)
            .store_coins(amount)
            .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
            .store_ref(state_init)
            .store_ref(nft_content);
  send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors
}
```

Note on `.store_uint(0x18, 6)` and `.store_uint(0x10, 6)`:

* These 6 leading bits encode the start of `CommonMsgInfo` for an internal message: `int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress …` (see [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout)). Concretely, the 6 bits are:

  * `tag` = `0` — selects `int_msg_info$0` (internal message info).
  * `ihr_disabled` — set to `1` to disable IHR (Instant Hypercube Routing). IHR is a legacy delivery mode and is not used in TON; contracts should always keep it disabled. See [Hypercube routing](/foundations/shards).
  * `bounce` — if `1`, the message will bounce back on errors during compute/action phases; if `0`, it will not bounce. We use `bounce=1` (0x18) for deploys to child items so the collection gets its funds back on failure, and `bounce=0` (0x10) for royalty replies to avoid bounces and ensure crediting wallets/uninitialized addresses. See [Bounce phase](/foundations/phases#bounce-phase).
  * `bounced` — set to `0` in outbound messages you build. This bit is meaningful only for inbound messages: when the blockchain generates a bounce, the inbound copy has `bounced=1`. Validators construct such bounce messages themselves; your outbound value does not make a message “bounced”. In normal sends, keep it `0`.
  * `src` — we serialize `addr_none` (the `00` tag). Validators replace it with the current contract address during the action phase, so you don’t need to spend bits storing the full source address. See [`send_raw_message`](/languages/func/stdlib#send-raw-message).

  Therefore:

  * `.store_uint(0x18, 6)` → `011000`: `ihr_disabled=1`, `bounce=1` (bounceable), `bounced=0`, `src=addr_none`. Used for deploy messages to child items.
  * `.store_uint(0x10, 6)` → `010000`: `ihr_disabled=1`, `bounce=0` (non‑bounceable), `bounced=0`, `src=addr_none`. Used for replies like `report_royalty_params` to avoid bounces.

  The destination comes next (`.store_slice(nft_address)`), followed by `value`, fees, timestamps, and optionally `StateInit` and body, as detailed below.

Note on `.store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)`:

* The right‑hand argument (bit width) is the total size of the remaining header fields we serialize in one shot, in this order (mapped to the TL‑B schema):
  * `1` — `ExtraCurrencyCollection` presence bit inside `value:CurrencyCollection` (see [`CurrencyCollection`](/foundations/whitepapers/tblkch#3-1-6-representing-collections-of-arbitrary-currencies) and the [credit phase](/foundations/whitepapers/tblkch#4-3-4-description-of-a-credit-phase)).
  * `4` — length prefix for `ihr_fee:Grams` (encoded as `VarUInteger 16`: 4‑bit length `len` then `len` bytes; writing `0000` here encodes zero). See [`LDGRAMS`](/tvm/instructions#fa00-ldgrams).
  * `4` — length prefix for `fwd_fee:Grams` (same `VarUInteger 16` rule; `0000` means zero). See [`LDGRAMS`](/tvm/instructions#fa00-ldgrams).
  * `64` — `created_lt:uint64` from `CommonMsgInfo` (we write `0`; validators rewrite the actual logical time). See [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout).
  * `32` — `created_at:uint32` from `CommonMsgInfo` (we write `0`; validators rewrite the block Unix time). See [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout).
  * `1` — `init:(Maybe (Either StateInit ^StateInit))` presence flag (0 = no `StateInit`, 1 = present). See [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout).
  * `1` — selector for the `Either` inside `init` (0 = inline `StateInit` follows, 1 = `^StateInit` by reference). In our case it is `1`, and `.store_ref(state_init)` follows. See [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout).
  * `1` — selector for `body:(Either X ^X)` (0 = inline body follows, 1 = body is provided by reference). In our case it is `1`, and `.store_ref(nft_content)` follows. See [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout).

* The left‑hand value `4 + 2 + 1` sets the three one‑bit selectors at the end of this header block to `1` (while earlier bits remain `0`). In order, they encode:
  * `init` is present (`Maybe …` = 1),
  * `init` is stored by reference (`Either StateInit ^StateInit` = 1),
  * body is stored by reference (`Either X ^X` = 1). See [message layout](/foundations/whitepapers/tblkch#3-1-7-message-layout).

This idiom is a terse way to zero‑initialize the residual CommonMsgInfo/Message fields and set only the required selector bits, instead of writing several consecutive `.store_uint(0, …)` calls followed by individual one‑bit writes.

These three functions are used for minting new items and returning item addresses in get‑methods.

### Internal messages

NFT standards only require one message to be processed in a specific way: `get_royalty_params`. All other logic for processing incoming messages can be arbitrary, but this reference contract shows a good example of how it can be organized for NFTs.

The `recv_internal` function starts with incoming message parsing boilerplate:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
        return ();
    }
    slice cs = in_msg_full.begin_parse();
    int flags = cs~load_uint(4); ;; the `int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool` part of CommonMsgInfo

    if (flags & 1) { ;; ignore all bounced messages by checking last bit of `flags` variable
        return ();
    }
    slice sender_address = cs~load_msg_addr(); ;; the `src:MsgAddressInt` part of CommonMsgInfo
    ;; ...
}
```

Then it parses `op` and `query_id` from incoming message body, and loads the storage data:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    int op = in_msg_body~load_uint(32);
    int query_id = in_msg_body~load_uint(64);

    var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_data();
    ;; ...
}
```

#### Request royalty parameters

The `get_royalty_params` message is the only inbound message handling required by TEP-66. On receipt, the contract sends a `report_royalty_params` message back to the sender with the `royalty_params` field from storage. The helper `send_royalty_params` is mostly boilerplate: it sends a [non‑bounceable](/foundations/addresses/formats) message (see [bounce behavior](/foundations/phases#bounce-phase)) with a body following the standard and carries the value of the incoming message.

Why non‑bounceable? The reply can go to any sender, including wallets or even uninitialized accounts. Making the response non‑bounceable ensures the value forwarded with `mode = 64` credits the requester even if it cannot execute code. It also avoids accidental bounces.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() send_royalty_params(slice to_address, int query_id, slice data) impure inline {
  var msg = begin_cell()
    .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress
    .store_slice(to_address)
    .store_coins(0)
    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    .store_uint(op::report_royalty_params(), 32) ;; opcode required by standard
    .store_uint(query_id, 64) ;; same query_id as request
    .store_slice(data);
  send_raw_message(msg.end_cell(), 64); ;; carry all the remaining value of the inbound message
}

() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    if (op == op::get_royalty_params()) {
        send_royalty_params(sender_address, query_id, royalty_params.begin_parse());
        return ();
    }
    ;; ...
}
```

The contract implements several owner-only features and verifies ownership first:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    throw_unless(401, equal_slices(sender_address, owner_address));
    ;; ...
}
```

Then there are 3 features: deploy single item (opcode `1`), deploy a batch of items (opcode `2`), and change owner (opcode `3`). The chosen opcodes are arbitrary.

#### Deploy single item

It parses the incoming message and expects it to contain 3 values: `item_index:uint64`, `amount:Coins`, and `content:^Cell` ([TL‑B](/languages/tl-b/overview) notation — the `^` means this field is stored in a separate referenced cell, i.e., the message carries a reference to a child [cell](/foundations/serialization/cells) that contains the content). It checks the item index to be less than or equal to the `next_item_index` from storage. Then it calls a helper function `deploy_nft_item`, which deploys the new NFT item. Lastly, if the item index equals `next_item_index`, it increments `next_item_index` to update the last item index.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    if (op == 1) { ;; deploy new nft
      int item_index = in_msg_body~load_uint(64);
      throw_unless(402, item_index <= next_item_index);
      var is_last = item_index == next_item_index;
      deploy_nft_item(item_index, nft_item_code, in_msg_body~load_coins(), in_msg_body~load_ref());
      if (is_last) {
        next_item_index += 1;
        save_data(owner_address, next_item_index, content, nft_item_code, royalty_params);
      }
      return ();
    }
    ;; ...
}
```

#### Batch deploy items

This feature is useful when you need to deploy many NFTs at once, for example when minting some initial items. It takes a single `deploy_list` dictionary from the message where keys are item indices and values are cells containing amounts of TON to attach to deployment, and content cells, similar to the values parsed by the singular deploy feature above. It traverses the dictionary in ascending order of indices and limits the batch to ≤249 items to stay comfortably under the [action list limit](/foundations/limits#message-and-transaction-limits) of 255 actions per transaction. This reference contract chooses a lower cap than the maximum as a safety margin.

The logic is straightforward. The loop uses [`~udict::delete_get_min(64)`](/languages/func/stdlib#dict-delete-get-min) to get and delete the item with the minimum unsigned 64‑bit key. It does that on each iteration and increments `counter` each iteration to avoid exceeding the action list limit. The deployment logic is the same as for the singular deployment feature above.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    if (op == 2) { ;; batch deploy of new nfts
      int counter = 0;
      cell deploy_list = in_msg_body~load_ref();
      do {
        var (item_index, item, f?) = deploy_list~udict::delete_get_min(64);
        if (f?) {
          counter += 1;
          if (counter >= 250) { ;; Limit due to action list size
            throw(399);
          }

          throw_unless(403 + counter, item_index <= next_item_index);
          deploy_nft_item(item_index, nft_item_code, item~load_coins(), item~load_ref());
          if (item_index == next_item_index) {
            next_item_index += 1;
          }
        }
      } until ( ~ f?);
      save_data(owner_address, next_item_index, content, nft_item_code, royalty_params);
      return ();
    }
    ;; ...
}
```

#### Change owner

The last incoming message handler is for changing ownership of the collection. It parses the `new_owner` address from the incoming message body and saves contract storage with it instead of the previous `owner`.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    if (op == 3) { ;; change owner
      slice new_owner = in_msg_body~load_msg_addr();
      save_data(new_owner, next_item_index, content, nft_item_code, royalty_params);
      return ();
    }
    ;; ...
}
```

Finally, if none of the conditions above are met, which means that the opcode provided in the message does not match any of the expected ones, it throws `0xffff`, which is a common exit code for unknown‑opcode errors:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    throw(0xffff);
}
```

### Get-methods

The collection contract implements only the get‑methods required by the standards: `get_collection_data`, `get_nft_address_by_index`, `royalty_params`, and `get_nft_content`.

#### Return collection data

The get‑method `get_collection_data` returns `next_item_index` (typically the number minted if minted sequentially), collection content, and an owner address. It loads the contract storage, loads the first reference from the `content` cell (which contains the collection content), and returns those values.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
(int, cell, slice) get_collection_data() method_id {
  var (owner_address, next_item_index, content, _, _) = load_data();
  slice cs = content.begin_parse();
  return (next_item_index, cs~load_ref(), owner_address);
}
```

#### Return item address by index

The get‑method `get_nft_address_by_index` takes an item index and returns the item's address. It uses helper functions `calculate_nft_item_state_init` and `calculate_nft_item_address` to calculate the address.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
slice get_nft_address_by_index(int index) method_id {
    var (_, _, _, nft_item_code, _) = load_data();
    cell state_init = calculate_nft_item_state_init(index, nft_item_code);
    return calculate_nft_item_address(workchain(), state_init);
}
```

#### Return royalty parameters

The get‑method `royalty_params` returns royalty parameters: `numerator`, `denominator`, and `destination`, as required by the standard. These values are stored in a `royalty` cell in data, which this method parses and returns.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
(int, int, slice) royalty_params() method_id {
     var (_, _, _, _, royalty) = load_data();
     slice rs = royalty.begin_parse();
     return (rs~load_uint(16), rs~load_uint(16), rs~load_msg_addr());
}
```

#### Compose full item content

The get‑method `get_nft_content` takes an item index and item content, and returns full content of the item. It composes and returns a cell following the off‑chain format for metadata from the TEP‑64 standard.

This reference implementation works with an off‑chain metadata format, joining a common prefix stored in the collection contract with individual item content from the item contract. The specific way of composing the full item's content is not forced by the standard, and can be implemented in any way.

Since item content in the off‑chain format is a URL pointing to a JSON with all the metadata fields, the common prefix is usually a URL to the endpoint for getting metadata for each item or a directory with all the JSON documents, for example `https://example-collection.com/items/`. Then each item can contain just the suffix of the full URL, such as `123.json`.

Composing a full URL means joining these two strings, which this method does. This is the most common approach, but it's possible to implement any custom logic for composing item content, like calculating the suffix right in the get‑method from the index and allowing items to not hold any additional content.

The common prefix for content is stored as a second reference in the `content` cell from the collection's contract storage. The resulting cell is then composed with an off‑chain tag `0x01`, followed by a common prefix joined with individual content in [snake format](/standard/tokens/metadata#snake-data-encoding).

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
cell get_nft_content(int index, cell individual_nft_content) method_id {
  var (_, _, content, _, _) = load_data();
  slice cs = content.begin_parse();
  cs~load_ref();
  slice common_content = cs~load_ref().begin_parse();
  return (begin_cell()
                      .store_uint(1, 8) ;; offchain tag
                      .store_slice(common_content)
                      .store_ref(individual_nft_content)
          .end_cell());
}
```

## Item

The full source code is in [nft-item.fc](https://github.com/ton-blockchain/token-contract/blob/1182ad99413242f09925d50e70ccb7e0e09f94d4/nft/nft-item.fc) file.

The TEP‑62 standard defines `transfer` and `get_static_data` messages that must be processed by the item contract, and a single `get_nft_data` get‑method that it must implement. In this contract, the reference implementation does not go beyond the requirements and only implements these three core features.

| Capability                                                    | Requirement                                    |
| ------------------------------------------------------------- | ---------------------------------------------- |
| `get_nft_data` (get-method)                                   | Required by TEP-62, data format follows TEP-64 |
| `transfer` (inbound internal message)                         | Required by TEP-62                             |
| `excesses`, `ownership_assigned` (outbound internal messages) | Required by TEP-62                             |
| `get_static_data`, `report_static_data` (internal messages)   | Required by TEP-62                             |

### Storage

Same as for the collection, the storage schema itself is not defined by the standard and it can be arbitrary. This contract stores only the essentials: the `index` of the item, `collection_address`, `owner_address`, and `content` with individual item content. An important consideration with this contract is that it has two states: initialized or not initialized. It is made this way to properly support deployments only by the collection contract. When the item is not initialized yet, it only has an index and collection address. Below are `load_data` and `store_data` for the item contract. Note that `load_data` also returns as the first value the boolean initialization flag, equal to `-1` if the item is initialized and `0` if not.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
(int, int, slice, slice, cell) load_data() {
    slice ds = get_data().begin_parse();
    var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr());
    if (ds.slice_bits() > 0) { ;; initialized
      return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref());
    } else { ;; not initialized
      return (0, index, collection_address, null(), null()); ;; nft not initialized yet
    }
}

() store_data(int index, slice collection_address, slice owner_address, cell content) impure {
    set_data(
        begin_cell()
            .store_uint(index, 64)
            .store_slice(collection_address)
            .store_slice(owner_address)
            .store_ref(content)
            .end_cell()
    );
}
```

### Internal messages

There is a boilerplate helper function for sending messages in this contract, for convenience. It composes and sends a message by taking high‑level arguments like `op` and `payload`:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
  var msg = begin_cell()
    .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress
    .store_slice(to_address)
    .store_coins(amount)
    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    .store_uint(op, 32)
    .store_uint(query_id, 64);

  if (~ builder_null?(payload)) {
    msg = msg.store_builder(payload);
  }

  send_raw_message(msg.end_cell(), send_mode);
}
```

The `recv_internal` function begins with boilerplate code for parsing flags, the sender address, and estimating the forward fee. The forward fee estimation assumes that all outbound messages will not be larger than the inbound message, and calculates the original forward fee of the inbound message as an upper bound. The `muldiv` operation multiplying the `fwd_fee` by 3 and dividing by 2 is an outdated way of doing that, as there exists a `get_original_fwd_fee` function which mostly does the same thing (see [TVM instructions → `GETORIGINALFWDFEE`](/tvm/instructions#f83a-getoriginalfwdfee)).

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
        return ();
    }

    slice cs = in_msg_full.begin_parse();
    int flags = cs~load_uint(4); ;; the `int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool` part of CommonMsgInfo

    if (flags & 1) { ;; ignore all bounced messages by checking last bit of `flags` variable
        return ();
    }
    slice sender_address = cs~load_msg_addr(); ;; the `src:MsgAddressInt` part of CommonMsgInfo

    cs~load_msg_addr(); ;; skip `dest:MsgAddressInt`
    cs~load_coins(); ;; skip `grams:Grams` from `value:CurrencyCollection`
    cs~skip_bits(1); ;; skip `other:ExtraCurrencyCollection` from `value:CurrencyCollection`
    cs~load_coins(); ;; skip ihr_fee:Grams
    int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; use the inbound message's fwd_fee to estimate an upper bound for outbound messages
    ;; ...
}
```

Load the data and handle the uninitialized state by checking that the sender is the collection contract. If so, parse the message body and save the new `content` and `owner_address`:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
    if (~ init?) {
      throw_unless(405, equal_slices(collection_address, sender_address));
      store_data(index, collection_address, in_msg_body~load_msg_addr(), in_msg_body~load_ref());
      return ();
    }
    ;; ...
}
```

Parse `op` and `query_id` from the incoming message body:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    int op = in_msg_body~load_uint(32);
    int query_id = in_msg_body~load_uint(64);
    ;; ...
}
```

And then there are two conditions for opcodes, processing either `transfer` or `get_static_data` messages required by the standard.

#### Transfer ownership

The `transfer` message's structure is defined by the standard. It must contain, apart from the opcode and `query_id`: `new_owner` (the destination address of the item transfer), `response_destination` (an address where the contract should send the remaining TON left after the transfer along with a confirmation message), `custom_payload` for extensions of the standard and additional custom logic, `forward_amount` (amount of TON to send to a new owner with the item transfer), and `forward_payload` (a cell for any arbitrary data to send to a new owner with the item transfer).

The `transfer` message handler checks whether the `sender_address` equals `owner_address`, then parses fields from the message. While parsing, it checks them for validity. The new owner's address must have a workchain equal to a constant specified in source code. This is a safety measure made to prevent troubles with differences in transaction fees between workchains.

Then it checks all the TON amounts to make sure it has enough balance to complete the transfer. After that it finally completes the transfer by updating the `owner_address` field in storage. It also sends from 0 to 2 messages, depending on whether a response is required and whether there is any `forward_amount`. The `custom_payload` is ignored in this implementation.

The response message has the `excesses` opcode and only contains the original `query_id`, with its only purpose being forwarding the remaining TON value of the transfer message to some address, usually the sender address.

The `ownership_assigned` message is sent only if `forward_amount` is greater than 0. It has the `ownership_assigned` opcode and includes `query_id`, previous `owner_address`, and the `forward_payload`, which at this point in code is the only thing left in `in_msg_body` from the original transfer message. This message is often used for processing item transfers in destination smart contracts, as well as attaching comments to transfers, and attaching any TON value by specifying `forward_amount`.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() transfer_ownership(int my_balance, int index, slice collection_address, slice owner_address, cell content, slice sender_address, int query_id, slice in_msg_body, int fwd_fees) impure inline {
    throw_unless(401, equal_slices(sender_address, owner_address));

    slice new_owner_address = in_msg_body~load_msg_addr();
    force_chain(new_owner_address);
    slice response_destination = in_msg_body~load_msg_addr();
    in_msg_body~load_int(1); ;; this nft don't use custom_payload
    int forward_amount = in_msg_body~load_coins();
    throw_unless(708, slice_bits(in_msg_body) >= 1);

    int rest_amount = my_balance - min_tons_for_storage();
    if (forward_amount) {
      rest_amount -= (forward_amount + fwd_fees);
    }
    int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
    if (need_response) {
      rest_amount -= fwd_fees;
    }

    throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response

    if (forward_amount) {
      send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, begin_cell().store_slice(owner_address).store_slice(in_msg_body), 1);  ;; paying fees, revert on errors
    }
    if (need_response) {
      force_chain(response_destination);
      send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors
    }

    store_data(index, collection_address, new_owner_address, content);
}

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    if (op == op::transfer()) {
      transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee);
      return ();
    }
    ;; ...
}
```

#### Report static data

On `get_static_data`, respond with `report_static_data` containing the item's `index` and `collection` address. This handler composes and sends a message with the index and collection address in the body.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    if (op == op::get_static_data()) {
      send_msg(sender_address, 0, op::report_static_data(), query_id, begin_cell().store_uint(index, 256).store_slice(collection_address), 64);  ;; carry all the remaining value of the inbound message
      return ();
    }
    ;; ...
}
```

Finally, if none of the conditions above are met, which means that the opcode provided in the message does not match any of the expected ones, it throws `0xffff`, which is a common exit code for unknown‑opcode errors:

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    ;; ...
    throw(0xffff);
}
```

### Get-methods

The standard only requires the `get_nft_data` get‑method.

#### Return item data

The get‑method `get_nft_data` returns the `init?` flag (whether the item is initialized), `index`, `collection_address`, `owner_address`, and `individual_content`. In this implementation, it returns all the values from `load_data` as is, including the logic for setting the `init?` flag.

Importantly, the `content` returned by this contract follows the format required by the collection contract, where it only includes the suffix of the URL pointing to an off‑chain JSON with metadata. But this is not forced by the standard, and the format can be arbitrary. The only requirements are that the collection's `get_nft_content` must return full content following TEP‑64, and that if the item has no collection it must return full content right away at this point, but the latter is out of scope here.

```func theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
(int, int, slice, slice, cell) get_nft_data() method_id {
  (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
  return (init?, index, collection_address, owner_address, content);
}
```
