Skip to main content
The NFT standard (TEP-62) describes the required messages and get-methods. The NFT royalty standard (TEP-66) describes a standard way to implement royalty information for NFT sales. The token data standard (TEP-64) describes available formats for metadata of tokens. A widely used reference implementation 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 implementation contains multiple smart contracts written in FunC 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 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.
CapabilityRequirement
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 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:
(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 in this contract contains 2 cells in references: 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, 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 — 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 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.
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 for the resulting address). It calculates the address by hashing the StateInit and composing a MsgAddressInt slice according to the format required by the TON blockchain.
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), and finally composes and sends the deploy message with the StateInit and content (using send_raw_message).
() 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). 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.
    • 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.
    • 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.
    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):
    • 1ExtraCurrencyCollection presence bit inside value:CurrencyCollection (see CurrencyCollection and the 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.
    • 4 — length prefix for fwd_fee:Grams (same VarUInteger 16 rule; 0000 means zero). See LDGRAMS.
    • 64created_lt:uint64 from CommonMsgInfo (we write 0; validators rewrite the actual logical time). See message layout.
    • 32created_at:uint32 from CommonMsgInfo (we write 0; validators rewrite the block Unix time). See message layout.
    • 1init:(Maybe (Either StateInit ^StateInit)) presence flag (0 = no StateInit, 1 = present). See 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.
    • 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.
  • 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.
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:
() 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:
() 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 message (see bounce behavior) 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.
() 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:
() 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 notation — the ^ means this field is stored in a separate referenced cell, i.e., the message carries a reference to a child cell 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.
() 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 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) 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.
() 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.
() 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:
() 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.
(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.
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.
(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.
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 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.
CapabilityRequirement
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.
(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:
() 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).
() 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:
() 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:
() 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.
() 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.
() 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:
() 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.
(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);
}