Overview
The decentralised.art SDK is a typed client for the chain API at https://api.decentralised.art/chain. It comes in two languages with the same
capabilities:
- JavaScript / TypeScript (package
decentralised-art), for browsers, Node.js and other modern runtimes. It also contains the World runtime and host. - Python (
decentralised-art, imported asdecentralised_art), for scripts, notebooks, services and agents.
With it you can:
- read connectors, transformations, conditions, formats, accounts and the event feed;
- create your own operations as drafts and simulate them for free;
- publish them on chain from your own wallet;
- execute published connectors and get verifiable results;
- build Worlds that interpret those results inside the platform.
Both clients are generated from the platform's OpenAPI specification, so request and response types match the API exactly. Agents can also use the MCP server, which offers the same operations as tools.
The SDK covers the chain API. Profiles, follows and World uploads belong to the separate services API, which uses Sign-In with Ethereum sessions; the SDK does not wrap it.
Installation
The SDK is distributed as GitHub release assets. JavaScript needs a runtime with fetch (modern browsers, Node.js 18 or later). Python needs version 3.9 or later.
npm install "https://github.com/decentralised-art/sdk/releases/latest/download/decentralised-art-js-sdk.tgz"
# Optional: wallet signing for login and publication
npm install etherspip install "decentralised-art @ https://github.com/decentralised-art/sdk/releases/latest/download/decentralised-art-python-sdk.tar.gz"For production, pin a release so installs are reproducible. Replace vX.Y.Z with a
version from the release list:
npm install "https://github.com/decentralised-art/sdk/releases/download/vX.Y.Z/decentralised-art-js-sdk.tgz"pip install "decentralised-art @ https://github.com/decentralised-art/sdk/releases/download/vX.Y.Z/decentralised-art-python-sdk.tar.gz"The Python package already depends on eth-account for signing. In JavaScript,
bring any wallet library; the examples use ethers v6.
Quick start
Reading and executing need no account. This reads the published pitch connector and
runs it on chain for eight steps:
import { DecentralisedArtClient } from "decentralised-art";
const sdk = new DecentralisedArtClient(); // https://api.decentralised.art/chain
// Read a published connector.
const connector = await sdk.connectorGet("pitch");
console.log(connector.dimensions, connector.format_hash);
// Run it on chain. No login and no gas.
const result = await sdk.execute("pitch", 8);
console.log(result.particles);
// [{ path: "/pitch:0", data: [0, 1, 2, 3, 4, 5, 6, 7] }]import decentralised_art
with decentralised_art.Client() as sdk: # https://api.decentralised.art/chain
# Read a published connector.
connector = sdk.connector_get("pitch")
print(connector.dimensions, connector.format_hash)
# Run it on chain. No login and no gas.
result = sdk.execute("pitch", 8)
print(result.particles[0].path, result.particles[0].data)
# /pitch:0 [0, 1, 2, 3, 4, 5, 6, 7]Every code sample on this page has a JavaScript and a Python version. The tab you choose is remembered across the page.
Core concepts
For the ideas behind the platform, see About. This is what they mean for the SDK.
Transformations
A transformation is a small Solidity function that derives the next value from the current
one. You write only its body. It receives x (a uint32) and args (a uint32[]) and returns a uint32. Its number of
arguments, args_count, is derived from the highest args[i] it uses.
Conditions
A condition is a Solidity function body that receives args (an int32[]) and returns a bool. It is checked every time the connector
it guards runs. When it returns false, the run stops with ConditionNotMet.
Connectors and dimensions
A connector is the runnable unit. It has one or more ordered dimensions.
- Each dimension has an ordered list of transformation calls (a name plus its arguments). Applied step by step, they generate that dimension's space of values.
- A dimension can point to another connector, its
composite. The dimension's values are then passed to that connector as indexes, so it reads its own space at exactly those points. This is how connected connectors narrow a space down. - A dimension without a composite is an open slot. A parent connector can fill open slots of
its composites with
bindings(slot id to connector name). - A connector can be guarded by one condition, with its own arguments.
Running instances
A running instance controls where generation starts on a branch: start_point is the first index and transformation_shift offsets which
transformation is applied first. Positions are numbered depth-first: 0 is the connector itself,
then each dimension in order, with a composite's dimensions numbered right after the dimension that
leads to it.
A connector can fix running instances when it is created (static_ri), and a run
can set others (dynamic_ri). A run cannot override a position the connector has
already fixed; that run fails.
Particles and paths
A run returns one stream of values per output path, for example { path: "/pitch:0", data: [0, 1, 2, …] }. The path names the connector and
dimension that produced it; nested connectors extend it, as in /phrase:0/pitch:0. What the numbers mean (a pitch, a time, a colour, a speed) is
up to the World that interprets them.
Formats
Every connector has a format_hash describing the shape of its output. Connectors with
the same format produce compatible streams, so a World that accepts one can accept the others.
Names, drafts and published operations
Names are global. They start with a letter or underscore, contain only letters, digits and
underscores, and are at most 128 characters long. A created operation cannot be changed or
overwritten: create a new name for a new version. A draft reports address "0x0"; a published operation reports its contract address.
From draft to the network
Operations move through four distinct steps. Each one is its own call.
| Step | Methods | Login | Gas | Where it runs |
|---|---|---|---|---|
| Create | transformationPost, conditionPost, connectorPost | Yes | No | Stored on the server as your draft |
| Simulate | simulate | No | No | The server's local EVM |
| Publish | publish, or publishPrepare + publishConfirm | Yes, as the owner | Yes, paid by the owner | Ethereum (Sepolia during testing) |
| Execute | execute | No | No | A read-only call to the chain at a pinned block |
Publishing is the only step that costs anything, and the cost is network gas paid from your own wallet. The server never holds your key.
Configuration
Both clients default to https://api.decentralised.art/chain. Set the DECENTRALISED_ART_API_BASE environment variable, or pass a base URL, to target another
deployment, such as a local chain backend.
import { DecentralisedArtClient } from "decentralised-art";
const sdk = new DecentralisedArtClient({
baseUrl: "https://api.decentralised.art/chain", // or DECENTRALISED_ART_API_BASE in Node
accessToken: savedToken ?? null, // reuse a token from an earlier login
fetch: globalThis.fetch, // custom runtimes, tests, instrumentation
});import decentralised_art
sdk = decentralised_art.Client(
base_url="https://api.decentralised.art/chain", # or DECENTRALISED_ART_API_BASE
access_token=None, # reuse a token from an earlier login
timeout=15.0, # seconds per request
verify_ssl=True,
)
...
sdk.close() # or use the client as a context manager: with Client() as sdk: ...Authentication
Reading, simulating and executing need no login. Creating and publishing need a bearer token, which you get by signing a one-time nonce with your Ethereum key:
- The client asks for a nonce for your address.
- You sign the message
Login nonce: <nonce>(EIP-191). - The server returns an access token, which the client stores and sends from then on.
import { Wallet } from "ethers";
// Server or script: a local key also becomes the default signer for publish().
const wallet = new Wallet(process.env.OWNER_KEY!);
await sdk.loginWithWallet(wallet);
// Browser: any ethers signer works for login.
// const signer = await new BrowserProvider(window.ethereum).getSigner();
// await sdk.loginWithWallet(signer);
console.log(sdk.accessToken);import os
from eth_account import Account
# The account also becomes the default signer for publish().
account = Account.from_key(os.environ["OWNER_KEY"])
sdk.login_with_account(account)
print(sdk.access_token)If you sign somewhere else (a hardware wallet, another service), submit the signature yourself:
const { nonce } = await sdk.getNonce(address);
const message = `Login nonce: ${nonce}`;
const signature = await signMessageSomehow(message); // EIP-191 personal_sign
await sdk.loginWithSignature(address, message, signature);nonce = sdk.get_nonce(address).nonce
message = f"Login nonce: {nonce}"
signature = sign_message_somehow(message) # EIP-191 personal_sign
sdk.login_with_signature(address, message, signature)Access tokens are short-lived, currently five minutes. When a protected
call returns 401, log in again. Your drafts stay on the server; they belong to
your address, not to the token.
The address you log in with owns everything you create, and only that address can publish it.
Reading the network
Every operation can be read by name. Lookups return both published operations and drafts;
check address to tell them apart. Transformation and condition lookups return their
argument count but never their Solidity source.
// Existence checks return false on 404 instead of throwing.
if (await sdk.connectorExists("pitch")) {
const connector = await sdk.connectorGet("pitch");
console.log(connector.owner, connector.address); // address is "0x0" for a local draft
}
const add = await sdk.transformationGet("add");
console.log(add.args_count); // 1
// Who has published what.
const { accounts } = await sdk.listAccounts({ limit: 50 });
const owned = await sdk.accountInfo(accounts[0]);
// Connectors that share an output shape.
const { formats } = await sdk.listFormats();
const format = await sdk.formatInfo(formats[0]);
console.log(format.connectors, format.scalars);# Existence checks return False on 404 instead of raising.
if sdk.connector_exists("pitch"):
connector = sdk.connector_get("pitch")
print(connector.owner, connector.address) # address is "0x0" for a local draft
add = sdk.transformation_get("add")
print(add.args_count) # 1
# Who has published what.
accounts = sdk.list_accounts(limit=50).accounts
owned = sdk.account_info(accounts[0])
# Connectors that share an output shape.
formats = sdk.list_formats().formats
fmt = sdk.format_info(formats[0])
print(fmt.connectors, fmt.scalars)Pagination
List methods use cursors. Pass the next_after value of one page as after for the next, while has_more is true. Page sizes default to 50.
let after: string | undefined;
do {
const page = await sdk.listFormats({ limit: 100, after });
handle(page.formats);
after = page.cursor.has_more ? (page.cursor.next_after ?? undefined) : undefined;
} while (after);after = None
while True:
page = sdk.list_formats(limit=100, after=after)
handle(page.formats)
if not page.cursor.has_more:
break
after = page.cursor.next_afterFeed and live updates
The feed lists chain events newest first: connector_added, transformation_added and condition_added. Each item carries a
compact payload (type, name, owner); read the full
definition through the matching lookup.
Items move through the statuses observed, safe and finalized as the chain confirms them, or become removed after a reorganisation.
By default the feed includes items that are not finalized yet; ask for finalized items only when
you need settled history.
// Newest first. includeUnfinalized: false returns finalized items only.
const page = await sdk.feed({ limit: 20, type: "connector_added", includeUnfinalized: false });
for (const item of page.items) {
console.log(item.status, item.payload.type, item.payload.name, item.payload.owner);
}
// Older items: pass the cursor back.
const older = await sdk.feed({ limit: 20, before: page.cursor.next_before ?? undefined });# Newest first. include_unfinalized=False returns finalized items only.
page = sdk.feed(limit=20, event_type="connector_added", include_unfinalized=False)
for item in page.items:
print(item.status, item.payload.name, item.payload.owner)
# Older items: pass the cursor back.
older = sdk.feed(limit=20, before=page.cursor.next_before)Live stream
feedStream opens a Server-Sent Events stream. It first replays events after since_seq (up to limit), sends a stream_meta event,
then delivers new events as they happen. Remember the last stream_seq you processed
and pass it back when you reconnect.
const response = await sdk.feedStream({ sinceSeq: 0 });
const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const frames = buffer.split("\n\n");
buffer = frames.pop() ?? "";
for (const frame of frames) {
const data = frame.split("\n").find((line) => line.startsWith("data: "));
if (data && !frame.includes("event: stream_meta")) {
const delta = JSON.parse(data.slice(6));
console.log(delta.stream_seq, delta.event_type, delta.payload.name);
}
}
}import json
with sdk.feed_stream(since_seq=0) as response:
event = None
for line in response.iter_lines():
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event != "stream_meta":
delta = json.loads(line[6:])
print(delta["stream_seq"], delta["event_type"], delta["payload"]["name"])
elif line == "":
event = NoneCreating operations
Creating an operation stores it on the server as your draft. Nothing is sent to the chain and
it costs nothing. Creation fails with 409 if the name is already taken on chain.
Transformations
// The body of: function run(uint32 x, uint32[] args) returns (uint32)
await sdk.transformationPost({
name: "shift_up",
sol_src: "return x + args[0];",
});
const created = await sdk.transformationGet("shift_up");
console.log(created.args_count, created.address); // 1 "0x0"# The body of: function run(uint32 x, uint32[] args) returns (uint32)
sdk.transformation_post({
"name": "shift_up",
"sol_src": "return x + args[0];",
})
created = sdk.transformation_get("shift_up")
print(created.args_count, created.address) # 1 0x0Conditions
// The body of: function check(int32[] args) view returns (bool)
await sdk.conditionPost({
name: "positive_only",
sol_src: "return args[0] > 0;",
});# The body of: function check(int32[] args) view returns (bool)
sdk.condition_post({
"name": "positive_only",
"sol_src": "return args[0] > 0;",
})Connectors
A connector refers to transformations and conditions by name. They can be your drafts or published operations from anyone.
await sdk.connectorPost({
name: "rising_line",
dimensions: [
{
// Applied in order on this dimension.
transformations: [
{ name: "add", args: [1] },
{ name: "shift_up", args: [2] },
],
},
],
// Optional gate, checked whenever the connector runs.
condition_name: "positive_only",
condition_args: [1],
});sdk.connector_post({
"name": "rising_line",
"dimensions": [
{
# Applied in order on this dimension.
"transformations": [
{"name": "add", "args": [1]},
{"name": "shift_up", "args": [2]},
],
},
],
# Optional gate, checked whenever the connector runs.
"condition_name": "positive_only",
"condition_args": [1],
})To build on another connector, point a dimension at it as a composite. A
connector can also fix running instances with static_ri:
await sdk.connectorPost({
name: "phrase",
dimensions: [
// This dimension builds on the published "pitch" connector.
{ composite: "pitch", transformations: [{ name: "add", args: [1] }] },
{ transformations: [{ name: "add", args: [2] }] },
],
// Fixed running instance for position 0 (the connector itself).
static_ri: { "0": { start_point: 60, transformation_shift: 0 } },
});sdk.connector_post({
"name": "phrase",
"dimensions": [
# This dimension builds on the published "pitch" connector.
{"composite": "pitch", "transformations": [{"name": "add", "args": [1]}]},
{"transformations": [{"name": "add", "args": [2]}]},
],
# Fixed running instance for position 0 (the connector itself).
"static_ri": {"0": {"start_point": 60, "transformation_shift": 0}},
})| Field | Description |
|---|---|
name | Required. Unique name for the connector. |
dimensions | Required, at least one. Each has transformations (ordered { name, args } calls), and optionally composite and bindings. |
condition_name | Optional. A condition checked on every run. Empty or omitted means none. |
condition_args | Optional. Integer arguments for the condition. |
static_ri | Optional. Fixed running instances keyed by position ("0", "1", …). |
Simulating
Simulation runs a connector in the server's local EVM, so you can try drafts before paying for
anything. It also uses published operations your drafts depend on. It takes the same arguments
as execute and returns the output streams.
// Runs your drafts, and any published operations they use, in the server's local EVM.
const streams = await sdk.simulate("rising_line", 16);
for (const { path, data } of streams) console.log(path, data);# Runs your drafts, and any published operations they use, in the server's local EVM.
streams = sdk.simulate("rising_line", 16)
for stream in streams:
print(stream.path, stream.data)Simulation results have no chain provenance: they show what a connector would produce, not what the network has recorded.
Publishing
Publishing records a draft on chain under your address. You pay the gas from your own wallet; the transaction never transfers any other value. The server rebuilds exactly the definition it stored when you created the draft, so the published operation matches what you simulated.
Publish with a local key
publish does everything in one call and needs no chain RPC endpoint: it prepares the
transaction, signs it locally with the wallet or account you logged in with, lets the server broadcast
it, and confirms it until it is mined. If the registry already holds exactly this operation, it
returns without signing anything.
import { Wallet } from "ethers";
const wallet = new Wallet(process.env.OWNER_KEY!); // no provider needed
await sdk.loginWithWallet(wallet);
// Dependencies first, one at a time, then the connector that uses them.
await sdk.publish("transformation", "shift_up", { maxFeePerGas: 50_000_000_000n });
await sdk.publish("condition", "positive_only", { maxFeePerGas: 50_000_000_000n });
const result = await sdk.publish("connector", "rising_line", {
maxFeePerGas: 50_000_000_000n, // refuse to sign above 50 gwei
chainId: 11155111, // refuse to sign for any other chain (Sepolia)
});
console.log(result.status); // "mined", or "published" if it already wasimport os
from eth_account import Account
account = Account.from_key(os.environ["OWNER_KEY"])
sdk.login_with_account(account)
# Dependencies first, one at a time, then the connector that uses them.
sdk.publish("transformation", "shift_up", max_fee_per_gas=50_000_000_000)
sdk.publish("condition", "positive_only", max_fee_per_gas=50_000_000_000)
result = sdk.publish(
"connector",
"rising_line",
max_fee_per_gas=50_000_000_000, # refuse to sign above 50 gwei
chain_id=11155111, # refuse to sign for any other chain (Sepolia)
)
print(type(result).__name__) # ConfirmResponse, or AlreadyPublished if it already wasPublish from a browser wallet
Browser wallets such as MetaMask send transactions themselves and cannot sign offline, so publish stops with an explanation before anything is signed or sent. Use the two-step
flow instead: prepare the transaction, let the wallet send it, then confirm it.
const prepared = await sdk.publishPrepare("transformation", "shift_up");
if (prepared.status === "prepared") {
// { from, to, data, chainId, gas } as hex quantities: the params of eth_sendTransaction.
const txHash = (await window.ethereum.request({
method: "eth_sendTransaction",
params: [prepared.transaction],
})) as string;
// Store txHash now. If the page reloads, confirm again with the same hash.
const request = { name: "shift_up", content_hash: prepared.content_hash, tx_hash: txHash };
let confirmed = await sdk.publishConfirm("transformation", request);
while (confirmed.status === "pending") {
await new Promise((resolve) => setTimeout(resolve, 3000));
confirmed = await sdk.publishConfirm("transformation", request);
}
console.log(confirmed.status); // "mined"
}
// prepared.status === "published": the registry already holds this exact entity.Step by step
The steps behind publish are available separately, for custom signers. With relay, the prepared answer also contains the account nonce and fees needed to
sign offline.
const prepared = await sdk.publishPrepare("transformation", "shift_up", { relay: true });
if (prepared.status === "prepared" && prepared.signing) {
// Sign { ...prepared.transaction, ...prepared.signing } offline (eth_signTransaction).
const raw_tx = await signOffline({ ...prepared.transaction, ...prepared.signing });
const { tx_hash } = await sdk.publishSend("transformation", {
name: "shift_up",
content_hash: prepared.content_hash,
raw_tx,
});
await sdk.publishConfirm("transformation", {
name: "shift_up",
content_hash: prepared.content_hash,
tx_hash,
});
}from decentralised_art.client import PreparedPublication
prepared = sdk.publish_prepare("transformation", "shift_up", relay=True)
if isinstance(prepared, PreparedPublication):
raw_tx = sign_offline(prepared.transaction, prepared.signing) # 0x02… signed transaction
sent = sdk.publish_send("transformation", "shift_up", prepared.content_hash, raw_tx)
sdk.publish_confirm("transformation", "shift_up", prepared.content_hash, sent.tx_hash)Rules to know
- Dependencies first. A connector can only be published once every
transformation, condition and connector it uses is published with the same definition.
Otherwise preparing fails with
409and lists them undermissingandmismatched. Dependencies are never published automatically. - One at a time per owner. Publications from one address share a nonce. Publish serially, and resolve a pending publication before preparing the next.
- Keep the transaction hash. If a confirmation is interrupted, confirm again with the same hash. Do not prepare and send a new transaction.
- Mined is not instantly executable. Execution reads the chain at a safe block, so a just-mined operation can take a short while to become available. Retry the execution later instead of publishing again.
- Fee and chain limits.
maxFeePerGasandchainIdmakepublishrefuse to sign anything more expensive, or for another chain. During testing, publications go to Sepolia (chain id11155111).
Executing on chain
execute runs a published connector through the on-chain runner. It is a read-only call:
no login, no transaction and no gas. The result is pinned to a block, so anyone can check it by
calling the same runner at the same block.
const result = await sdk.execute("pitch", 8, {
// Override the running instance at position 0 for this run only.
"0": { start_point: 12, transformation_shift: 0 },
});
console.log(result.block_number, result.block_hash, result.runner);
console.log(result.particles);
// [{ path: "/pitch:0", data: [12, 13, 14, 15, 16, 17, 18, 19] }]result = sdk.execute(
"pitch",
8,
# Override the running instance at position 0 for this run only.
{"0": {"start_point": 12, "transformation_shift": 0}},
)
print(result.block_number, result.block_hash, result.runner)
print(result.particles[0].path, result.particles[0].data)
# /pitch:0 [12, 13, 14, 15, 16, 17, 18, 19]| Field | Description |
|---|---|
particles | Output streams: { path, data } per path. |
block_number, block_hash | The block the call was pinned to. |
runner | Address of the runner contract that executed it. |
The step count must be between 1 and 65536. Running instances (up to 4096) are keyed by position, as described in Core concepts. Keep the block and runner together with the values whenever you store or share a result.
Errors
A response outside the 2xx range raises DecentralisedArtApiError, carrying the
HTTP status and the decoded response body. Network failures surface as the runtime's own
errors.
import { DecentralisedArtApiError } from "decentralised-art";
try {
await sdk.connectorGet("does_not_exist");
} catch (error) {
if (error instanceof DecentralisedArtApiError) {
console.log(error.status, error.body); // 404 { message: "..." }
} else {
throw error; // network failure, aborted request, ...
}
}from decentralised_art.client import DecentralisedArtApiError
try:
sdk.connector_get("does_not_exist")
except DecentralisedArtApiError as error:
print(error.status_code, error.body) # 404 {'message': '...'}| Status | Meaning |
|---|---|
400 | Invalid request, or the runner rejected the execution (for example a condition was not met). |
401 | Missing or expired access token. Log in again. |
403 | You do not own the operation, or publication is disabled on this server. |
404 | No operation with that name. |
409 | The name is taken on chain, dependencies are missing or different, or the account nonce is already in use. |
503 | The chain provider or artifact storage is temporarily unavailable. Retry later. |
A pending publication is not an error: publishConfirm returns it with status: "pending" (HTTP 202).
Building a World
A World is a web page that interprets connector output as sound, image or interaction. It is a static bundle (HTML, JavaScript, CSS and assets) that the platform shows in a sandboxed iframe, on its World page and inside Studio.
A World cannot reach the network on its own. It talks to the page that hosts it through the World runtime, and the host makes each call on its behalf, but only if the World's manifest grants the permission for it. The World never holds a token or a key.
Bundle layout
Upload a ZIP with world-manifest.json at its root, the HTML entry file it names, and
everything the page loads. Limits: 25 MB compressed, 100 MB extracted, 1000 files.
cd my-world
zip -r ../my-world.zip world-manifest.json index.html world.js style.css preview.pngEntry script
Import the runtime the platform serves instead of bundling it, so every World uses the same build. Subscribe to state, then announce that the World is ready:
// world.js — loaded by index.html as <script type="module" src="world.js">.
// Import the runtime the platform serves; Worlds never bundle their own copy.
// From /world-assets/{id}/world.js this resolves to /js/sdk/world-runtime.js.
import { createWorldSdk } from "../../js/sdk/world-runtime.js";
const sdk = createWorldSdk({ worldId: "simple-counter" });
sdk.onState(async (state) => {
const payload = state.payload ?? {};
const streams = payload.executeOutput ?? []; // [{ path, data }]
try {
draw(streams, payload.particlesCount);
// Brokered calls: the host checks this World's manifest permissions.
if (payload.connectorName) {
const connector = await sdk.connectorGet(payload.connectorName);
showFormat(connector.format_hash);
}
sdk.reportRendered(state.requestId);
} catch (error) {
sdk.reportError(error instanceof Error ? error.message : "Render failed");
}
});
// Tell the host the World is listening. Calls made before this are rejected.
sdk.ready();Upload the ZIP from Upload a World while signed in. The upload page validates the bundle before publishing it to the gallery.
World manifest
{
"schemaVersion": 1,
"slug": "simple-counter",
"name": "Simple Counter World",
"version": "0.1.0",
"entry": "index.html",
"runtime": "iframe",
"surfaces": ["world-page", "studio-plugin"],
"permissions": ["decentralised.art.connectors.read", "decentralised.art.execute"],
"description": "Renders connector metadata and numeric output streams.",
"shortDescription": "Minimal World example.",
"accentColor": "#34d399",
"preview": "preview.png",
"acceptedConnectorSets": [
{ "connectors": ["simple_counter"], "optionalConnectors": ["velocity_midi"] }
],
"valueLimits": {
"particlesCount": { "min": 1, "max": 64 },
"connectorValues": { "/simple_counter:0": { "min": 0, "max": 127 } }
}
}| Field | Description |
|---|---|
schemaVersion | Required. Currently 1. |
slug | Required. Lowercase letters, digits and dashes, up to 80 characters. |
name, version, description | Required. Shown in the gallery and on the World page. |
entry | Required. Path of the HTML entry file inside the ZIP. |
runtime | Required. Always "iframe". |
surfaces | Required, at least one: "world-page" (its own page) and/or "studio-plugin" (inside Studio). |
permissions | What the World may ask the host to do. See below. |
acceptedConnectorSets | Connector names the World understands, with optional extras. Declare these or acceptedFormatHashes; at least one is required. |
acceptedFormatHashes | Formats the World understands. |
valueLimits | Optional ranges for particlesCount and for values on specific output paths.
The host enforces the step count range on every call. |
preview | Gallery image inside the ZIP: PNG, JPEG or WebP, up to 8 MB. |
shortDescription, heroLabel, accentColor | Optional presentation details. |
Permissions
| Permission | Allows |
|---|---|
decentralised.art.connectors.read | connectorGet, connectorExists, listFormats, formatInfo |
decentralised.art.transformations.read | transformationGet, transformationExists |
decentralised.art.conditions.read | conditionGet, conditionExists |
decentralised.art.social.read | feed |
decentralised.art.execute | execute, simulate |
browser.audio | Playing audio. |
browser.downloads | Offering files for download. |
World runtime API
createWorldSdk(options) returns the object a World uses for everything. Create it once.
| Option | Description |
|---|---|
worldId | Required. The World's id, echoed in every message. |
requestTimeoutMs | Timeout per call. Default 15000. |
channelToken | Defaults to the worldChannel URL parameter the host adds. Rarely needed. |
targetOrigin, expectedOrigin | Restrict which origin messages go to and come from. Inferred by default. |
| Method | Description |
|---|---|
ready() | Tell the host the World is listening. Calls before this are rejected. |
onState(callback) | Receive state from the host. Returns an unsubscribe function. |
reportRendered(requestId?) | Tell the host a state has been rendered. |
reportError(message) | Report an unrecoverable error to the host. |
connectorGet, transformationGet, conditionGet,
and their …Exists versions | Read operations. Results are cached for the life of the World. |
listFormats, formatInfo, feed | Same as on the client; page size 1–100. |
execute, simulate | Run a connector. Up to 100 running instances per call. |
dispose() | Stop listening and reject calls still in flight. |
// Needs "decentralised.art.execute" in the manifest.
const result = await sdk.execute("pitch", 16, { "0": { start_point: 60, transformation_shift: 0 } });
const preview = await sdk.simulate("pitch", 16);
// Needs "decentralised.art.social.read".
const latest = await sdk.feed({ limit: 10 });State from the platform
When the platform shows a World, it pushes state whose payload describes the current
composition. The fields a World usually needs:
| Field | Description |
|---|---|
executeOutput | Output streams to interpret: [{ path, data }]. |
connectorName | The connector that produced them. |
particlesCount | Number of steps. |
executionMode | "execute" for on-chain results, "simulate" for simulation. |
executionProvenance | Block and runner of an on-chain result. |
surface | "world-page" or "studio-plugin". |
Treat any other fields as optional; more may be added over time.
Call errors
A rejected call throws WorldRpcCallError with a code: permission_denied (missing permission), bad_request (invalid
arguments, or a call before ready()), unknown_method, host_error (the API call failed; status holds its HTTP status) or timeout.
Hosting Worlds
To show Worlds in your own application, use the host side, createWorldHost from decentralised-art/worlds/host. It answers a
World's calls with your client, enforces the World's permissions and value limits, and pushes
state to it.
import { DecentralisedArtClient } from "decentralised-art";
import { createWorldHost } from "decentralised-art/worlds/host";
const iframe = document.querySelector<HTMLIFrameElement>("#world")!;
iframe.setAttribute("sandbox", "allow-scripts"); // opaque "null" origin
const host = createWorldHost({
client: new DecentralisedArtClient(),
iframe,
permissions: manifest.permissions,
valueLimits: manifest.valueLimits,
expectedOrigin: "null",
onReady: () =>
host.pushState({
payload: { connectorName: "pitch", particlesCount: 8, executeOutput: result.particles },
}),
onRendered: () => console.log("rendered"),
onError: ({ message }) => console.error(message),
});
// The per-mount channel token travels in the URL; set src only after creating the host.
iframe.src = host.worldUrl(worldEntryUrl);
// Later: host.dispose();- Load the World in an iframe with
sandbox="allow-scripts"and withoutallow-same-origin, so it cannot reach your page or its storage. - Each mount gets a random channel token, added to the iframe URL by
worldUrl(). Messages without it are ignored in both directions. - Pass the permissions and value limits from the World's validated manifest, never from the World itself.
Client methods
The JavaScript client is DecentralisedArtClient; the Python client is decentralised_art.Client. Responses use the API's field names (format_hash, block_number, …) in both languages. Python returns typed models with a to_dict() method.
| Method | Description |
|---|---|
JS version()PY version() | Chain API version and build timestamp. |
JS getNonce(address)PY get_nonce(address) | One-time login nonce for an address. |
JS loginWithWallet(wallet)PY login_with_account(account) | Sign the login nonce and store the access token. The wallet or account also becomes the default publication signer. |
JS loginWithSignature(address, message, signature)PY login_with_signature(address, message, signature) | Log in with a signature produced elsewhere. |
JS listAccounts({ limit, after })PY list_accounts(limit=, after=) | Accounts that own published operations. |
JS accountInfo(address, opts)PY account_info(address, ...) | Connectors, transformations and conditions owned by an address, each with its own cursor. |
JS connectorExists(name)PY connector_exists(name) | true, or false on 404. |
JS connectorGet(name)PY connector_get(name) | Connector definition, owner, address and format hash. |
JS connectorPost(request)PY connector_post(request) | Login Create a connector draft. |
JS transformationExists(name)PY transformation_exists(name) | true, or false on 404. |
JS transformationGet(name)PY transformation_get(name) | Name, args_count, owner and address. |
JS transformationPost(request)PY transformation_post(request) | Login Create a transformation draft. |
JS conditionExists(name)PY condition_exists(name) | true, or false on 404. |
JS conditionGet(name)PY condition_get(name) | Name, args_count, owner and address. |
JS conditionPost(request)PY condition_post(request) | Login Create a condition draft. |
JS simulate(name, count, dynamicRi?)PY simulate(name, count, dynamic_ri=) | Run a connector in the server's local EVM. Returns output streams. |
JS execute(name, count, dynamicRi?)PY execute(name, count, dynamic_ri=) | Run a published connector on chain at a pinned block. |
JS publish(kind, name, opts)PY publish(kind, name, account=, ...) | Login Prepare, sign offline, relay and confirm until mined. |
JS publishPrepare(kind, name, { relay })PY publish_prepare(kind, name, relay=) | Login Unsigned publication transaction, or the existing identical publication. |
JS publishSend(kind, request)PY publish_send(kind, name, content_hash, raw_tx) | Login Broadcast an offline-signed transaction through the server. |
JS publishConfirm(kind, request)PY publish_confirm(kind, name, content_hash, tx_hash) | Login Look the receipt up once: mined, or pending. |
JS listFormats({ limit, after })PY list_formats(limit=, after=) | Known format hashes. |
JS formatInfo(hash, opts)PY format_info(hash, ...) | Connectors and scalar labels for one format. |
JS feed(opts)PY feed(...) | Newest-first page of chain events. |
JS feedStream({ sinceSeq, limit })PY feed_stream(since_seq=, limit=) | Server-Sent Events stream of new events. |
JS accessTokenPY access_token | The current bearer token, if logged in. |
Python CLI
The Python package installs a small command, decentralised-art-auth, for quick
checks against an API. It prints JSON.
decentralised-art-auth version
decentralised-art-auth nonce 0xYourAddress
decentralised-art-auth --base-url http://localhost:54321 versionVersions and source
- Source, issues and releases: github.com/decentralised-art/sdk.
- The clients are generated from the OpenAPI contracts in api-spec, and a release is tied
to one version of them. The live API reports its own version through
version(). - Endpoint-level details are in the API reference, and the current state of the services is on API status.