DEV Community

Mulandi Cecilia
Mulandi Cecilia

Posted on

Getting Started with Polkadot API (PAPI): Building a Balance Checker

Introduction

PAPI (Polkadot API) is the next-generation library for building on Polkadot. In this tutorial, we'll build a simple balance checker that demonstrates PAPI's core features: type safety, Smoldot light client integration, and on-chain queries.

Why PAPI?

PAPI is the officially recommended library for Polkadot development, offering:

  • Full TypeScript type safety - Catch errors at compile time, not runtime
  • Excellent performance - Optimized for speed with smaller bundle sizes
  • Built-in Smoldot light client - Build truly decentralized applications
  • Developer experience - IDE autocomplete and inline documentation
  • Future-proof - Actively maintained and officially supported

Prerequisites

Setup

Let's get started!

  1. Create a directory and cd into it
   mkdir papi-tutorial && cd papi-tutorial
Enter fullscreen mode Exit fullscreen mode
  1. Initialize a project using bun
   bun init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install polkadot api
   bun install polkadot-api
Enter fullscreen mode Exit fullscreen mode
  1. Add chain descriptors for the desired chain (in this case Polkadot)
   bun papi add -n polkadot dot
Enter fullscreen mode Exit fullscreen mode

Code Breakdown

For the following code we will query the account balance for this address:

const ADDRESS = "16JGzEsi8gcySKjpmxHVrkLTHdFHodRepEz8n244gNZpr9J";
Enter fullscreen mode Exit fullscreen mode

1. Smoldot Light Client Setup

PAPI uses light clients because they are:

  • Decentralized - No dependency on RPC providers
  • Trustless - Cryptographically verify all data
  • Browser-compatible - Can run anywhere, even in browsers

For better performance we will use worker threads (it keeps the main thread responsive):

// 1. start a light client on a new worker
// This helps with better performance since
// all the intensive work is moved from the main thread
const workerPath = fileURLToPath(
  import.meta.resolve("polkadot-api/smoldot/node-worker")
);
const worker = new Worker(workerPath);
const smoldot = startFromWorker(worker);
const chain = getSmProvider(smoldot.addChain({ chainSpec }));
Enter fullscreen mode Exit fullscreen mode

2. Create a PAPI Client

PAPI helps you connect your app to the blockchain. When you create a PAPI client, you get access to all APIs that allow you to:

  • Query the blockchain
  • Submit transactions
  • Subscribe to events
  • And much more!
// 2. Now we can create a papi client
console.log("Creating a PAPI client...");
const client = createClient(chain);

// 3. Get the api that will help us fetch information from the chain
const api = client.getTypedApi(dot);
Enter fullscreen mode Exit fullscreen mode

3. Wait for the Light Client to Sync Headers

Light clients need to sync headers before queries work properly. finalizedBlock$ is an Observable that emits new finalized blocks:

console.log("Waiting for chain to sync...");
await new Promise((resolve) => {
  const subscription = client.finalizedBlock$.subscribe((block) => {
    console.log(`Chain synced to block #${block.number}`);
    subscription.unsubscribe();
    resolve(block);
  });
});
Enter fullscreen mode Exit fullscreen mode

4. Querying Balance

For now we will query the free balance, which is the balance a user can spend:

console.log("Querying account info...");
  const accountInfo = await api.query.System.Account.getValue(ADDRESS);
  const freebal = accountInfo.data.free;
  const freebalInDot = Number(freebal) / Math.pow(10, 10);
console.log(`The current free balance for user ${ADDRESS} is ${freebalInDot} DOT`);
Enter fullscreen mode Exit fullscreen mode

Understanding the conversion:

  • Balances are stored in plancks (smallest unit)
  • 1 DOT = 10^10 plancks
  • We divide by 10^10 to convert to DOT

To understand more about how balances are handled in Polkadot, read this: Polkadot Account Balances

Complete Code

Here's the full implementation:

import { startFromWorker } from "polkadot-api/smoldot/from-node-worker";
import { getSmProvider } from "polkadot-api/sm-provider";
import { chainSpec } from "polkadot-api/chains/polkadot";
import { createClient } from "polkadot-api";
import { dot } from "@polkadot-api/descriptors";
import { Worker } from "worker_threads";
import { fileURLToPath } from "url";
const ADDRESS = "16JGzEsi8gcySKjpmxHVrkLTHdFHodRepEz8n244gNZpr9J";
async function main() {
  // 1. start a light client on a new worker
  // This is helps with better performance since
  // all the intensive work is moved from the main thread
  const workerPath = fileURLToPath(
    import.meta.resolve("polkadot-api/smoldot/node-worker")
  );
  const worker = new Worker(workerPath);
  const smoldot = startFromWorker(worker);
  const chain = getSmProvider(smoldot.addChain({ chainSpec }));
  //2. Now we can create a papi client
  console.log("Creating a PAPI client...");
  const client = createClient(chain);
  //3. Get the api that will help us fetch information from the chain
  const api = client.getTypedApi(dot);
  console.log("Waiting for chain to sync...");
  await new Promise((resolve) => {
    const subscription = client.finalizedBlock$.subscribe((block) => {
      console.log(`Chain synced to block #${block.number}`);
      subscription.unsubscribe();
      resolve(block);
    });
  });
  console.log("Querying account info...");
  const accountInfo = await api.query.System.Account.getValue(ADDRESS);
  const freebal = accountInfo.data.free;
  const freebalInDot = Number(freebal) / Math.pow(10, 10);
  console.log(`The current free balance for user ${ADDRESS} is ${freebalInDot} DOT`);
}
main();

Enter fullscreen mode Exit fullscreen mode

Running the Application

Execute your application:

bun run index.ts
Enter fullscreen mode Exit fullscreen mode

Expected output:

Creating a PAPI client...
Waiting for chain to sync...
[smoldot] Smoldot v2.0.39
[smoldot] Chain initialization complete for polkadot. Name: "Polkadot". Genesis hash: 0x91b1…90c3. Chain specification starting at: 0x0b34…aaaa (#27920033)
[runtime-polkadot] Successfully compiled runtime. Spec version: 1006002. Size of `:code`: 1.9 MiB.
[smoldot] The task named `runtime-polkadot` has occupied the CPU for an unreasonable amount of time (153ms).
Chain synced to block #27920033
Querying account info...
[runtime-polkadot] Finalized block runtime ready. Spec version: 1007001. Size of `:code`: 2.0 MiB.
[runtime-polkadot] Successfully compiled runtime. Spec version: 1007001. Size of `:code`: 2.0 MiB.
[smoldot] Smoldot v2.0.39. Current memory usage: 16.2 MiB. Average download: 322 kiB/s. Average upload: 926 B/s. Average CPU cores: 0.05.
[smoldot] The task named `runtime-polkadot` has occupied the CPU for an unreasonable amount of time (400ms).
The current free balance for user 16JGzEsi8gcySKjpmxHVrkLTHdFHodRepEz8n244gNZpr9J is 72.2473249769 DOT
Enter fullscreen mode Exit fullscreen mode

Note: First run takes 30-60 seconds while Smoldot syncs headers.

Next Steps

Now that you understand PAPI basics, try:

  • Query multiple addresses
  • Display reserved and frozen balances
  • Subscribe to real-time balance changes
  • Build a REST API or web interface

Resources

Conclusion

PAPI makes Polkadot development safer and faster with type safety and seamless light client integration. This balance checker demonstrates the foundation for building any PAPI application.

Find this tutorial code here:[github link]https://github.com/CECILIA-MULANDI/polkadot-balance-checker.git under the tutorial/ folder.

If you'd like to explore more: Look at the frontend/ and backend/ folders where I have added a simple UI and a nodejs backend with the PAPI code and connected them!

Happy building! 🚀

Top comments (0)