Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kynesys.xyz/llms.txt

Use this file to discover all available pages before exploring further.

Getting Started with Storage Programs

This guide walks you through creating a Storage Program, writing data to it, and reading it back over RPC.

Prerequisites

  • Bun (latest) installed
  • The Demos SDK: bun add @kynesyslabs/demosdk@latest
  • A Demos wallet with enough balance to cover storage fees (1 DEM per 10 KB chunk, minimum 1 DEM)
  • Access to a Demos Network RPC node

The mental model

Every Storage Program operation follows the same four-step shape:
  1. Build a typed payload with one of the StorageProgram.* static helpers (createStorageProgram, writeStorage, updateAccessControl, deleteStorageProgram, plus the granular setField / setItem / appendItem / deleteField / deleteItem).
  2. Sign it with demos.storagePrograms.sign(payload) — this returns a signed Transaction.
  3. Confirm gas with demos.confirm(tx).
  4. Broadcast with demos.broadcast(validityData) (or demos.broadcastAndWait for deterministic inclusion).
Reads are different — they go through demos.storagePrograms.read(address) directly, no transaction required.

Step 1: Connect

import { Demos } from "@kynesyslabs/demosdk/websdk"
import { StorageProgram } from "@kynesyslabs/demosdk/storage"

const demos = new Demos()
await demos.connect("https://node2.demos.sh")
await demos.connectWallet(process.env.MNEMONIC!)

const ownerAddress = demos.getAddress()  // sync, returns string
demos.getAddress() is synchronous and returns the connected wallet’s address as a string. It does not return a Promise.

Step 2: Build the create payload

StorageProgram.createStorageProgram(deployer, programName, data, encoding, acl?, options) builds the payload for the create transaction. The deterministic storage address is derived inside the helper — you don’t compute it separately.
const nonce = await demos.getAddressNonce(ownerAddress)

const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "userProfile",
    {
        displayName: "Alice",
        joinedAt: Date.now(),
        stats: { posts: 0, followers: 0 },
    },
    "json",
    { mode: "public" },        // anyone reads, only owner writes
    { nonce, salt: "v1" },     // nonce is required; salt is optional
)

console.log("storageAddress:", payload.storageAddress)
// stor-{40 hex chars} — derived from sha256(deployer:programName:nonce:salt)
options.nonce is required. It is the sender’s current account nonce, fetched via demos.getAddressNonce(ownerAddress). Without it the helper throws "nonce is required for storage program creation". The nonce is mixed into the address derivation so each create from the same deployer produces a unique address even when programName collides.

Step 3: Sign, confirm, broadcast

const tx = await demos.storagePrograms.sign(payload)
const validityData = await demos.confirm(tx)

// Option A — fire-and-forget; returns when the node accepts the tx.
const res = await demos.broadcast(validityData)

// Option B — wait for inclusion in a block.
const { broadcast, status } = await demos.broadcastAndWait(validityData)
if (status.state !== "included") {
    throw new Error(`Storage program not included; ended at ${status.state}`)
}
See Broadcasting a Transaction for the difference between broadcast and broadcastAndWait and the error types each can throw.

Step 4: Write data

writeStorage replaces the entire data field of the program. For surgical updates use the granular operations below.
const writePayload = StorageProgram.writeStorage(
    payload.storageAddress,
    {
        displayName: "Alice",
        bio: "Web3 builder",
        stats: { posts: 1, followers: 5 },
    },
    "json",
)

const writeTx = await demos.storagePrograms.sign(writePayload)
await demos.broadcast(await demos.confirm(writeTx))

Step 5: Read data

Reads are served directly over RPC — no transaction, no fee.
const result = await demos.storagePrograms.read(payload.storageAddress)

if (result.success) {
    console.log("data:", result.data)         // your stored payload
    console.log("metadata:", result.metadata) // optional metadata
    console.log("size:", result.sizeBytes)
} else {
    console.error(result.error, result.errorCode)
}
The full response shape is StorageProgramResponse from @kynesyslabs/demosdk/storage. Notable fields: owner, programName, encoding, data, metadata, storageLocation, sizeBytes, createdAt, updatedAt. See RPC Queries for queries beyond the address-based lookup.

Granular updates

For JSON-encoded programs, you can change individual fields and array elements without rewriting the whole document:
// Set a single field
const a = StorageProgram.setField(payload.storageAddress, "bio", "Updated bio")

// Append to an array
const b = StorageProgram.appendItem(payload.storageAddress, "posts", { id: 7 })

// Replace an array element (negative indices supported, -1 = last)
const c = StorageProgram.setItem(payload.storageAddress, "posts", 0, { id: 8 })

// Delete a field
const d = StorageProgram.deleteField(payload.storageAddress, "legacyConfig")

// Delete an array element by index
const e = StorageProgram.deleteItem(payload.storageAddress, "posts", -1)
Each returns a StorageProgramPayload you sign + confirm + broadcast like any other write. Granular operations are JSON-only — they are rejected for binary-encoded programs.

Updating access control and deleting

// Tighten access to the owner only
const aclPayload = StorageProgram.updateAccessControl(
    payload.storageAddress,
    { mode: "owner" },
)

// Remove the program permanently (owner / ACL-permissioned only)
const delPayload = StorageProgram.deleteStorageProgram(payload.storageAddress)
Both return payloads that follow the same sign / confirm / broadcast flow.

Fees

Storage Programs are billed at 1 DEM per 10 KB chunk, minimum 1 DEM per write. Calculate the fee for a payload before sending it:
import { denomination } from "@kynesyslabs/demosdk"

const feeOs = StorageProgram.calculateStorageFee(
    { displayName: "Alice", stats: { posts: 0 } },
    "json",
)
console.log(`fee: ${denomination.osToDem(feeOs)} DEM`)
The fee is returned as a bigint in OS (1 DEM = 10⁹ OS). See Amounts & Denominations for the conversion helpers.

Resource limits

LimitValueConstant
Max payload size1 MB (1,048,576 bytes)STORAGE_PROGRAM_CONSTANTS.MAX_SIZE_BYTES
Max JSON nesting depth64STORAGE_PROGRAM_CONSTANTS.MAX_JSON_NESTING_DEPTH
Pricing chunk10 KB (10,240 bytes)STORAGE_PROGRAM_CONSTANTS.PRICING_CHUNK_BYTES
Fee per chunk1 DEM (= 10⁹ OS)STORAGE_PROGRAM_CONSTANTS.FEE_PER_CHUNK
import { STORAGE_PROGRAM_CONSTANTS } from "@kynesyslabs/demosdk/storage"

if (!StorageProgram.validateSize(myData, "json")) {
    throw new Error(`Payload exceeds ${STORAGE_PROGRAM_CONSTANTS.MAX_SIZE_BYTES} bytes`)
}

if (!StorageProgram.validateNestingDepth(myData)) {
    throw new Error("JSON nesting too deep (>64 levels)")
}

Where to go next

  • Operations — when to use create vs. write vs. granular ops, and the full lifecycle.
  • Access Controlowner / public / restricted modes, allowlists, blacklists, and group permissions.
  • RPC Queries — full read-side surface, including the node-level RPC endpoints for listing, searching, and field-level lookups.
  • API Reference — every StorageProgram static helper, payload shape, and response interface.
  • Cookbook — end-to-end recipes (public profile, team workspace, binary attachments).