Mutating Data
Learn how to create, update, delete, and extend entities on Arkiv using the TypeScript SDK’s wallet client.
WalletClient Setup
Section titled “WalletClient Setup”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}`),})Payload Helpers
Section titled “Payload Helpers”Convert data to Arkiv payloads before storing:
import { jsonToPayload, stringToPayload, payloadToString } from "@arkiv-network/sdk/utils"
// JSON dataconst jsonPayload = jsonToPayload({ title: "My Note", content: "Hello!" })
// Plain textconst textPayload = stringToPayload("Hello Arkiv!")
// Reading backconst text = payloadToString(entity.payload)const data = entity.toJson()Create Entity
Section titled “Create Entity”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)Parameters
Section titled “Parameters”| Field | Type | Description |
|---|---|---|
payload | Uint8Array | Entity data (use jsonToPayload or stringToPayload) |
contentType | string | MIME type (application/json, text/plain, etc.) |
attributes | Array | Key-value pairs for querying |
expiresIn | number | Lifetime in seconds (use ExpirationTime helpers) |
Validation Rules
Section titled “Validation Rules”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.5→1500, 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. expiresInmust be a positive integer and a multiple of 2 seconds. Arkiv measures expiration in whole blocks (1 block = 2 seconds).
// ❌ InvalidAttributeError — floats are not supportedattributes: [{ 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 timeexpiresIn: 7
// ✅ positive integer, multiple of 2expiresIn: ExpirationTime.fromHours(1)Returns
Section titled “Returns”| Field | Type | Description |
|---|---|---|
entityKey | string | Unique identifier for the entity |
txHash | string | Transaction hash on the chain |
Update Entity
Section titled “Update Entity”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),})Delete Entity
Section titled “Delete Entity”const { txHash } = await walletClient.deleteEntity({ entityKey: entityKey,})Change Ownership
Section titled “Change Ownership”Transfer an entity to a new owner:
const { entityKey, txHash } = await walletClient.changeOwnership({ entityKey: entityKey, newOwner: "0x1234567890abcdef1234567890abcdef12345678",})Parameters
Section titled “Parameters”| Field | Type | Description |
|---|---|---|
entityKey | Hex | Key of the entity to transfer |
newOwner | Hex | Address of the new owner |
Returns
Section titled “Returns”| Field | Type | Description |
|---|---|---|
entityKey | Hex | Key of the transferred entity |
txHash | string | Transaction hash on the chain |
Extend Expiration
Section titled “Extend Expiration”Add more time to an entity before it expires:
const { txHash } = await walletClient.extendEntity({ entityKey: entityKey, expiresIn: ExpirationTime.fromHours(1),})Batch Operations
Section titled “Batch Operations”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), }, ],})Mixing Operation Types
Section titled “Mixing Operation Types”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..." }],})Batch with Dynamic Data
Section titled “Batch with Dynamic Data”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), })),})Concurrent Writes and Nonces
Section titled “Concurrent Writes and Nonces”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 replacedawait 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 noncesawait Promise.all([ walletClient.createEntity({ ... }), walletClient.createEntity({ ... }),])See viem’s createNonceManager docs for customization.
ExpirationTime Reference
Section titled “ExpirationTime Reference”Always use the helper instead of raw numbers:
import { ExpirationTime } from "@arkiv-network/sdk/utils"
ExpirationTime.fromMinutes(30) // 1800 secondsExpirationTime.fromHours(1) // 3600 secondsExpirationTime.fromHours(12) // 43200 secondsExpirationTime.fromHours(24) // 86400 secondsExpirationTime.fromDays(7) // 604800 secondsBrowser Usage with MetaMask
Section titled “Browser Usage with MetaMask”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 connectionawait window.ethereum.request({ method: 'eth_requestAccounts' })
// Use MetaMask as transportconst walletClient = createWalletClient({ chain: braga, transport: custom(window.ethereum),})See the MetaMask Sketch App tutorial for a complete browser example.
Error Handling
Section titled “Error Handling”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 }}