# How to authenticate users with `ton_proof` (https://docs.ton.org/llms/applications/ton-connect/how-to/ton-proof/content.md)



`ton_proof` lets a backend verify that a user controls the wallet account they connected. The wallet signs an application-provided payload with the account's private key; the backend checks the signature and issues a session token.

## Verification flow [#verification-flow]

<Image src="/resources/images/ton-connect/ton-proof-scheme.svg" darkSrc="/resources/images/ton-connect/ton-proof-scheme-dark.svg" alt="TON Proof verification flow: client requests payload, signs with wallet, and sends proof to backend" />

During wallet connection, the client receives a `TonProofItemReplySuccess` object and sends it to the backend:

```typescript
type TonProofItemReplySuccess = {
  name: 'ton_proof';
  proof: {
    timestamp: number;     // Unix epoch time of the signing operation in seconds
    domain: {
      lengthBytes: number; // AppDomain length
      value: string;       // App domain name (URL part, without encoding)
    };
    signature: string;     // base64-encoded signature
    payload: string;       // payload from the request
  };
};
```

The SDK exposes `timestamp` as a number, while the TON Connect specification defines its wire value as a decimal string. Backend validation should accept both representations and normalize the value before verification.

## Request a proof [#request-a-proof]

Generate an unpredictable, single-use payload on the backend. Associate it with the login attempt and give it a short expiration time. Then pass the payload to `setConnectRequestParameters` before opening the wallet modal:

```ts
// Not runnable: initialize tonConnectUI and provide the API endpoint.
const payload = await fetch('/api/ton-proof/payload', {
  method: 'POST',
}).then((response) => response.text());

tonConnectUI.setConnectRequestParameters({
  state: 'ready',
  value: { tonProof: payload },
});
```

Keep the payload short. The SDK allows at most 128 UTF-8 bytes for the domain, 128 UTF-8 bytes for the payload, and 222 bytes for both combined.

Read the proof from the wallet status callback and send it to the backend together with `wallet.account`. A wallet that does not support `ton_proof` returns a connect-item error instead of a `proof` field, so handle both variants:

```ts
// Not runnable: initialize tonConnectUI and provide the API endpoint.
tonConnectUI.onStatusChange(async (wallet) => {
  const tonProof = wallet?.connectItems?.tonProof;
  if (!wallet || !tonProof || !('proof' in tonProof)) {
    return;
  }

  await fetch('/api/ton-proof/verify', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      account: wallet.account,
      proof: tonProof.proof,
    }),
  });
});
```

## Verify the signature [#verify-the-signature]

To verify, reconstruct the same message the wallet signed and check the signature against it.

### Message assembly [#message-assembly]

Assemble the message exactly as the wallet did:

```text
message = utf8_encode("ton-proof-item-v2/") ++
          Address ++
          AppDomain ++
          Timestamp ++
          Payload
```

* **Address** — the user's wallet address: `workchain` (32-bit signed, big-endian) + `hash` (256-bit unsigned, big-endian).
* **AppDomain** — `lengthBytes` (32-bit unsigned, little-endian) + the UTF-8-encoded app domain. Verify that `lengthBytes` equals the encoded domain's byte length.
* **Timestamp** — 64-bit little-endian Unix epoch time in seconds.
* **Payload** — the exact UTF-8-encoded payload from the request. Use a backend-issued, single-use value to prevent replay attacks.

### Signature verification [#signature-verification]

The signing digest is `sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message))`:

```text
signature = Ed25519Sign(
  privkey,
  sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message))
)
```

The backend hashes the message, prepends `0xffff ++ "ton-connect"`, hashes again, and verifies with the user's public key. Mainnet and testnet use the empty signature domain. Other global IDs use the corresponding L2 signature domain.

## Obtain the public key [#obtain-the-public-key]

### From walletStateInit (preferred) [#from-walletstateinit-preferred]

Extract the public key from the `walletStateInit` in `TonAddressItemReply` — no network call needed.

1. Parse `walletStateInit` — decode the `walletStateInit` cell.
2. Identify the wallet version — compare the `code` hash of the `walletStateInit` with known standard wallet contract codes.
3. Extract the public key — parse the `data` section according to the identified wallet version.

### From on-chain get\_public\_key (fallback) [#from-on-chain-get_public_key-fallback]

For non-standard wallets where `walletStateInit` parsing fails, query the blockchain:

1. Invoke the `get_public_key` get method on the smart contract at the user's address.
2. Extract the public key from the result.

### Final checks [#final-checks]

After extracting the public key, verify:

1. It matches the `publicKey` field in `TonAddressItemReply`.
2. `contractAddress(workchain, walletStateInit)` equals the user's address.
3. The domain equals the expected dApp domain and `lengthBytes` equals its UTF-8 byte length.
4. The timestamp is within a short acceptance window, including a bound on future clock skew.
5. The payload belongs to this login attempt, has not expired, and is consumed atomically so it cannot be reused.
6. The connected network matches a network accepted by the application. The `ton_proof` signature does not bind the network.

<Callout>
  Public-key extraction is wallet-version-specific. Identify the wallet by its code hash and parse the matching data layout; see [`tryExtractPublicKey`](https://docs.ton.org/llms/applications/ton-connect/how-to/sign-data/content.md) for an implementation covering standard wallet versions.
</Callout>

## React example [#react-example]

Adapted from the [SDK demo frontend](https://github.com/ton-connect/sdk/tree/a07a165977eae17578b2414797f74d4bf54962eb/apps/demo-dapp-with-react-ui/src):

```tsx expandable
// Not runnable: copy the linked app and install its dependencies.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactJson from 'react-json-view';
import { TonProofDemoApi } from '../../TonProofDemoApi';
import { useTonConnectUI, useTonWallet, CHAIN } from '@tonconnect/ui-react';
import useInterval from '../../hooks/useInterval';

export const TonProofDemo = () => {
  const firstProofLoading = useRef<boolean>(true);

  const [data, setData] = useState({});
  const wallet = useTonWallet();
  const [authorized, setAuthorized] = useState(false);
  const [tonConnectUI] = useTonConnectUI();

  const recreateProofPayload = useCallback(async () => {
    if (firstProofLoading.current) {
      tonConnectUI.setConnectRequestParameters({ state: 'loading' });
      firstProofLoading.current = false;
    }

    const payload = await TonProofDemoApi.generatePayload();

    if (payload) {
      tonConnectUI.setConnectRequestParameters({ state: 'ready', value: payload });
    } else {
      tonConnectUI.setConnectRequestParameters(null);
    }
  }, [tonConnectUI, firstProofLoading]);

  if (firstProofLoading.current) {
    recreateProofPayload();
  }

  useInterval(recreateProofPayload, TonProofDemoApi.refreshIntervalMs);

  useEffect(
    () =>
      tonConnectUI.onStatusChange(async (w) => {
        if (!w || w.account.chain === CHAIN.TESTNET) {
          TonProofDemoApi.reset();
          setAuthorized(false);
          return;
        }

        if (w.connectItems?.tonProof && 'proof' in w.connectItems.tonProof) {
          await TonProofDemoApi.checkProof(w.connectItems.tonProof.proof, w.account);
        }

        if (!TonProofDemoApi.accessToken) {
          tonConnectUI.disconnect();
          setAuthorized(false);
          return;
        }

        setAuthorized(true);
      }),
    [tonConnectUI],
  );

  const handleClick = useCallback(async () => {
    if (!wallet) {
      return;
    }
    const response = await TonProofDemoApi.getAccountInfo(wallet.account);
    setData(response);
  }, [wallet]);

  if (!authorized) {
    return null;
  }

  return (
    <div className="ton-proof-demo">
      <h3>Demo backend API with ton_proof verification</h3>
      {authorized ? (
        <button onClick={handleClick}>Call backend getAccountInfo()</button>
      ) : (
        <div className="ton-proof-demo__error">Connect wallet to call API</div>
      )}
      <ReactJson src={data} name="response" theme="ocean" />
    </div>
  );
};
```

## Backend example [#backend-example]

Adapted from the [SDK demo backend](https://github.com/ton-connect/sdk/tree/a07a165977eae17578b2414797f74d4bf54962eb/apps/demo-dapp-with-react-ui/src/server).

### Proof request schema [#proof-request-schema]

```ts
// Not runnable: add this file to the linked server and install its dependencies.
import zod from 'zod';

export const CheckProofRequest = zod.object({
  address: zod.string(),
  network: zod.string().regex(/^-?\d+$/),
  public_key: zod.string(),
  proof: zod.object({
    timestamp: zod.coerce.number().int().nonnegative(),
    domain: zod.object({
      lengthBytes: zod.number(),
      value: zod.string(),
    }),
    payload: zod.string(),
    signature: zod.string(),
    state_init: zod.string(),
  }),
  payloadToken: zod.string(),
});

export type CheckProofRequestDto = zod.infer<typeof CheckProofRequest>;
```

### Proof verification service [#proof-verification-service]

```ts expandable
// Not runnable: add the referenced DTO and wallet parsing helpers.
import { getSecureRandomBytes, sha256 } from '@ton/crypto';
import {
  Address,
  Cell,
  contractAddress,
  domainSignVerify,
  loadStateInit,
  type SignatureDomain,
} from '@ton/ton';
import { Buffer } from 'buffer';
import { CheckProofRequestDto } from '../dto/check-proof-request-dto';
import { tryParsePublicKey } from '../wrappers/wallets-data';

const tonProofPrefix = 'ton-proof-item-v2/';
const tonConnectPrefix = 'ton-connect';
const allowedDomains = [
  'tonconnect-demo-dapp-with-react-ui.vercel.app',
  'ton-connect.github.io',
  'localhost:5173',
];
const validAuthTime = 15 * 60; // 15 minutes

function signatureDomain(network: string): SignatureDomain {
  return network === '-239' || network === '-3'
    ? { type: 'empty' }
    : { type: 'l2', globalId: Number(network) };
}

export class TonProofService {
  /** Generate random bytes for the payload. */
  public async generateRandomBytes(): Promise<Buffer> {
    return await getSecureRandomBytes(32);
  }

  /**
   * Verify the ton_proof payload.
   * Reference: https://github.com/ton-blockchain/ton-connect/blob/main/spec/connect.md#address-proof-signature-ton_proof
   */
  public async checkProof(
    payload: CheckProofRequestDto,
    getWalletPublicKey: (address: string) => Promise<Buffer | null>,
  ): Promise<boolean> {
    try {
      const stateInit = loadStateInit(
        Cell.fromBase64(payload.proof.state_init).beginParse(),
      );

      // 1. Try to obtain public key from stateInit.
      // 2. Fall back to get_public_key get method on-chain.
      let publicKey =
        tryParsePublicKey(stateInit) ??
        (await getWalletPublicKey(payload.address));
      if (!publicKey) {
        return false;
      }

      // Verify public key matches the one provided by TonAddressItemReply
      const wantedPublicKey = Buffer.from(payload.public_key, 'hex');
      if (!publicKey.equals(wantedPublicKey)) {
        return false;
      }

      // Verify address matches the stateInit hash
      const wantedAddress = Address.parse(payload.address);
      const address = contractAddress(wantedAddress.workChain, stateInit);
      if (!address.equals(wantedAddress)) {
        return false;
      }

      if (!allowedDomains.includes(payload.proof.domain.value)) {
        return false;
      }

      const domainBytes = Buffer.from(payload.proof.domain.value, 'utf8');
      if (payload.proof.domain.lengthBytes !== domainBytes.length) {
        return false;
      }

      const now = Math.floor(Date.now() / 1000);
      if (Math.abs(now - payload.proof.timestamp) > validAuthTime) {
        return false;
      }

      const message = {
        workchain: address.workChain,
        address: address.hash,
        domain: {
          lengthBytes: payload.proof.domain.lengthBytes,
          value: payload.proof.domain.value,
        },
        signature: Buffer.from(payload.proof.signature, 'base64'),
        payload: payload.proof.payload,
        stateInit: payload.proof.state_init,
        timestamp: payload.proof.timestamp,
      };

      const wc = Buffer.alloc(4);
      wc.writeInt32BE(message.workchain, 0);

      const ts = Buffer.alloc(8);
      ts.writeBigUInt64LE(BigInt(message.timestamp), 0);

      const dl = Buffer.alloc(4);
      dl.writeUInt32LE(message.domain.lengthBytes, 0);

      // message = utf8_encode("ton-proof-item-v2/") ++ Address ++ AppDomain ++ Timestamp ++ Payload
      const msg = Buffer.concat([
        Buffer.from(tonProofPrefix),
        wc,
        message.address,
        dl,
        domainBytes,
        ts,
        Buffer.from(message.payload, 'utf8'),
      ]);

      const msgHash = Buffer.from(await sha256(msg));

      // signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message)))
      const fullMsg = Buffer.concat([
        Buffer.from([0xff, 0xff]),
        Buffer.from(tonConnectPrefix),
        msgHash,
      ]);

      const result = Buffer.from(await sha256(fullMsg));

      return domainSignVerify({
        data: result,
        signature: message.signature,
        publicKey,
        domain: signatureDomain(payload.network),
      });
    } catch (e) {
      return false;
    }
  }
}
```

### API endpoints [#api-endpoints]

<Callout type="warning">
  The demo below verifies the payload token's signature and expiration, but that alone does not make it single-use. In production, store a nonce identifier on the backend and consume it atomically during `checkProof`; reject every later request that uses the same identifier.
</Callout>

<CodeGroup>
  <CodeBlockTabs defaultValue="Check proof">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="Check proof">
        Check proof
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Generate payload">
        Generate payload
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="Check proof">
      ```ts  icon="globe"
      // Not runnable: add the linked server's services, utilities, and DTO.
      import { sha256 } from '@ton/crypto';
      import { HttpResponseResolver } from 'msw';
      import { CheckProofRequest } from '../dto/check-proof-request-dto';
      import { TonApiService } from '../services/ton-api-service';
      import { TonProofService } from '../services/ton-proof-service';
      import { badRequest, ok } from '../utils/http-utils';
      import { createAuthToken, verifyToken } from '../utils/jwt';

      // POST /api/check_proof
      export const checkProof: HttpResponseResolver = async ({ request }) => {
        try {
          const body = CheckProofRequest.parse(await request.json());

          const client = TonApiService.create(body.network);
          const service = new TonProofService();

          const isValid = await service.checkProof(body, (address) =>
            client.getWalletPublicKey(address),
          );
          if (!isValid) {
            return badRequest({ error: 'Invalid proof' });
          }

          const payloadTokenHash = body.proof.payload;
          const payloadToken = body.payloadToken;
          if (!(await verifyToken(payloadToken))) {
            return badRequest({ error: 'Invalid token' });
          }
          if ((await sha256(payloadToken)).toString('hex') !== payloadTokenHash) {
            return badRequest({ error: 'Invalid payload token hash' });
          }

          const token = await createAuthToken({
            address: body.address,
            network: body.network,
          });

          return ok({ token: token });
        } catch (e) {
          console.error(e);
          return badRequest({ error: 'Invalid request' });
        }
      };
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Generate payload">
      ```ts  icon="globe"
      // Not runnable: add the linked server's services and utilities.
      import { sha256 } from '@ton/crypto';
      import { HttpResponseResolver } from 'msw';
      import { TonProofService } from '../services/ton-proof-service';
      import { badRequest, ok } from '../utils/http-utils';
      import { createPayloadToken } from '../utils/jwt';

      // POST /api/generate_payload
      export const generatePayload: HttpResponseResolver = async () => {
        try {
          const service = new TonProofService();

          const randomBytes = await service.generateRandomBytes();
          const payloadToken = await createPayloadToken({
            randomBytes: randomBytes.toString('hex'),
          });
          const payloadTokenHash = (await sha256(payloadToken)).toString('hex');

          return ok({
            payloadToken: payloadToken,
            payloadTokenHash: payloadTokenHash,
          });
        } catch (e) {
          console.error(e);
          return badRequest({ error: 'Invalid request' });
        }
      };
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Verification examples in other languages [#verification-examples-in-other-languages]

<Callout type="caution" title="Community examples">
  The following examples were created by the community — they may be out of date or unsuitable for production. Proceed with caution. Always test thoroughly on testnet before using real funds.
</Callout>

* [Go demo app](https://github.com/ton-connect/demo-dapp-backend)
* [Rust demo app](https://github.com/liketurbo/demo-dapp-backend-rs)
* [JS demo app](https://github.com/liketurbo/demo-dapp-backend-js)
* [Python example](https://github.com/XaBbl4/pytonconnect/blob/main/examples/check_proof.py)
* [PHP example](https://github.com/vladimirfokingithub/Ton-Connect-Proof-Php-Check)
* [C# demo app](https://github.com/WinoGarcia/TonProof.NET)

## See also [#see-also]

* [TON Connect overview](https://docs.ton.org/llms/applications/ton-connect/overview/content.md)
* [dApp integration](https://docs.ton.org/llms/applications/ton-connect/get-started/content.md)
* [SDK demo backend](https://github.com/ton-connect/sdk/tree/a07a165977eae17578b2414797f74d4bf54962eb/apps/demo-dapp-with-react-ui/src/server)
