Skip to content

Learn how to create, update, delete, and extend entities on Arkiv using the TypeScript SDK’s wallet client.

All write operations require a wallet client:

import { createWalletClient } from "@arkiv-network/sdk"
import { braga } from "@arkiv-network/sdk/chains"
import { http } from "viem"
import { privateKeyToAccount } from "viem/accounts"
const walletClient = createWalletClient({
chain: braga,
transport: http(),
account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
})

Convert data to Arkiv payloads before storing:

import { jsonToPayload, stringToPayload, payloadToString } from "@arkiv-network/sdk/utils"
// JSON data
const jsonPayload = jsonToPayload({ title: "My Note", content: "Hello!" })
// Plain text
const textPayload = stringToPayload("Hello Arkiv!")
// Reading back
const text = payloadToString(entity.payload)
const data = entity.toJson()
import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk/utils"
const { entityKey, txHash } = await walletClient.createEntity({
payload: jsonToPayload({ title: "My Note", content: "Hello Arkiv!" }),
contentType: "application/json",
attributes: [
{ key: "type", value: "note" },
{ key: "id", value: crypto.randomUUID() },
{ key: "created", value: Date.now() },
],
expiresIn: ExpirationTime.fromHours(12),
})
console.log("Created:", entityKey)
FieldTypeDescription
payloadUint8ArrayEntity data (use jsonToPayload or stringToPayload)
contentTypestringMIME type (application/json, text/plain, etc.)
attributesArrayKey-value pairs for querying
expiresInnumberLifetime in seconds (use ExpirationTime helpers)

The SDK validates mutations before sending them and throws a descriptive error on invalid input:

  • Numeric attribute values must be integers. A non-integer number throws an InvalidAttributeError. To store a non-integer, scale it to an integer (e.g. 1.51500, dividing by the same factor on read) to keep numeric ordering and range queries working — or store it as a string ("1.5"), which only supports equality.
  • expiresIn must be a positive integer and a multiple of 2 seconds. Arkiv measures expiration in whole blocks (1 block = 2 seconds).
// ❌ InvalidAttributeError — floats are not supported
attributes: [{ key: "price", value: 19.99 }]
// ✅ scale to an integer (divide by 100 on read)
attributes: [{ key: "priceCents", value: 1999 }]
// ❌ InvalidExpirationError — not a multiple of the 2s block time
expiresIn: 7
// ✅ positive integer, multiple of 2
expiresIn: ExpirationTime.fromHours(1)
FieldTypeDescription
entityKeystringUnique identifier for the entity
txHashstringTransaction hash on the chain

Replace an entity’s payload, attributes, and expiration:

const { txHash } = await walletClient.updateEntity({
entityKey: entityKey,
payload: jsonToPayload({ title: "Updated Note", content: "New content" }),
contentType: "application/json",
attributes: [
{ key: "type", value: "note" },
{ key: "updated", value: Date.now() },
],
expiresIn: ExpirationTime.fromHours(24),
})
const { txHash } = await walletClient.deleteEntity({
entityKey: entityKey,
})

Transfer an entity to a new owner:

const { entityKey, txHash } = await walletClient.changeOwnership({
entityKey: entityKey,
newOwner: "0x1234567890abcdef1234567890abcdef12345678",
})
FieldTypeDescription
entityKeyHexKey of the entity to transfer
newOwnerHexAddress of the new owner
FieldTypeDescription
entityKeyHexKey of the transferred entity
txHashstringTransaction hash on the chain

Add more time to an entity before it expires:

const { txHash } = await walletClient.extendEntity({
entityKey: entityKey,
expiresIn: ExpirationTime.fromHours(1),
})

Perform multiple operations in a single transaction using mutateEntities(). It accepts creates, updates, deletes, extensions, and ownershipChanges:

await walletClient.mutateEntities({
creates: [
{
payload: jsonToPayload({ content: "Item 1" }),
contentType: "application/json",
attributes: [{ key: "type", value: "item" }],
expiresIn: ExpirationTime.fromMinutes(30),
},
{
payload: jsonToPayload({ content: "Item 2" }),
contentType: "application/json",
attributes: [{ key: "type", value: "item" }],
expiresIn: ExpirationTime.fromMinutes(30),
},
],
})
await walletClient.mutateEntities({
creates: [{
payload: jsonToPayload({ content: "New item" }),
contentType: "application/json",
attributes: [{ key: "type", value: "item" }],
expiresIn: ExpirationTime.fromHours(1),
}],
extensions: [{
entityKey: "0x456...",
expiresIn: ExpirationTime.fromHours(1),
}],
deletes: [{ entityKey: "0x321..." }],
})
const items = ["frontend", "backend", "devops"]
await walletClient.mutateEntities({
creates: items.map((skill) => ({
payload: jsonToPayload({ profileId: "alice-123", skill }),
contentType: "application/json",
attributes: [
{ key: "type", value: "skill" },
{ key: "profileId", value: "alice-123" },
{ key: "skill", value: skill },
],
expiresIn: ExpirationTime.fromDays(30),
})),
})

Every write is an on-chain transaction, and all transactions from one wallet must use strictly sequential nonces. The SDK does not manage nonces for you — each write fetches the next nonce from the network at send time. Two writes in flight at the same moment fetch the same nonce and collide: one transaction gets rejected or silently replaces the other.

// ❌ Both writes fetch the same nonce — one of them will fail or be replaced
await Promise.all([
walletClient.createEntity({ ... }),
walletClient.createEntity({ ... }),
])

There are two ways to write concurrently from one wallet:

Batch into a single transaction (preferred). mutateEntities() submits any number of operations as one transaction — one nonce, no collisions. See Batch Operations.

Use viem’s nonce manager. If separate transactions are unavoidable (e.g. independent writes triggered from different code paths), create the account with viem’s nonceManager so nonces are allocated locally and sequentially instead of fetched per write:

import { createWalletClient } from "@arkiv-network/sdk"
import { braga } from "@arkiv-network/sdk/chains"
import { http } from "viem"
import { privateKeyToAccount, nonceManager } from "viem/accounts"
const walletClient = createWalletClient({
chain: braga,
transport: http(),
account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`, {
nonceManager,
}),
})
// ✅ Safe — the nonce manager hands out sequential nonces
await Promise.all([
walletClient.createEntity({ ... }),
walletClient.createEntity({ ... }),
])

See viem’s createNonceManager docs for customization.

Always use the helper instead of raw numbers:

import { ExpirationTime } from "@arkiv-network/sdk/utils"
ExpirationTime.fromMinutes(30) // 1800 seconds
ExpirationTime.fromHours(1) // 3600 seconds
ExpirationTime.fromHours(12) // 43200 seconds
ExpirationTime.fromHours(24) // 86400 seconds
ExpirationTime.fromDays(7) // 604800 seconds

In browser applications, use MetaMask as the transport instead of a private key:

import { createWalletClient } from "@arkiv-network/sdk"
import { braga } from "@arkiv-network/sdk/chains"
import { custom } from "viem"
// Request wallet connection
await window.ethereum.request({ method: 'eth_requestAccounts' })
// Use MetaMask as transport
const walletClient = createWalletClient({
chain: braga,
transport: custom(window.ethereum),
})

See the MetaMask Sketch App tutorial for a complete browser example.

The SDK does not retry on failure — all methods throw on error. Wrap write operations in try/catch:

try {
const { entityKey, txHash } = await walletClient.createEntity({
payload: jsonToPayload({ title: "My Post" }),
contentType: "application/json",
attributes: [{ key: "type", value: "post" }],
expiresIn: ExpirationTime.fromHours(12),
})
} catch (error) {
// Common failures:
// - Invalid input (InvalidAttributeError, InvalidExpirationError)
// - User rejected the transaction (MetaMask popup dismissed)
// - Insufficient funds / gas
// - Network error (RPC unreachable)
// - Entity already expired (for update/extend)
console.error("Transaction failed:", error)
}

The SDK’s typed errors are exported from the package root, so you can handle them specifically:

import { InvalidAttributeError, InvalidExpirationError } from "@arkiv-network/sdk"
try {
await walletClient.createEntity({ ... })
} catch (error) {
if (error instanceof InvalidAttributeError) {
// a numeric attribute value was not an integer
} else if (error instanceof InvalidExpirationError) {
// expiresIn was not a positive multiple of the 2s block time
} else {
throw error
}
}