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:
- Build a typed payload with one of the
StorageProgram.* static helpers (createStorageProgram, writeStorage, updateAccessControl, deleteStorageProgram, plus the granular setField / setItem / appendItem / deleteField / deleteItem).
- Sign it with
demos.storagePrograms.sign(payload) — this returns a signed Transaction.
- Confirm gas with
demos.confirm(tx).
- 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
| Limit | Value | Constant |
|---|
| Max payload size | 1 MB (1,048,576 bytes) | STORAGE_PROGRAM_CONSTANTS.MAX_SIZE_BYTES |
| Max JSON nesting depth | 64 | STORAGE_PROGRAM_CONSTANTS.MAX_JSON_NESTING_DEPTH |
| Pricing chunk | 10 KB (10,240 bytes) | STORAGE_PROGRAM_CONSTANTS.PRICING_CHUNK_BYTES |
| Fee per chunk | 1 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 Control —
owner / 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).