> For the complete documentation index, see [llms.txt](https://docs.kubchain.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kubchain.com/quickstart/build-an-app-on-kub.md).

# Build an App on KUB

This guide walks you through building an on-chain demo Tally app on the KUB Testnet: connecting wallets, reading and writing to a deployed counter contract, and understanding when to reach for the KUB SDK instead of standard wallet connectors.

## What you will build

* A [Next.js](https://nextjs.org/) app that connects EVM wallets (MetaMask and other injected wallets)
* Contract reads and writes
* An understanding of the two wallets on KUB: standard Web3 wallets and KUB Wallet

## Start Building

### Set up your project

```bash
npx create-next-app@latest my-kub-app --typescript --tailwind --app
cd my-kub-app
npm install wagmi viem @tanstack/react-query
```

### Define the KUB networks and configure Wagmi

[viem](https://github.com/wagmi-dev/viem/tree/main/src/chains/index.ts) ships chain definitions for KUB (exported as `bitkub` / `bitkubTestnet` in `viem/chains`Check your installed version. Defining the chains explicitly with `defineChain` also works on any version and makes the config self-documenting:

```typescript
// config/chains.ts
import { defineChain } from 'viem'

export const kubTestnet = defineChain({
  id: 25925,
  name: 'KUB Testnet',
  nativeCurrency: { name: 'tKUB', symbol: 'tKUB', decimals: 18 },
  rpcUrls: {
    default: { http: ['https://rpc-testnet.kubchain.io'] },
  },
  blockExplorers: {
    default: { name: 'KUB Scan Testnet', url: 'https://testnet.kubscan.com' },
  },
  testnet: true,
})

export const kubMainnet = defineChain({
  id: 96,
  name: 'KUB Mainnet',
  nativeCurrency: { name: 'KUB', symbol: 'KUB', decimals: 18 },
  rpcUrls: {
    default: { http: ['https://rpc.kubchain.io'] },
  },
  blockExplorers: {
    default: { name: 'KUB Scan', url: 'https://kubscan.com' },
  },
})
```

```typescript
// config/wagmi.ts
import { http, createConfig, createStorage, cookieStorage } from 'wagmi'
import { injected } from 'wagmi/connectors'
import { kubTestnet } from './chains'

export const config = createConfig({
  chains: [kubTestnet],
  connectors: [injected()],
  storage: createStorage({ storage: cookieStorage }),
  ssr: true,
  transports: {
    [kubTestnet.id]: http('https://rpc-testnet.kubchain.io'),
  },
})

declare module 'wagmi' {
  interface Register {
    config: typeof config
  }
}
```

{% hint style="info" %}
`ssr: true` combined with `cookieStorage` prevents Next.js hydration mismatches. This pairs with the `cookieToInitialState` call in the root layout below, which restores the connection state on the server so it does not flash on load. The injected connector handles browser extension wallets like MetaMask, which connect to KUB as a custom network.
{% endhint %}

### Wrap your app in the providers

The providers component accepts an `initialState` prop so the layout can pass in the connection state recovered from cookies:

```typescript
// app/providers.tsx
'use client'

import { WagmiProvider, type State } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { type ReactNode } from 'react'
import { config } from '@/config/wagmi'

const queryClient = new QueryClient()

export function Providers({
  children,
  initialState,
}: {
  children: ReactNode
  initialState?: State
}) {
  return (
    <WagmiProvider config={config} initialState={initialState}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </WagmiProvider>
  )
}
```

### Wire the providers into the root layout

Wrap `{children}` with `Providers` in `app/layout.tsx`. This step is required: any component calling a wagmi hook outside `WagmiProvider` crashes at runtime with `useConfig must be used within WagmiProvider`. Reading the cookie here and converting it with `cookieToInitialState` completes the SSR setup, so a returning user's connection state is already correct on first render instead of flashing through `Reconnecting...`:

```typescript
// app/layout.tsx
import type { Metadata } from 'next'
import { headers } from 'next/headers'
import { cookieToInitialState } from 'wagmi'
import { config } from '@/config/wagmi'
import { Providers } from './providers'
import './globals.css'

export const metadata: Metadata = {
  title: 'Onchain Tally',
}

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const initialState = cookieToInitialState(
    config,
    (await headers()).get('cookie')
  )

  return (
    <html lang="en">
      <body>
        <Providers initialState={initialState}>{children}</Providers>
      </body>
    </html>
  )
}
```

{% hint style="info" %}
`headers()` is asynchronous in recent Next.js versions, so the root layout is declared `async` and the call is awaited. If you are on an older Next.js where `headers()` is synchronous, drop the `await`.
{% endhint %}

### Connect wallets

```typescript
// components/ConnectWallet.tsx
'use client'

import { useAccount, useConnect, useDisconnect } from 'wagmi'

export function ConnectWallet() {
  const { address, isConnected, isConnecting, isReconnecting } = useAccount()
  const { connect, connectors } = useConnect()
  const { disconnect } = useDisconnect()

  if (isReconnecting) return <div>Reconnecting...</div>

  if (!isConnected) {
    return (
      <div className="flex flex-col gap-2">
        {connectors.map((connector) => (
          <button
            key={connector.uid}
            onClick={() => connect({ connector })}
            disabled={isConnecting}
          >
            Connect {connector.name}
          </button>
        ))}
      </div>
    )
  }

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-sm">
        {address?.slice(0, 6)}...{address?.slice(-4)}
      </span>
      <button onClick={() => disconnect()}>Disconnect</button>
    </div>
  )
}
```

{% hint style="info" %}
`useAccount` exposes four states: `isConnecting`, `isReconnecting`, `isConnected`, and `isDisconnected`. Checking only `isConnected` causes UI flashes on page load. Ensure you handle all four states.
{% endhint %}

### Deploy the contract

Follow this guide to deploy `Counter.sol` to KUB Testnet with Foundry, funded by the [KUB Faucet](https://faucet.kubchain.com/). Please take note of the deployed address.

{% hint style="warning" %}
Fund the account you will connect with in the browser too. The deployer key pays for the contract deployment, but clicking **Increment** later spends gas from your MetaMask account, which starts empty. Request tKUB from the [KUB Faucet](https://faucet.kubchain.com/) for that address as well — the faucet dispenses 5 tKUB per address every 24 hours.
{% endhint %}

### Read contract data

```typescript
// config/counter.ts
export const COUNTER_ADDRESS = '0x...' as const

export const counterAbi = [
  {
    type: 'function',
    name: 'number',
    inputs: [],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'increment',
    inputs: [],
    outputs: [],
    stateMutability: 'nonpayable',
  },
] as const
```

{% hint style="info" %}
`as const` is required. Without it, wagmi cannot infer function names, argument types, or return types from the ABI.
{% endhint %}

```typescript
// components/CounterDisplay.tsx
'use client'

import { useReadContract } from 'wagmi'
import { kubTestnet } from '@/config/chains'
import { COUNTER_ADDRESS, counterAbi } from '@/config/counter'

export function CounterDisplay() {
  const { data: count, isLoading, isError } = useReadContract({
    address: COUNTER_ADDRESS,
    abi: counterAbi,
    functionName: 'number',
    chainId: kubTestnet.id,
  })

  if (isLoading && count === undefined) return <p>Loading...</p>
  if (isError && count === undefined) return <p>Failed to read contract</p>

  return <p className="text-5xl font-bold">{count?.toString()}</p>
}
```

### Write to the contract

```typescript
// components/IncrementButton.tsx
'use client'

import { useEffect } from 'react'
import {
  useWriteContract,
  useWaitForTransactionReceipt,
  useChainId,
  useSwitchChain,
} from 'wagmi'
import { readContractQueryOptions } from 'wagmi/query'
import { useQueryClient } from '@tanstack/react-query'
import { config } from '@/config/wagmi'
import { kubTestnet } from '@/config/chains'
import { COUNTER_ADDRESS, counterAbi } from '@/config/counter'

export function IncrementButton() {
  const chainId = useChainId()
  const { switchChain, isPending: isSwitching } = useSwitchChain()
  const { data: hash, isPending, writeContract } = useWriteContract()
  const { isLoading: isConfirming, isSuccess } =
    useWaitForTransactionReceipt({ hash })
  const queryClient = useQueryClient()

  useEffect(() => {
    if (isSuccess) {
      queryClient.invalidateQueries({
        queryKey: readContractQueryOptions(config, {
          address: COUNTER_ADDRESS,
          abi: counterAbi,
          functionName: 'number',
          chainId: kubTestnet.id,
        }).queryKey,
      })
    }
  }, [isSuccess, queryClient])

  if (chainId !== kubTestnet.id) {
    return (
      <button onClick={() => switchChain({ chainId: kubTestnet.id })}>
        {isSwitching ? 'Switching...' : 'Switch to KUB Testnet'}
      </button>
    )
  }

  return (
    <div>
      <button
        onClick={() =>
          writeContract({
            address: COUNTER_ADDRESS,
            abi: counterAbi,
            functionName: 'increment',
            chainId: kubTestnet.id,
          })
        }
        disabled={isPending || isConfirming}
      >
        {isPending
          ? 'Confirm in Wallet...'
          : isConfirming
          ? 'Confirming...'
          : 'Increment'}
      </button>
      {isSuccess && <p>Confirmed!</p>}
      {hash && (
        <a href={`https://testnet.kubscan.com/tx/${hash}`} target="_blank">
          View on KUB Scan
        </a>
      )}
    </div>
  )
}
```

{% hint style="info" %}
Without `useSwitchChain`, calling `writeContract` while the wallet is on the wrong network causes wagmi to attempt a background chain switch. If the user misses the wallet popup, the button stays at "Confirm in Wallet..." indefinitely with no error and no recovery path. Since KUB is a custom network in MetaMask, users landing on your app for the first time are very often on the wrong chain; handle this state.
{% endhint %}

### Assemble the page

```typescript
// app/page.tsx
import { ConnectWallet } from '@/components/ConnectWallet'
import { CounterDisplay } from '@/components/CounterDisplay'
import { IncrementButton } from '@/components/IncrementButton'

export default function Home() {
  return (
    <main className="min-h-screen flex flex-col items-center justify-center gap-8 p-8">
      <h1 className="text-3xl font-bold">Onchain Tally</h1>
      <ConnectWallet />
      <CounterDisplay />
      <IncrementButton />
    </main>
  )
}
```

```bash
npm run dev
```

### Optional: Reach KUB Wallet users with the NEXT SDK

The integration differs from the wagmi flow in two important ways:

1. Install `@bitkub-chain/sdk.js`, initialize it with a Client ID and Project ID (from the [KUB Playground](https://playground.kubchain.com/) for KUB Testnet), and authenticate users via OAuth rather than a wallet popup

```typescript
import { initializeSDK, Network } from '@bitkub-chain/sdk.js'

const sdk = initializeSDK(clientID, projectID, Network.BKC_TESTNET, {
  loginRedirectPath: '/oauth/callback',
})

await sdk.loginWithBitkubNext()
const address = await sdk.getUserWalletAddress()
```

2. KUB Wallet users transact through the CallHelper router, so any write function they call needs an extra trailing `address _bitkubNext` parameter and a guard restricting the caller to the SDK CallHelper Router

```solidity
function increment(address _bitkubNext) external onlySdkCallHelperRouter {
    number++;
}

modifier onlySdkCallHelperRouter() {
    require(msg.sender == sdkCallHelperRouter, "Authorization: restricted only SDK CallHelper Router");
    _;
}
```

Transactions then go through `sdk.sendCustomTx(toAddress, functionReadableABI, methodParams)` rather than `writeContract`. A production KUB app typically supports both paths: wagmi/injected for MetaMask users, NEXT SDK for KUB Wallet users. See the [NEXT SDK](https://docs.kubchain.com/build-on-kub/libraries-and-sdks/next-sdk) and the [Code Cookbook](https://docs.kubchain.com/build-on-kub/libraries-and-sdks/next-sdk/code-cookbook) for working examples.

## Next Steps

* **Go to KUB Mainnet:** add `kubMainnet` to your `chains` array and transports, redeploy your contract, and update `COUNTER_ADDRESS`.
* **Scale on KUB Layer 2:** the rollup-based L2 (chain ID 9601 mainnet / 259251 testnet) offers lower costs for high-frequency apps; bridge tKUB from the faucet flow to the L2 testnet to experiment.
* **Bridge assets in:** [KUB Bridge](https://bridge.kubchain.com/en) supports Ethereum, BNB Smart Chain, and JFIN Chain, so users can bring assets to your app.
* **Get listed:** submit your dApp via the [KUB Developer Center](https://developers.kubchain.com/) to appear in the KUB Ecosystem directory.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kubchain.com/quickstart/build-an-app-on-kub.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
