> ## 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": "/ecosystem/walletkit/web/toncoin",
  "feedback": "Description of the issue"
}
```

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

</AgentInstructions>

# How to work with Toncoin using WalletKit on the Web platform

export const Aside = ({type = "note", title = "", icon = "", iconType = "regular", children}) => {
  const asideVariants = ["note", "tip", "caution", "danger"];
  const asideComponents = {
    note: {
      outerStyle: "border-sky-500/20 bg-sky-50/50 dark:border-sky-500/30 dark:bg-sky-500/10",
      innerStyle: "text-sky-900 dark:text-sky-200",
      calloutType: "note",
      icon: <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-sky-500" aria-label="Note">
          <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.3C10.14 1.3 12.7 3.86 12.7 7C12.7 10.14 10.14 12.7 7 12.7C5.48908 12.6974 4.0408 12.096 2.97241 11.0276C1.90403 9.9592 1.30264 8.51092 1.3 7C1.3 3.86 3.86 1.3 7 1.3ZM7 0C3.14 0 0 3.14 0 7C0 10.86 3.14 14 7 14C10.86 14 14 10.86 14 7C14 3.14 10.86 0 7 0ZM8 3H6V8H8V3ZM8 9H6V11H8V9Z"></path>
        </svg>
    },
    tip: {
      outerStyle: "border-emerald-500/20 bg-emerald-50/50 dark:border-emerald-500/30 dark:bg-emerald-500/10",
      innerStyle: "text-emerald-900 dark:text-emerald-200",
      calloutType: "tip",
      icon: <svg width="11" height="14" viewBox="0 0 11 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg" className="text-emerald-600 dark:text-emerald-400/80 w-3.5 h-auto" aria-label="Tip">
          <path d="M3.12794 12.4232C3.12794 12.5954 3.1776 12.7634 3.27244 12.907L3.74114 13.6095C3.88471 13.8248 4.21067 14 4.46964 14H6.15606C6.41415 14 6.74017 13.825 6.88373 13.6095L7.3508 12.9073C7.43114 12.7859 7.49705 12.569 7.49705 12.4232L7.50055 11.3513H3.12521L3.12794 12.4232ZM5.31288 0C2.52414 0.00875889 0.5 2.26889 0.5 4.78826C0.5 6.00188 0.949566 7.10829 1.69119 7.95492C2.14321 8.47011 2.84901 9.54727 3.11919 10.4557C3.12005 10.4625 3.12175 10.4698 3.12261 10.4771H7.50342C7.50427 10.4698 7.50598 10.463 7.50684 10.4557C7.77688 9.54727 8.48281 8.47011 8.93484 7.95492C9.67728 7.13181 10.1258 6.02703 10.1258 4.78826C10.1258 2.15486 7.9709 0.000106649 5.31288 0ZM7.94902 7.11267C7.52078 7.60079 6.99082 8.37878 6.6077 9.18794H4.02051C3.63739 8.37878 3.10743 7.60079 2.67947 7.11294C2.11997 6.47551 1.8126 5.63599 1.8126 4.78826C1.8126 3.09829 3.12794 1.31944 5.28827 1.3126C7.2435 1.3126 8.81315 2.88226 8.81315 4.78826C8.81315 5.63599 8.50688 6.47551 7.94902 7.11267ZM4.87534 2.18767C3.66939 2.18767 2.68767 3.16939 2.68767 4.37534C2.68767 4.61719 2.88336 4.81288 3.12521 4.81288C3.36705 4.81288 3.56274 4.61599 3.56274 4.37534C3.56274 3.6515 4.1515 3.06274 4.87534 3.06274C5.11719 3.06274 5.31288 2.86727 5.31288 2.62548C5.31288 2.38369 5.11599 2.18767 4.87534 2.18767Z"></path>
        </svg>
    },
    caution: {
      outerStyle: "border-amber-500/20 bg-amber-50/50 dark:border-amber-500/30 dark:bg-amber-500/10",
      innerStyle: "text-amber-900 dark:text-amber-200",
      calloutType: "warning",
      icon: <svg className="flex-none w-5 h-5 text-amber-400 dark:text-amber-300/80" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-label="Warning">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
        </svg>
    },
    danger: {
      outerStyle: "border-red-500/20 bg-red-50/50 dark:border-red-500/30 dark:bg-red-500/10",
      innerStyle: "text-red-900 dark:text-red-200",
      calloutType: "danger",
      icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" className="text-red-600 dark:text-red-400/80 w-4 h-4" aria-label="Danger">
          <path d="M17.1 292c-12.9-22.3-12.9-49.7 0-72L105.4 67.1c12.9-22.3 36.6-36 62.4-36l176.6 0c25.7 0 49.5 13.7 62.4 36L494.9 220c12.9 22.3 12.9 49.7 0 72L406.6 444.9c-12.9 22.3-36.6 36-62.4 36l-176.6 0c-25.7 0-49.5-13.7-62.4-36L17.1 292zm41.6-48c-4.3 7.4-4.3 16.6 0 24l88.3 152.9c4.3 7.4 12.2 12 20.8 12l176.6 0c8.6 0 16.5-4.6 20.8-12L453.4 268c4.3-7.4 4.3-16.6 0-24L365.1 91.1c-4.3-7.4-12.2-12-20.8-12l-176.6 0c-8.6 0-16.5 4.6-20.8 12L58.6 244zM256 128c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"></path>
        </svg>
    }
  };
  let variant = type;
  let gotInvalidVariant = false;
  if (!asideVariants.includes(type)) {
    gotInvalidVariant = true;
    variant = "danger";
  }
  const iconVariants = ["regular", "solid", "light", "thin", "sharp-solid", "duotone", "brands"];
  if (!iconVariants.includes(iconType)) {
    iconType = "regular";
  }
  return <>
      <div className={`callout my-4 px-5 py-4 overflow-hidden rounded-2xl flex gap-3 border ${asideComponents[variant].outerStyle}`} data-callout-type={asideComponents[variant].calloutType}>
        <div className="mt-0.5 w-4" data-component-part="callout-icon">
          {}
          {icon === "" ? asideComponents[variant].icon : <Icon icon={icon} iconType={iconType} size={14} />}
        </div>
        <div className={`text-sm prose min-w-0 w-full ${asideComponents[variant].innerStyle}`} data-component-part="callout-content">
          {gotInvalidVariant ? <p>
              <span className="font-bold">
                Invalid <code>type</code> passed!
              </span>
              <br />
              <span className="font-bold">Received: </span>
              {type}
              <br />
              <span className="font-bold">Expected one of: </span>
              {asideVariants.join(", ")}
            </p> : <>
              {title && <p className="font-bold">{title}</p>}
              {children}
            </>}
        </div>
      </div>
    </>;
};

<Aside>
  [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.
</Aside>

To work with Toncoin, the wallet service needs to handle [contract balances](#balances) and perform transfers initiated [from dApps](#transfers-from-dapps) and [from within the wallet service itself](#transfers-from-the-wallet-service).

## Balances

Blockchain state changes constantly as new blocks are produced. This has implications for when and how to check TON wallet contract balances:

* [Discrete one-off checks](#on-demand-balance-check) have almost no value on their own — the state might change immediately after the query completes, invalidating its results. Thus, such checks are only practical when handling `transaction` requests.
* [Continuous monitoring](#continuous-balance-monitoring) is useful for UI display, showing the most recent balance to users, but should not be used for transaction confirmations.

Notice that both cases require querying the blockchain data via the API client set during the [WalletKit initialization](/ecosystem/walletkit/web/init#param-networks). Obtain and provide the key from the selected client to access higher requests-per-second limits.

### On-demand balance check

Use the `getBalance()` method to check the wallet contract balance in TON wallets managed by WalletKit. The balance is returned in nanoToncoin, with 1 Toncoin equal to 10<sup>9</sup> nanoToncoin.

<Aside type="caution">
  Do not store the balance check results anywhere in the wallet service's state, as they become outdated very quickly. For UI purposes, do [continuous balance monitoring](#continuous-balance-monitoring).
</Aside>

```ts title="TypeScript" 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"]}}
async function getBalance(walletId: string): Promise<string | undefined> {
  // Get TON wallet instance
  const wallet = kit.getWallet(walletId);
  if (!wallet) return;

  // Query its balance in nanoToncoin
  return await wallet.getBalance();
}
```

The most practical use of one-off balance checks is right before approving a transaction request. At this point, the actual wallet contract balance usually is not less than the checked amount, though it might be higher if new funds arrived right after the check.

<Aside>
  Despite this check, the transaction may still fail due to insufficient balance at the time of transfer.
</Aside>

```ts title="TypeScript" 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"]}}
// An enumeration of various common error codes
import { SEND_TRANSACTION_ERROR_CODES } from '@ton/walletkit';

kit.onTransactionRequest(async (event) => {
  const wallet = kit.getWallet(event.walletId ?? '');
  if (!wallet) {
    console.error('Wallet not found for a transaction request', event);
    await kit.rejectTransactionRequest(event, {
      code: SEND_TRANSACTION_ERROR_CODES.UNKNOWN_ERROR,
      message: 'Wallet not found',
    });
    return;
  }

  // Calculate the minimum balance needed for this transaction
  const balance = BigInt(await wallet.getBalance());
  const minNeededBalance = event.request.messages.reduce(
    (acc, message) => acc + BigInt(message.amount),
    0n,
  );

  // Reject early if balance is clearly insufficient
  if (balance < minNeededBalance) {
    await kit.rejectTransactionRequest(event, {
      code: SEND_TRANSACTION_ERROR_CODES.BAD_REQUEST_ERROR,
      message: 'Insufficient balance',
    });
    return;
  }

  // Proceed with the regular transaction flow
  // ...
});
```

### Continuous balance monitoring

Poll the balance 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.

This example should be modified according to the wallet service's logic:

```ts title="TypeScript" expandable 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"]}}
// Not runnable: implement the updateUI()!

/**
 * Starts the monitoring of a given `walletId`,
 * calling `onBalanceUpdate()` every `intervalMs` milliseconds
 *
 * @returns a function to stop monitoring
 */
export function startBalanceMonitoring(
  walletId: string,
  onBalanceUpdate: (balance: string) => void,
  intervalMs: number = 10_000,
): () => void {
  let isRunning = true;

  const poll = async () => {
    while (isRunning) {
      const wallet = kit.getWallet(walletId);
      if (wallet) {
        const balance = await wallet.getBalance();
        onBalanceUpdate(balance);
      }
      await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }
  };

  // Start monitoring
  poll();

  // Return a cleanup function to stop monitoring
  return () => {
    isRunning = false;
  };
}

// Usage
const stopMonitoring = startBalanceMonitoring(
  walletId,
  // The updateUI() function is exemplary and should be replaced by
  // a wallet service function that refreshes the
  // state of the balance displayed in the interface
  (balance) => updateUI(balance),
);

// Stop monitoring once it is no longer needed
stopMonitoring();
```

## Transfers from dApps

When a connected dApp requests a Toncoin transfer, the wallet service follows this flow:

```mermaid 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"]}}
sequenceDiagram
    participant dApp
    participant Bridge
    participant WalletKit
    participant User
    participant Blockchain

    dApp->>Bridge: Send transaction request
    Bridge->>WalletKit: Forward request
    WalletKit->>WalletKit: Emulate transaction
    WalletKit->>User: Show preview (money flow)
    User-->>WalletKit: Approve / Decline
    alt Approved
        WalletKit->>Blockchain: Send transaction
        WalletKit->>Bridge: Return success + BoC
        Bridge->>dApp: Transaction sent
    else Declined
        WalletKit->>Bridge: Return rejection
        Bridge->>dApp: User rejected
    end
```

### Emulation and preview

WalletKit tries to automatically emulate every incoming transaction request before presenting it to the user. The emulation result is available in the `event.preview.data` object, which can be `undefined` if emulation was skipped:

```ts title="TypeScript" 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"]}}
kit.onTransactionRequest(async (event) => {
  if (!event.preview.data) {
    console.log('Transaction emulation skipped');
  } else if (event.preview.data.result === 'success') {
    // Emulation succeeded — show the predicted money flow
    const { ourTransfers } = event.preview.data.moneyFlow;

    // This is an array of values,
    // where positive amounts mean incoming funds
    // and negative amounts — outgoing funds
    console.log('Predicted transfers:', ourTransfers);
  } else {
    // Emulation failed — warn the user but allow proceeding
    console.warn('Transaction emulation failed:', event.preview);
  }

  // Present the preview to the user and await their decision
  // ...
});
```

<Aside>
  Emulation uses the API client configured during [WalletKit initialization](/ecosystem/walletkit/web/init#param-networks).
</Aside>

### Approve or reject

After showing the preview, handle the user's decision:

```ts title="TypeScript" expandable 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"]}}
import {
  // An enumeration of various common error codes
  SEND_TRANSACTION_ERROR_CODES,
  // The transfer type
  type TransactionTraceMoneyFlowItem,
} from '@ton/walletkit';

kit.onTransactionRequest(async (event) => {
  try {
    // Show the emulation preview to the wallet service user
    const preview = event.preview;
    const isEmulationSuccessful = preview.data?.result === 'success';

    // Build a confirmation message
    let confirmMessage = 'Confirm this transaction?';
    if (isEmulationSuccessful) {
      const transfers = preview.data.moneyFlow?.ourTransfers || [];
      confirmMessage = `Send ${formatTransfers(transfers)}?`;
    } else {
      confirmMessage = 'Emulation failed or skipped. Proceed anyway?';
    }

    // Handle user's decision
    if (confirm(confirmMessage)) {
      // Approve — this sends the transaction to the blockchain
      // and returns the signed message BoC to the dApp
      await kit.approveTransactionRequest(event);
      console.log('Transaction approved and sent');
    } else {
      // Reject — notify the dApp that the user declined
      await kit.rejectTransactionRequest(event, {
        code: SEND_TRANSACTION_ERROR_CODES.USER_REJECTS_ERROR,
        message: 'User rejected the transaction',
      });
    }
  } catch (error) {
    console.error('Transaction handler error:', error);
    await kit.rejectTransactionRequest(event, {
      code: SEND_TRANSACTION_ERROR_CODES.UNKNOWN_ERROR,
      message: 'Transaction processing failed',
    });
  }
});

function formatTransfers(transfers: Array<TransactionTraceMoneyFlowItem>): string {
  const formatNano = (value: string, decimals: number = 9): string => {
    const bigintValue = BigInt(value);
    const isNegative = bigintValue < 0n;
    const absoluteValue = bigintValue < 0n ? -bigintValue : bigintValue;
    const str = absoluteValue.toString().padStart(decimals + 1, '0');

    const intPart = str.slice(0, -decimals) || '0';
    const fracPart = str.slice(-decimals).replace(/0+$/, '');

    const formatted = fracPart ? `${intPart}.${fracPart}` : intPart;
    return `${isNegative ? '-' : ''}${formatted}`;
  };

  return transfers.map((t) => `${formatNano(t.amount)} ${t.assetType}`).join(', ');
}
```

### Confirm transaction delivery

TON achieves transaction [finality](https://en.wikipedia.org/wiki/Blockchain#Finality) after a single masterchain block confirmation, where new blocks are produced approximately every 3 seconds. Once a transaction appears in a masterchain block, it becomes irreversible.

Therefore, to reliably confirm the transaction delivery and status, one needs to check whether a transaction has achieved masterchain finality using the selected API client.

That said, the wallet service should not block the UI while waiting for such confirmation. After all, with [continuous wallet balance monitoring](#continuous-balance-monitoring) and subsequent transaction requests, users will receive the latest information either way. Confirmations are only needed to reliably display a list of past transactions, including the most recent ones.

For detailed transaction tracking and message lookups, the [message lookup guide](/ecosystem/ton-connect/message-lookup) covers finding transactions by external message hash, waiting for confirmations, and applying message normalization.

## Transfers in the wallet service

Transactions can be created directly from the wallet service (not from dApps) and fed into the regular approval flow via `handleNewTransaction()` method of the WalletKit. It creates a new [transaction request event](/ecosystem/walletkit/web/events#handle-ontransactionrequest), enabling the same UI confirmation-to-transaction flow for both dApp-initiated and wallet-initiated transactions.

This example should be modified according to the wallet service's logic:

```ts title="TypeScript" 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"]}}
import type { TONTransferRequest, Base64String } from '@ton/walletkit';

async function sendToncoin(
  // Sender's TON `walletId` as a string
  walletId: string,
  // Recipient's TON wallet address as a string
  recipientAddress: string,
  // Amount in nanoToncoins
  nanoAmount: BigInt,
  // Optional comment string
  comment?: string,
  // Optional payload body as a BoC in Base64-encoded string
  payload?: Base64String,
) {
  if (comment && payload) {
    console.error('Cannot attach both a comment or a payload body');
    return;
  }

  const fromWallet = kit.getWallet(walletId);
  if (!fromWallet) {
    console.error('No wallet contract found');
    return;
  }

  const transferParams: TONTransferRequest = {
    recipientAddress: recipientAddress,
    transferAmount: nanoAmount.toString(),
    // Optional comment OR payload, not both
    ...(comment && { comment }),
    ...(payload && { payload }),
  }

  // Build transaction content
  const tx = await fromWallet.createTransferTonTransaction(transferParams);

  // Route into the normal flow,
  // triggering the onTransactionRequest() handler
  await kit.handleNewTransaction(fromWallet, tx);
}
```

<Aside type="caution">
  To avoid triggering the `onTransactionRequest()` handler and send the transaction directly, use the `sendTransaction()` method of the wallet instead of the `handleNewTransaction()` method of the WalletKit, modifying the last part of the previous code snippet:

  ```ts title="TypeScript" 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"]}}
  // Instead of calling kit.handleNewTransaction(fromWallet, tx)
  // one can avoid routing into the normal flow,
  // skip the transaction requests handler,
  // and make the transaction directly.
  await fromWallet.sendTransaction(tx);
  ```

  Do not use this approach unless it is imperative to complete a transaction without the user's direct consent. Funds at risk: test this approach using testnet and proceed with utmost caution.
</Aside>

## See also

* [Handle transaction requests](/ecosystem/walletkit/web/events#handle-ontransactionrequest)
* [Transaction fees](/foundations/fees)
* [WalletKit overview](/ecosystem/walletkit/overview)
* [TON Connect overview](/ecosystem/ton-connect/overview)
