From b84a013e7a016cc5d4196a2bba2a5e34a473b469 Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:02:17 +0100
Subject: [PATCH 1/9] feat(appkit): how to work with nfts
---
docs.json | 3 +-
ecosystem/appkit/jettons.mdx | 15 +-
ecosystem/appkit/nfts.mdx | 482 +++++++++++++++++++++++++++++++
ecosystem/appkit/overview.mdx | 7 +
ecosystem/walletkit/web/nfts.mdx | 2 +-
5 files changed, 506 insertions(+), 3 deletions(-)
create mode 100644 ecosystem/appkit/nfts.mdx
diff --git a/docs.json b/docs.json
index 7bea52056..3aa91b2b1 100644
--- a/docs.json
+++ b/docs.json
@@ -141,7 +141,8 @@
"ecosystem/appkit/overview",
"ecosystem/appkit/init",
"ecosystem/appkit/toncoin",
- "ecosystem/appkit/jettons"
+ "ecosystem/appkit/jettons",
+ "ecosystem/appkit/nfts"
]
},
{
diff --git a/ecosystem/appkit/jettons.mdx b/ecosystem/appkit/jettons.mdx
index 654ca9ca1..bdb3dd63f 100644
--- a/ecosystem/appkit/jettons.mdx
+++ b/ecosystem/appkit/jettons.mdx
@@ -145,7 +145,7 @@ USDT has a decimal precision of 6, meaning that the fractional balance string `'
#### Single jetton
@@ -412,6 +412,8 @@ Modify the following example according to the application logic:
Mitigation: Verify the correct `decimals` value before calculating transfer amounts. For USDTs, the decimals value is 6.
+Jetton transfers require various Toncoin [transaction fees](/foundations/fees). Before making a transfer, make sure there is enough Toncoin in the balance to cover the fees.
+
Modify the following examples according to the application logic:
@@ -559,6 +561,17 @@ Modify the following examples according to the application logic:
```
+## Next steps
+
+
+
+
+
## See also
Jettons:
diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx
new file mode 100644
index 000000000..14f861ef5
--- /dev/null
+++ b/ecosystem/appkit/nfts.mdx
@@ -0,0 +1,482 @@
+---
+title: "How to work with NFTs using AppKit"
+sidebarTitle: "Work with NFTs"
+---
+
+import { Aside } from '/snippets/aside.jsx';
+
+
+
+[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/ecosystem/appkit/jettons), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token.
+
+
+
+
+
+## Ownership
+
+NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not.
+
+Similar to other asset queries, [discrete one-off checks](#on-demand-ownership-check) have limited value on their own and [continuous monitoring](#continuous-ownership-monitoring) should be used for UI display.
+### On-demand ownership check
+
+
+
+#### Single NFT
+
+Obtain the information of a specific NFT by its address and check the ownership:
+
+
+ ```tsx title="React" icon="react"
+ import {
+ useNft,
+ useSelectedWallet,
+ } from '@ton/appkit-react';
+
+ export const NftCard = ({ nftAddress }) => {
+ const [wallet, _] = useSelectedWallet();
+ const {
+ data: nft,
+ isLoading,
+ error,
+ } = useNft({
+ // NFT contract address
+ address: nftAddress ?? '',
+ });
+
+ if (isLoading) {
+ return
Loading...
;
+ }
+
+ if (error) {
+ return
Error: {error.message}
;
+ }
+
+ return (
+
+
NFT info
+
Name: {nft?.info?.name}
+
Collection: {nft?.collection?.name}
+
Owner address: {nft?.ownerAddress?.toString()}
+
Am I the owner: {nft?.ownerAddress === wallet?.getAddress() ? 'yes' : 'no'}
+ );
+ };
+ ```
+
+ ```ts title="TypeScript" icon="globe"
+ import {
+ type AppKit,
+ type NFT,
+ getJettonsByAddress,
+ getSelectedWallet,
+ // Helper function targeting the connected wallet
+ getNfts,
+ } from '@ton/appkit';
+
+ async function fetchNftsByAddress(
+ /** AppKit instance */
+ kit: AppKit,
+ ): Promise {
+ const selectedWallet = getSelectedWallet(kit);
+ const response = await getNftsByAddress(kit, {
+ address: selectedWallet?.getAddress() ?? '',
+ });
+
+ // Alternatively, query the connected wallet directly
+ // const response = await getNfts();
+
+ console.log('NFTs by address:', response.nfts.length);
+ return response.nfts;
+ }
+ ```
+
+
+### Continuous ownership monitoring
+
+Poll the NFT ownership at regular intervals to keep the displayed value up to date. Use an appropriate interval based on UX requirements — shorter intervals provide fresher data but increase API usage.
+
+Modify the following example according to the application logic:
+
+
+ ```tsx title="React" icon="react"
+ import {
+ useNfts,
+ } from '@ton/appkit-react';
+
+ export const NftsCard = () => {
+ const {
+ data: nfts,
+ isLoading,
+ error,
+ refetch,
+ } = useNfts({
+ // Only looks for up to 100 NFTs at a time
+ limit: 100,
+ });
+
+ if (isLoading) {
+ return
Loading...
;
+ }
+
+ if (error) {
+ return (
+
+
Error: {error.message}
+
+
+ );
+ }
+
+ return
NFTs by address: {nfts?.nfts.length}
;
+ };
+ ```
+
+ ```ts title="TypeScript" icon="globe" expandable
+ // Not runnable: implement the updateNftGallery()
+ import {
+ type AppKit,
+ type NFT,
+ getNfts,
+ } from '@ton/appkit';
+
+ /**
+ * Starts the monitoring of a given wallet's NFT ownership,
+ * calling `onNftsUpdate()` every `intervalMs` milliseconds
+ *
+ * @returns a function to stop monitoring
+ */
+ export function startNftOwnershipMonitoring(
+ kit: AppKit,
+ onNftsUpdate: (nfts: NFT[]) => void,
+ intervalMs: number = 10_000,
+ ): () => void {
+ let isRunning = true;
+
+ const poll = async () => {
+ while (isRunning) {
+ // Only looks for up to 100 NFTs.
+ // To get more, call the `getNfts()` function
+ // multiple times with increasing offsets
+ const { nfts } = await getNfts(kit, { limit: 100 });
+ onNftsUpdate(nfts);
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
+ }
+ };
+
+ // Start monitoring
+ poll();
+
+ // Return a cleanup function to stop monitoring
+ return () => {
+ isRunning = false;
+ };
+ }
+
+ // Usage
+ const stopMonitoring = startNftOwnershipMonitoring(
+ kit,
+ // The updateNftGallery() function is exemplary and should
+ // be replaced by an app function that refreshes the NFT gallery
+ // displayed in the interface
+ (balance) => updateNftGallery(balance),
+ );
+
+ // Stop monitoring once it is no longer needed
+ stopMonitoring();
+ ```
+
+
+## Transfers
+
+
+
+NFT transfers require various Toncoin [transaction fees](/foundations/fees). Before making a transfer, make sure there is enough Toncoin in the balance to cover the fees.
+
+Modify the following examples according to the application logic:
+
+
+ ```tsx title="React" icon="react"
+ import { useTransferNft } from '@ton/appkit-react';
+
+ export const SendNft = ({ recipientAddress, nftAddress }) => {
+ const { mutate: transfer, isPending, error, data } = useTransferNft();
+
+ const handleTransfer = () => {
+ transfer({
+ // New owner of the sent NFT.
+ // For example: 'UQ...'
+ recipientAddress,
+
+ // NFT contract address.
+ nftAddress,
+
+ // (optional) Additional Toncoin sent to recipient.
+ // An amount string in fractional units.
+ // For example, '0.1' or '1' Toncoin.
+ amount: '',
+
+ // (optional) Comment string. Defaults to none if not provided.
+ comment: 'Hello from AppKit!',
+ });
+ };
+
+ return (
+
+
+ {error &&
Error: {error.message}
}
+ {data && (
+
+
Transfer successful: {data.boc}
+
+ )}
+
+ );
+ };
+ ```
+
+ ```ts title="TypeScript" icon="globe"
+ import {
+ type AppKit,
+ type Base64String,
+ // Single-call transfer
+ transferNft,
+ // Two-step transfer: create a transaction object separately from sending it
+ createTransferNftTransaction,
+ sendTransaction,
+ } from '@ton/appkit';
+
+ async function sendNft(
+ /** Initialized AppKit instance */
+ kit: AppKit,
+ /** Recipient's TON wallet address as a string */
+ recipientAddress: string,
+ /** NFT contract address */
+ nftAddress: string,
+ /**
+ * Optional additional Toncoin sent to recipient.
+ * An amount string in fractional units.
+ * E.g., '0.1' or '1' Toncoin.
+ */
+ amount?: string,
+ /** Optional comment string */
+ comment?: string,
+ ) {
+ // Sign and send via TON Connect
+ const result = await transferNft(kit, {
+ recipientAddress,
+ nftAddress,
+ ...(amount && { amount }),
+ ...(comment && { comment }),
+ });
+ console.log('Transaction sent:', result.boc);
+
+ // Alternatively, build the transaction first with createTransferNftTransaction,
+ // then pass the resulting object to the sendTransaction function.
+ }
+ ```
+
+
+## `NFT` type
+
+NFT-related queries produce objects that conform to the following interface:
+
+```ts title="TypeScript"
+/**
+ * Non-fungible token (NFT) on the TON blockchain.
+ */
+export interface NFT {
+ /**
+ * Contract address of the NFT item
+ */
+ address: string;
+
+ /**
+ * Index of the item within its collection
+ */
+ index?: string;
+
+ /**
+ * Display information about the NFT (name, description, images, etc.)
+ */
+ info?: TokenInfo;
+
+ /**
+ * Custom attributes/traits of the NFT (e.g., rarity, properties)
+ */
+ attributes?: NFTAttribute[];
+
+ /**
+ * Information about the collection this item belongs to
+ */
+ collection?: NFTCollection;
+
+ /**
+ * Address of the auction contract, if the NFT is being auctioned
+ */
+ auctionContractAddress?: string;
+
+ /**
+ * Hash of the NFT smart contract code
+ */
+ codeHash?: string; // hexadecimal characters
+
+ /**
+ * Hash of the NFT's on-chain data
+ */
+ dataHash?: string; // hexadecimal characters
+
+ /**
+ * Whether the NFT contract has been initialized
+ */
+ isInited?: boolean;
+
+ /**
+ * Whether the NFT is soulbound (non-transferable)
+ */
+ isSoulbound?: boolean;
+
+ /**
+ * Whether the NFT is currently listed for sale
+ */
+ isOnSale?: boolean;
+
+ /**
+ * Current owner address of the NFT
+ */
+ ownerAddress?: string;
+
+ /**
+ * Real owner address when NFT is on sale (sale contract becomes temporary owner)
+ */
+ realOwnerAddress?: string;
+
+ /**
+ * Address of the sale contract, if the NFT is listed for sale
+ */
+ saleContractAddress?: string;
+
+ /**
+ * Off-chain metadata of the NFT (key-value pairs)
+ */
+ extra?: { [key: string]: unknown };
+}
+```
+
+## See also
+
+NFTs:
+
+- [NFT overview](/standard/tokens/nft/overview)
+- [NFT metadata](/standard/tokens/nft/metadata)
+
+General:
+
+- [Transaction fees](/foundations/fees)
+- [AppKit overview](/ecosystem/appkit/overview)
+- [TON Connect overview](/ecosystem/ton-connect)
diff --git a/ecosystem/appkit/overview.mdx b/ecosystem/appkit/overview.mdx
index 6ec799bc0..fe4ca7c99 100644
--- a/ecosystem/appkit/overview.mdx
+++ b/ecosystem/appkit/overview.mdx
@@ -50,6 +50,13 @@ TON Connect's **AppKit** is an open-source SDK that integrates web2 and web3 app
horizontal="true"
href="/ecosystem/appkit/jettons"
/>
+
+
## See also
diff --git a/ecosystem/walletkit/web/nfts.mdx b/ecosystem/walletkit/web/nfts.mdx
index ad1719392..0e754cbf8 100644
--- a/ecosystem/walletkit/web/nfts.mdx
+++ b/ecosystem/walletkit/web/nfts.mdx
@@ -9,7 +9,7 @@ import { Aside } from '/snippets/aside.jsx';
[Initialize the WalletKit](/ecosystem/walletkit/web/init), [set up at least one TON wallet](/ecosystem/walletkit/web/wallets), handle [connection requests](/ecosystem/walletkit/web/connections) and [transaction requests](/ecosystem/walletkit/web/events) before using examples on this page.
-[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike jettons, which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token.
+[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/ecosystem/walletkit/jettons), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token.
To work with NFTs, the wallet service needs to handle [NFT ownership queries](#ownership) and perform transfers initiated [from dApps](#transfers-from-dapps) and [from within the wallet service itself](#transfers-in-the-wallet-service).
From eb09a5c73d0444155ba8dbb732a88d86cf67bc4f Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:06:06 +0100
Subject: [PATCH 2/9] fmt
---
ecosystem/appkit/nfts.mdx | 1 +
1 file changed, 1 insertion(+)
diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx
index 14f861ef5..4b11b3718 100644
--- a/ecosystem/appkit/nfts.mdx
+++ b/ecosystem/appkit/nfts.mdx
@@ -29,6 +29,7 @@ import { Aside } from '/snippets/aside.jsx';
NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not.
Similar to other asset queries, [discrete one-off checks](#on-demand-ownership-check) have limited value on their own and [continuous monitoring](#continuous-ownership-monitoring) should be used for UI display.
+
### On-demand ownership check
-[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/ecosystem/appkit/jettons), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token.
+[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/standard/tokens/jettons/overview), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token.
-
-
## Ownership
NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not.
From 053d4cc4a0a6fc23adf400edecbf059a464d0573 Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Fri, 13 Mar 2026 18:06:10 +0100
Subject: [PATCH 7/9] correct code sample
---
ecosystem/appkit/nfts.mdx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx
index b9e0f6815..a63939fa0 100644
--- a/ecosystem/appkit/nfts.mdx
+++ b/ecosystem/appkit/nfts.mdx
@@ -250,8 +250,8 @@ Modify the following example according to the application logic:
// Only looks for up to 100 NFTs.
// To get more, call the `getNfts()` function
// multiple times with increasing offsets
- const { nfts } = await getNfts(kit, { limit: 100 });
- onNftsUpdate(nfts);
+ const nfts = await getNfts(kit, { limit: 100 });
+ onNftsUpdate(nfts?.nfts ?? []);
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
};
From 93cd315b30f79f3888f8aa4e808246104c7bb7d9 Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Fri, 13 Mar 2026 18:06:37 +0100
Subject: [PATCH 8/9] prefer `useAddress` to `useSelectedWallet`
---
ecosystem/appkit/nfts.mdx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx
index a63939fa0..8e648d0b0 100644
--- a/ecosystem/appkit/nfts.mdx
+++ b/ecosystem/appkit/nfts.mdx
@@ -42,11 +42,11 @@ Obtain the information of a specific NFT by its address and check the ownership:
```tsx title="React" icon="react"
import {
useNft,
- useSelectedWallet,
+ useAddress,
} from '@ton/appkit-react';
export const NftCard = ({ nftAddress }) => {
- const [wallet, _] = useSelectedWallet();
+ const address = useAddress();
const {
data: nft,
isLoading,
@@ -70,7 +70,7 @@ Obtain the information of a specific NFT by its address and check the ownership:
Name: {nft?.info?.name}
Collection: {nft?.collection?.name}
Owner address: {nft?.ownerAddress?.toString()}
-
Am I the owner: {nft?.ownerAddress === wallet?.getAddress() ? 'yes' : 'no'}
+
Am I the owner: {address && nft?.ownerAddress === address ? 'yes' : 'no'}
);
};
@@ -114,20 +114,20 @@ Retrieve every NFT held by the connected TON wallet or an arbitrary address:
```tsx title="React" icon="react"
import {
useNftsByAddress,
- useSelectedWallet,
+ useAddress,
// Helper function targeting the connected wallet
useNfts,
} from '@ton/appkit-react';
export const NftListByAddress = () => {
- const [wallet, _] = useSelectedWallet();
+ const address = useAddress();
const {
data: nfts,
isLoading,
error,
} = useNftsByAddress({
// TON wallet address of the NFT holder
- address: wallet?.getAddress() ?? '',
+ address: address ?? '',
});
// Alternatively, query the connected wallet directly
From ca6eaf9d79f1064335e9c839f88391f29994ce89 Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Fri, 13 Mar 2026 18:09:26 +0100
Subject: [PATCH 9/9] `useAddress` over `useSelectedWallet` for jettons page
too
---
ecosystem/appkit/jettons.mdx | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/ecosystem/appkit/jettons.mdx b/ecosystem/appkit/jettons.mdx
index bdb3dd63f..13440a8c1 100644
--- a/ecosystem/appkit/jettons.mdx
+++ b/ecosystem/appkit/jettons.mdx
@@ -79,18 +79,18 @@ Each jetton holder has a dedicated jetton wallet contract. To resolve its addres
```tsx title="React" icon="react" expandable
import {
useJettonWalletAddress,
- useSelectedWallet,
+ useAddress,
} from '@ton/appkit-react';
export const JettonWalletAddressCard = ({ jettonAddress }) => {
- const [wallet, _] = useSelectedWallet();
+ const ownerAddress = useAddress();
const {
data: walletAddress,
isLoading,
error,
} = useJettonWalletAddress({
// TON wallet address of the jetton holder
- ownerAddress: wallet?.getAddress() ?? '',
+ ownerAddress: ownerAddress ?? '',
// Jetton master (minter) contract address
jettonAddress,
@@ -156,18 +156,18 @@ Check the balance of a specific jetton for the connected TON wallet or an arbitr
```tsx title="React" icon="react"
import {
useJettonBalanceByAddress,
- useSelectedWallet,
+ useAddress,
} from '@ton/appkit-react';
export const JettonBalanceCard = ({ jettonAddress }) => {
- const [wallet, _] = useSelectedWallet();
+ const ownerAddress = useAddress();
const {
data: balance,
isLoading,
error,
} = useJettonBalanceByAddress({
// TON wallet address of the jetton holder
- ownerAddress: wallet?.getAddress() ?? '',
+ ownerAddress: ownerAddress ?? '',
// Jetton master (minter) contract address
jettonAddress,
@@ -222,20 +222,20 @@ Retrieve every jetton held by the connected TON wallet or an arbitrary address:
```tsx title="React" icon="react"
import {
useJettonsByAddress,
- useSelectedWallet,
+ useAddress,
// Helper function targeting the connected wallet
useJettons,
} from '@ton/appkit-react';
export const JettonListByAddress = () => {
- const [wallet, _] = useSelectedWallet();
+ const address = useAddress();
const {
data: jettons,
isLoading,
error,
} = useJettonsByAddress({
// TON wallet address of the jetton holder
- address: wallet?.getAddress() ?? '',
+ address: address ?? '',
});
// Alternatively, query the connected wallet directly
@@ -303,11 +303,11 @@ Modify the following example according to the application logic:
```tsx title="React" icon="react"
import {
useJettonBalanceByAddress,
- useSelectedWallet,
+ useAddress,
} from '@ton/appkit-react';
export const JettonBalanceCard = ({ jettonAddress }) => {
- const [wallet, _] = useSelectedWallet();
+ const ownerAddress = useAddress();
const {
data: balance,
isLoading,
@@ -315,7 +315,7 @@ Modify the following example according to the application logic:
refetch,
} = useJettonBalanceByAddress({
// TON wallet address of the jetton holder
- ownerAddress: wallet?.getAddress() ?? '',
+ ownerAddress: ownerAddress ?? '',
// Jetton master (minter) contract address
jettonAddress,