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
- Node.js 20.6.0+ - Download here
-
TypeScript 5.2+ - Install with
npm install -g typescript
- Bun - Install here
Setup
Let's get started!
- Create a directory and cd into it
mkdir papi-tutorial && cd papi-tutorial
- Initialize a project using bun
bun init -y
- Install polkadot api
bun install polkadot-api
- Add chain descriptors for the desired chain (in this case Polkadot)
bun papi add -n polkadot dot
Code Breakdown
For the following code we will query the account balance for this address:
const ADDRESS = "16JGzEsi8gcySKjpmxHVrkLTHdFHodRepEz8n244gNZpr9J";
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 }));
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);
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);
});
});
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`);
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();
Running the Application
Execute your application:
bun run index.ts
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
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)