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.

RPC Queries

Storage Program reads are served over plain RPC. They cost nothing, require no transaction, and typically come back in well under 100 ms. This page covers the read surface only — for write flow see Operations.

Two read paths

You wantUse
The full program for a known addressdemos.storagePrograms.read(address) — the SDK shortcut
Anything else (list by owner, search, field-level lookup, type/exists checks)demos.rpcCall({ method, params }) against one of the 9 node endpoints below
The SDK does not wrap the granular endpoints. There is no demos.storagePrograms.list(), .search(), or .getField(). To call those, drop down to demos.rpcCall(...).
The SDK’s read helper attaches the wallet’s identity automatically (via signMessage) when a wallet is connected. ACL is enforced server-side: anonymous callers see only public programs; restricted/owner programs require the requesterAddress (passed implicitly by read, explicitly by rpcCall) to be in the ACL.

Reading a single program

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

const demos = new Demos()
await demos.connect("https://node2.demos.sh")
await demos.connectWallet(process.env.MNEMONIC!)  // optional; needed for ACL-gated reads

const result = await demos.storagePrograms.read("stor-7a8b9c...")

if (!result.success) {
    console.error(result.error, result.errorCode)
} else {
    console.log("owner:    ", result.owner)
    console.log("name:     ", result.programName)
    console.log("encoding: ", result.encoding)
    console.log("data:     ", result.data)
    console.log("metadata: ", result.metadata)
    console.log("size:     ", result.sizeBytes, "bytes")
    console.log("updated:  ", result.updatedAt)
}
Under the hood this performs GET <rpc>/storage-program/<address> and, when a wallet is connected, attaches identity: ed25519:<address> and signature: <ed25519-sig> headers. The node uses those headers to resolve the requester for ACL.

StorageProgramResponse shape

This is the actual response interface from @kynesyslabs/demosdk/storage:
interface StorageProgramResponse {
    success: boolean
    storageAddress?: string
    owner?: string
    programName?: string
    encoding?: "json" | "binary"
    data?: Record<string, unknown> | string | null
    metadata?: Record<string, unknown> | null
    storageLocation?: string
    sizeBytes?: number
    createdAt?: string  // ISO 8601
    updatedAt?: string  // ISO 8601
    error?: string
    errorCode?: string
}
There is no data.variables or data.metadata nesting. data, metadata, owner, programName, encoding, sizeBytes, createdAt, updatedAt all live at the top level of StorageProgramResponse. For binary-encoded programs data is a base64-encoded string; for JSON programs it is a Record<string, unknown>.

Read-time access control

success: false plus an errorCode is how access denials surface. The SDK does not throw on ACL rejection — it returns a structured response. Always branch on result.success before reading result.data.

The 9 node RPC endpoints

All of these are reached via demos.rpcCall({ method, params: [{ message, data, muid }] }). The message is the endpoint name; the data object carries the parameters. Most endpoints accept an optional requesterAddress for ACL — pass demos.getAddress() when you have a connected wallet and want non-public programs to resolve.
rpcCall returns an RPCResponse with shape { result: number, response: any, require_reply: boolean, extra: any }. result is an HTTP-style status code (200 on success, 400/403/404/500 on failure). The actual payload lives on response. When a program is missing or soft-deleted, the node returns 200 / null (not 404) for single-program reads.

1. getStorageProgram — full program by address

Same data the SDK’s read helper returns, but reachable via rpcCall if you’d rather not go through the HTTP route.
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgram",
        data: {
            storageAddress: "stor-7a8b9c...",
            requesterAddress: demos.getAddress(),  // optional; required for non-public ACL
        },
        muid: `storage-${Date.now()}`,
    }],
})

if (res.result === 200 && res.response) {
    const program = res.response  // StorageProgramData
    console.log(program.programName, program.data)
}
The response is StorageProgramData (or null) — a slightly richer cousin of StorageProgramResponse that adds createdByTx, lastModifiedByTx, and interactionTxs[].

2. getStorageProgramAll — full program (alias)

Despite the name, this is not a “list everything” endpoint. It takes a storageAddress and returns full StorageProgramData for that one program — kept as an alias of getStorageProgram for SDK back-compat. Same params, same response shape.
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgramAll",
        data: { storageAddress: "stor-7a8b9c...", requesterAddress: demos.getAddress() },
        muid: `storage-${Date.now()}`,
    }],
})

3. getStorageProgramsByOwner — list by owner (paginated)

Server-side pagination is pushed into SQL — limit and offset apply before deserialization, so very large owner sets remain cheap. ACL is filtered in the same SQL pass: callers who are not the owner see only programs they have read access to.
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgramsByOwner",
        data: {
            owner: "demos1abc...",
            requesterAddress: demos.getAddress(),  // optional
            limit: 50,    // default 100, clamped to [1, 200]
            offset: 0,    // default 0
        },
        muid: `storage-${Date.now()}`,
    }],
})

const programs = res.response as Array<{
    storageAddress: string
    programName: string
    encoding: "json" | "binary"
    sizeBytes: number
    storageLocation: string
    createdAt: string
    updatedAt: string
}>  // StorageProgramListItem[]
The response is always an array — never null. Items are StorageProgramListItem, the lighter shape (no data, no metadata, no ownerowner is implied because you queried for it).
The default limit is 100 today and is documented to drop in a future release. Pass an explicit limit if you depend on a particular page size.

4. getStorageProgramFields — top-level field names

JSON-only. Returns the keys of the top-level object.
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgramFields",
        data: { storageAddress: "stor-7a8b9c...", requesterAddress: demos.getAddress() },
        muid: `storage-${Date.now()}`,
    }],
})

const { fields, count } = res.response as { fields: string[]; count: number }
// { fields: ["displayName", "stats", "posts"], count: 3 }
This matches the StorageProgramFieldsResponse interface exported from @kynesyslabs/demosdk/storage. Binary-encoded programs return 400 / INVALID_FIELD_TYPE.

5. getStorageProgramFieldType — JSON type of a single field

JSON-only. Returns one of "string" | "number" | "boolean" | "array" | "object" | "null" | "undefined" (StorageFieldType).
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgramFieldType",
        data: {
            storageAddress: "stor-7a8b9c...",
            field: "posts",
            requesterAddress: demos.getAddress(),
        },
        muid: `storage-${Date.now()}`,
    }],
})

const { field, type } = res.response as { field: string; type: StorageFieldType }
// { field: "posts", type: "array" }
Field-not-found returns 404 / FIELD_NOT_FOUND.

6. getStorageProgramValue — single field value

JSON-only. Returns the field’s value plus its inferred JSON type.
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgramValue",
        data: {
            storageAddress: "stor-7a8b9c...",
            field: "displayName",
            requesterAddress: demos.getAddress(),
        },
        muid: `storage-${Date.now()}`,
    }],
})

const { field, value, type } = res.response as {
    field: string
    value: unknown
    type: StorageFieldType
}
This is the closest equivalent to “read a single key” — but be explicit about it. There is no demos.storagePrograms.read(address, key) overload.

7. getStorageProgramItem — array element by index

JSON-only. Supports negative indexing (-1 is the last item).
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "getStorageProgramItem",
        data: {
            storageAddress: "stor-7a8b9c...",
            field: "posts",
            index: -1,  // negative indexing supported
            requesterAddress: demos.getAddress(),
        },
        muid: `storage-${Date.now()}`,
    }],
})

const { field, index, value, arrayLength } = res.response as {
    field: string
    index: number      // resolved (non-negative) index actually used
    value: unknown
    arrayLength: number
}
Out-of-bounds returns 400 / INDEX_OUT_OF_BOUNDS. Field exists but is not an array returns 400 / INVALID_FIELD_TYPE.

8. hasStorageProgramField — existence check

JSON-only. For binary or non-object data the node returns exists: false rather than an error — it treats non-object data as having no fields.
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "hasStorageProgramField",
        data: {
            storageAddress: "stor-7a8b9c...",
            field: "apiKey",
            requesterAddress: demos.getAddress(),
        },
        muid: `storage-${Date.now()}`,
    }],
})

const { field, exists } = res.response as { field: string; exists: boolean }
Use this instead of getStorageProgramValue when you only need to know whether a field is present — it never throws on missing fields.

9. searchStoragePrograms — name search with ACL

Partial match by default. ACL is filtered at the SQL layer, so anonymous callers transparently see only public programs and paginated results are full pages (no post-fetch JS filter that would silently shorten them).
const res = await demos.rpcCall({
    method: "nodeCall",
    params: [{
        message: "searchStoragePrograms",
        data: {
            query: "config",
            options: {
                exactMatch: false,  // default false — partial match
                limit: 25,          // default 100, clamped to [1, 200]
                offset: 0,          // default 0
            },
            requesterAddress: demos.getAddress(),  // optional
        },
        muid: `storage-${Date.now()}`,
    }],
})

const programs = res.response as StorageProgramListItem[]
Response is always an array. Items are the StorageProgramListItem shape.

Higher-level convenience: StorageProgram static helpers

If you’d rather not assemble nodeCall envelopes yourself, the StorageProgram class exposes a parallel set of static async query methods that wrap the same RPC endpoints. They take an rpcUrl directly and return strongly-typed responses (or null on error). See API Reference for the full list — StorageProgram.getByAddress, getByOwner, searchByName, getFields, getValue, getItem, hasField, getFieldType, getAll.
import { StorageProgram } from "@kynesyslabs/demosdk/storage"

const programs = await StorageProgram.getByOwner(
    "https://node2.demos.sh",
    "demos1abc...",
    demos.getAddress(),  // identity for ACL
)
These helpers do not need a connected Demos instance — just an RPC URL — which makes them convenient for read-only/server-side code.

Performance & batching

Reads are RPC-only: no transaction, no fee, no consensus round-trip. Typical latency is sub-100 ms against a healthy node.

Parallelize independent reads

const [profile, settings, posts] = await Promise.all([
    demos.storagePrograms.read(profileAddress),
    demos.storagePrograms.read(settingsAddress),
    demos.storagePrograms.read(postsAddress),
])

Use updatedAt as a staleness signal

StorageProgramResponse.updatedAt is the canonical “did anything change?” field — it’s an ISO 8601 timestamp set on every successful write. Cache aggressively against it rather than polling the full document:
let cached: StorageProgramResponse | null = null

async function readIfChanged(address: string) {
    const head = await demos.storagePrograms.read(address)
    if (!head.success) return cached
    if (cached?.updatedAt === head.updatedAt) return cached
    cached = head
    return head
}
There is no metadata-only HEAD endpoint today, so the cheapest “did it change?” probe is still read(address) and a comparison against your cached updatedAt.

Field-level reads avoid full-document transfer

When you only need one key out of a 500 KB program, use getStorageProgramValue (or StorageProgram.getValue) instead of read — the node serializes only the requested field, not the whole document.

Error handling

SurfaceHow it shows up
Program not found / soft-deletedsuccess: false (or result.response === null over rpcCall)
ACL denialsuccess: false with an errorCode
Wrong encoding for a JSON-only endpoint400 / INVALID_FIELD_TYPE
Field missing404 / FIELD_NOT_FOUND
Array index out of bounds400 / INDEX_OUT_OF_BOUNDS
Bad params400 with error describing the missing/invalid field
Server error500 / INTERNAL_ERROR
The SDK helpers (storagePrograms.read, StorageProgram.getByAddress, etc.) never throw on these — they return a structured response (or null). For full payload details on individual error codes, see node docs.

See also

  • Getting Started — create / write / read flow end-to-end
  • Operations — write-side surface (granular updates, deletes, ACL changes)
  • Access Control — how owner / public / restricted modes interact with read ACL
  • API Reference — every static helper, payload, and response interface
  • Cookbook — end-to-end recipes