Arkiv SDK for TS - Getting Started
1) Arkiv "Hello, World!"
Create your account
This allows you to interact with Arkiv Testnet. Your is safe to share. Never share your .
Safety: The account you generate here is for Arkiv Testnet/sandbox use only. Never use it on any Mainnet.
2) Voting Board
You’ve written your first entity to Arkiv - now let’s build something that feels more alive. Using the same test account and client, we’ll create a tiny Voting Board: a simple structure where people can open proposals, cast votes, and read results directly from the chain.
This part of the guide shows how a few small entities can already form a collaborative application - all powered by Arkiv.
You can keep experimenting right here in the CodePlayground, or set up the SDK locally to continue from your own environment.
1. Proposal
A single entity that defines what is being decided and how long the voting stays open (expiresIn).
2. Votes
Multiple small entities that reference the proposal by its entityKey and store each voter’s choice.
3. Tally
A read query that fetches all votes linked to a proposal and counts them - the simplest form of an on-chain result.
Next up: we’ll create the proposal entity - the anchor for every vote that follows.
3) Open Proposal
Create the decision “room”: a proposal entity with a bounded time window (Expires In). This is where votes will attach-still using the very same client/account you verified.
- Goal: Write a proposal entity with an expiration window (Expires In).
- Why it matters: Gives your vote stream a clear scope and predictable cost.
- Success check: You get a proposal.entityKey (the proposal ID).
1import { createWalletClient, createPublicClient, http } from '@arkiv-network/sdk';
2import { stringToPayload } from '@arkiv-network/sdk/utils';
3import { mendoza } from '@arkiv-network/sdk/chains';
4import { eq } from '@arkiv-network/sdk/query';
5import { privateKeyToAccount } from 'viem/accounts';
6import { config } from 'dotenv';
7
8config({ path: '.env' }); // Load environment variables from .env file - needed only if you don't use BUN, BUN loads .env out of the box
9
10const walletClient = createWalletClient({
11 chain: mendoza,
12 transport: http(),
13 account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
14});
15
16const publicClient = createPublicClient({
17 chain: mendoza,
18 transport: http('https://mendoza.hoodi.arkiv.network/rpc'),
19});
20
21
22const { entityKey: proposalKey } = await walletClient.createEntity({
23 payload: stringToPayload('Proposal: Switch stand-up to 9:30?'),
24 contentType: 'text/plain',
25 attributes: [
26 { key: 'type', value: 'proposal' },
27 { key: 'status', value: 'open' },
28 { key: 'version', value: '1' },
29 ],
30 expiresIn: 200, // seconds
31});
32console.log('Proposal key:', proposalKey);4) Cast Votes
Attach votes to the proposal. Each vote is its own entity linked by proposalKey and attributed to a voter address. Same client, same journey-now with multiple actors.
- Goal: Create votes with { type="vote", proposalKey, voter, choice }.
- Why it matters: Votes are small, auditable facts you can query later.
- Success check: Two vote keys print, both linked to your proposal.
1const voterAddr = walletClient.account.address;
2
3await walletClient.mutateEntities({
4 creates: [
5 {
6 payload: stringToPayload('vote: no'),
7 contentType: 'text/plain',
8 attributes: [
9 { key: 'type', value: 'vote' },
10 { key: 'proposalKey', value: proposalKey },
11 { key: 'voter', value: voterAddr },
12 { key: 'choice', value: 'no' },
13 { key: 'weight', value: '1' },
14 ],
15 expiresIn: 200,
16 },
17 ],
18});
19console.log('Votes cast for proposal:', proposalKey);5) Batch Votes
Add many votes in one go-useful for demos, fixtures, or cross-proposal actions. You’re still operating with the same client and proposal context.
- Goal: Create multiple vote entities in a single call.
- Success check: Receipt count matches the number you pushed.
1const creates = Array.from({ length: 5 }, (_, i) => ({
2 payload: stringToPayload(`vote: yes #${i + 1}`),
3 contentType: 'text/plain' as const,
4 attributes: [
5 { key: 'type', value: 'vote' },
6 { key: 'proposalKey', value: proposalKey },
7 { key: 'voter', value: `${voterAddr}-bot${i}` },
8 { key: 'choice', value: 'yes' },
9 { key: 'weight', value: '1' },
10 ],
11 expiresIn: 200,
12}));
13
14await walletClient.mutateEntities({ creates });
15console.log(`Batch created: ${creates.length} votes`);6) Tally Votes
Read the chain back. Query annotated entities to compute the result. Because reads are deterministic, the same query yields the same answer.
- Goal: Query votes by proposalKey and choice.
- Success check: YES/NO counts match your inputs.
1const yes = await publicClient
2 .buildQuery()
3 .where([eq("type", "vote"), eq("proposalKey", proposalKey), eq("choice", "yes")])
4 .fetch();
5
6const no = await publicClient
7 .buildQuery()
8 .where([eq("type", "vote"), eq("proposalKey", proposalKey), eq("choice", "no")])
9 .fetch();
10
11console.log(`Tallies - YES: ${yes.entities.length}, NO: ${no.entities.length}`);7) Watch Live
Subscribe to creations and extensions in real time. No polling-just logs as the story unfolds. Keep the same client; it already knows where to listen.
- Goal: Subscribe to creation and extension events for votes (and proposals).
- Success check: Console logs “[Vote created] …” or “[Vote extended] …”.
1const stop = await publicClient.subscribeEntityEvents({
2 onEntityCreated: async (e) => {
3 try {
4 const ent = await publicClient.getEntity(e.entityKey);
5 const attrs = Object.fromEntries(
6 ent.attributes.map(a => [a.key, a.value])
7 );
8 const text = ent.toText();
9
10 if (attrs.type === 'vote') {
11 console.log('[Vote created]', text, 'key=', e.entityKey);
12 } else if (attrs.type === 'proposal') {
13 console.log('[Proposal created]', text, 'key=', e.entityKey);
14 }
15 } catch (err) {
16 console.error('[onEntityCreated] error:', err);
17 }
18 },
19
20 onEntityExpiresInExtended: (e) => {
21 console.log('[Extended]', e.entityKey, '→', e.newExpirationBlock);
22 },
23
24 onError: (err) => console.error('[subscribeEntityEvents] error:', err),
25});
26
27console.log('Watching for proposal/vote creations and extensions…');8) Extend Window
Need more time to decide? Extend the proposal’s Expires In. You’re updating the same entity you opened earlier-continuing the narrative of one decision from start to finish.
- Goal: Extend the proposal entity by N blocks.
- Success check: Console prints the new expiration block.
1const { txHash, entityKey } = await walletClient.extendEntity({
2 entityKey: proposalKey,
3 expiresIn: 150,
4});
5console.log('Proposal extended, tx:', txHash);Setup & Installation
If you want to run this outside the browser (CI, local ts-node, a service), set up the SDK in your own project. This section shows package.json, .env and a reference script so you can run the same Voting Board flow from your terminal.
Prerequisites
SDK v0.4.4 (tested with @arkiv-network/sdk@0.4.4). Requires Node.js 22+ (LTS) or newer; verified on Node.js 22.10. Support for Bun 1.x runtime is available.
Node.js 22+ (or Bun 1.x)
LTS recommended
TypeScript 5+ (optional)
For typed scripts
Ethereum Wallet
With test ETH for your RPC
RPC Endpoint
HTTP + (optionally) WS
Installation
1# Using npm
2npm init -y
3npm i @arkiv-network/sdk dotenv typescript
4
5# or with Bun
6bun init -y
7bun add @arkiv-network/sdk dotenvtsconfig.json (optional)
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
6 "strict": true,
7 "esModuleInterop": true,
8 "skipLibCheck": true
9 },
10 "include": ["*.ts"]
11}package.json (scripts)
1{
2 "type": "module",
3 "scripts": {
4 "start": "npx tsx voting-board.ts",
5 "build": "npx tsc",
6 "dev": "npx tsx watch voting-board.ts"
7 },
8 "dependencies": {
9 "@arkiv-network/sdk": "^0.4.4",
10 "dotenv": "^16.4.5"
11 },
12 "devDependencies": {
13 "tsx": "^4.19.2",
14 "typescript": "^5.6.3"
15 }
16}Environment Configuration
1# .env
2PRIVATE_KEY=0x... # use the (TEST) private key generated above
3Running the code
1# Using npm
2npx tsx voting-board.ts
3# or
4npm run start
5
6# or with Bun
7bun run voting-board.tsTroubleshooting
Invalid sender: Your RPC may point to an unexpected network for your key. Verify your RPC URL is correct.
Insufficient funds: Get test ETH from the faucet; writes require gas.
No events seen? Ensure fromBlock is low enough and keep the process running to receive logs.