Building Blocks from Scratch
By AnchorFactory ·
The high-level client.send() is great for simple transfers. But anchors and advanced services need full control — multi-operation blocks, chaining blocks in sequence, and publishing atomic staples across accounts.
This recipe covers the low-level Block Builder API.
When to use this
Your block needs multiple operations (e.g. SEND + RECEIVE in one block)
You need to chain blocks (each block references the previous one’s hash)
You’re building blocks for a different account than the signer (e.g. token account blocks signed by the pool owner)
You need atomic multi-block publishes (all-or-nothing staples)
For simple “send X tokens to Y”, use client.send() instead.
Core pattern
Every block needs four things: an account (whose chain this block lives on), a previous hash (chain head), a network ID, and one or more operations.
const KeetaNet = require("@keetanetwork/keetanet-client");
const seed = Buffer.from(process.env.SEED, "hex");
const account = KeetaNet.lib.Account.fromSeed(seed, 0);
// 1. Connect and get the chain head
const client = KeetaNet.UserClient.fromNetwork("test", account);
const head = await client.head();
// 2. Build the block
const block = await new KeetaNet.lib.Block.Builder({
version: 2,
account: account,
signer: account,
network: 0x54455354, // testnet
previous: head ?? KeetaNet.lib.Block.NO_PREVIOUS,
operations: [
{
type: KeetaNet.lib.Block.Builder.OperationType.SEND,
to: KeetaNet.lib.Account.fromPublicKeyString(recipientAddress),
token: KeetaNet.lib.Account.fromPublicKeyString(tokenAddress),
amount: 1000000000n, // 1 token (9 decimals on testnet)
}
]
}).seal();
// 3. Publish with fee block
await client.client.transmit([block], {
generateFeeBlock: async (staple) => {
const feeBuilder = client.initBuilder();
return await feeBuilder.computeFeeBlock(staple);
}
});Operation types
SEND
Transfer tokens from this account to a recipient.
{
type: KeetaNet.lib.Block.Builder.OperationType.SEND,
to: recipientAccount, // Account object
token: tokenAccount, // Token Account object
amount: 500000000n, // BigInt — raw amount, no decimals
external: "my-ref-123" // optional — metadata tag visible on-chain
}RECEIVE
Accept incoming tokens. Used in swap flows where both sides need to agree.
{
type: KeetaNet.lib.Block.Builder.OperationType.RECEIVE,
from: senderAccount, // who sent the tokens
token: tokenAccount,
amount: 500000000n,
exact: false, // false = "at least this amount"
forward: null // optional — forward to another account
}SET_INFO
Set name, description, and metadata on an account.
{
type: KeetaNet.lib.Block.Builder.OperationType.SET_INFO,
name: "My Service",
description: "An anchor service on Keeta",
metadata: btoa(JSON.stringify({ decimalPlaces: 9 })) // base64 JSON
}TOKEN_ADMIN_SUPPLY (mint / burn)
Adjust the supply of a token you administer.
{
type: KeetaNet.lib.Block.Builder.OperationType.TOKEN_ADMIN_SUPPLY,
amount: 1000000000000000000n,
method: KeetaNet.lib.Block.Builder.AdjustMethod.ADD // ADD = mint, SUBTRACT = burn
}CREATE_IDENTIFIER
Generate a new token address from a parent account.
{
type: KeetaNet.lib.Block.Builder.OperationType.CREATE_IDENTIFIER,
index: 0 // unique index per parent account
}Full example: send tokens
Here’s a complete, runnable script that sends tokens from one account to another on testnet.
const KeetaNet = require("@keetanetwork/keetanet-client");
async function sendTokens() {
// Your account (derived from seed)
const seed = Buffer.from(process.env.SEED, "hex");
const sender = KeetaNet.lib.Account.fromSeed(seed, 0);
// Recipient (public key only — no private key needed)
const recipient = KeetaNet.lib.Account.fromPublicKeyString(
"keeta_aab..." // replace with actual address
);
// Token to send
const token = KeetaNet.lib.Account.fromPublicKeyString(
"keeta_anyiff4v3..." // KTA on testnet, or any token address
);
// Connect to testnet
const client = KeetaNet.UserClient.fromNetwork("test", sender);
// Get the sender's chain head (latest block hash)
const head = await client.head();
// Build the send block
const block = await new KeetaNet.lib.Block.Builder({
version: 2,
account: sender,
signer: sender,
network: 0x54455354, // testnet network ID
previous: head ?? KeetaNet.lib.Block.NO_PREVIOUS,
operations: [
{
type: KeetaNet.lib.Block.Builder.OperationType.SEND,
to: recipient,
token: token,
amount: 5000000000n, // 5 tokens (9 decimals)
}
]
}).seal();
// Publish — includes automatic fee block generation
await client.client.transmit([block], {
generateFeeBlock: async (staple) => {
const feeBuilder = client.initBuilder();
return await feeBuilder.computeFeeBlock(staple);
}
});
console.log("Sent! Block hash:", block.hash);
}
sendTokens().catch(console.error);Multi-operation blocks
A single block can contain multiple operations. This is common in swap flows where a pool sends tokens out and receives tokens in:
const operations = [
{
type: KeetaNet.lib.Block.Builder.OperationType.SEND,
to: traderAccount,
token: outputToken,
amount: outputAmount,
},
{
type: KeetaNet.lib.Block.Builder.OperationType.RECEIVE,
from: traderAccount,
token: inputToken,
amount: inputAmount,
exact: false,
}
];
const block = await new KeetaNet.lib.Block.Builder({
version: 2,
account: poolAccount,
signer: poolAccount,
network: 0x54455354,
previous: head,
operations
}).seal();Chaining blocks
When you publish multiple blocks for the same account, each block must reference the previous block’s hash — not the original chain head.
// First block uses the chain head
const block1 = await new KeetaNet.lib.Block.Builder({
version: 2,
account: poolAccount,
signer: poolAccount,
network: 0x54455354,
previous: head,
operations: [/* ... */]
}).seal();
// Second block chains off the first
const block2 = await new KeetaNet.lib.Block.Builder({
version: 2,
account: poolAccount,
signer: poolAccount,
network: 0x54455354,
previous: block1.hash, // chain from block1, not from head
operations: [/* ... */]
}).seal();
// Publish both atomically
await client.client.transmit([block1, block2], {
generateFeeBlock: async (staple) => {
const feeBuilder = client.initBuilder();
return await feeBuilder.computeFeeBlock(staple);
}
});Signing on behalf of another account
Token accounts don’t have their own private key — they’re administered by a parent account. When building blocks on a token’s chain, set account to the token and signer to the admin:
const tokenAccount = KeetaNet.lib.Account.fromPublicKeyString(tokenAddress);
const adminAccount = KeetaNet.lib.Account.fromSeed(seed, 0);
// Get the token's chain head (need admin as signer for access)
const tokenClient = KeetaNet.UserClient.fromNetwork("test", adminAccount, {
account: tokenAccount
});
const tokenHead = await tokenClient.head();
const mintBlock = await new KeetaNet.lib.Block.Builder({
version: 2,
account: tokenAccount, // block lives on the token's chain
signer: adminAccount, // signed by the admin
network: 0x54455354,
previous: tokenHead ?? KeetaNet.lib.Block.NO_PREVIOUS,
operations: [{
type: KeetaNet.lib.Block.Builder.OperationType.TOKEN_ADMIN_SUPPLY,
amount: 1000000000000000000n,
method: KeetaNet.lib.Block.Builder.AdjustMethod.ADD
}]
}).seal();Fee blocks
Every publish requires a fee block — a small KTA payment for network validation. The SDK handles fee calculation automatically:
await client.client.transmit(blocks, {
generateFeeBlock: async (staple) => {
// feeAccount pays the fee (must have KTA balance)
const feeClient = KeetaNet.UserClient.fromNetwork("test", feeAccount);
const feeBuilder = feeClient.initBuilder();
return await feeBuilder.computeFeeBlock(staple);
}
});The fee account can be different from the block’s account — useful when a service pays fees on behalf of users.
Account helpers
// From a seed (has private key — can sign)
const account = KeetaNet.lib.Account.fromSeed(seed, index);
// From a public key string (no private key — read-only)
const account = KeetaNet.lib.Account.fromPublicKeyString("keeta_aab...");
// Get the address string
const address = account.publicKeyString.get(); // NOT .toString()Common patterns
Pattern | Operations | Use Case |
|---|---|---|
Simple send | 1x SEND | Token transfer |
Swap | 1x SEND + 1x RECEIVE | DEX trade (one side) |
Token creation | CREATE_IDENTIFIER + SET_INFO + MINT + SEND(s) | New token launch |
Fee sweep | SEND (from fee account) | Collect accumulated fees |
Metadata update | SET_INFO | Update service registration |