ixo-chain

Read-only IXO chain data for the model: live balances, and claims, collections, entities, DID documents, 1155 tokens and transactions from IXO Blocksync.

Source: packages/oracle-runtime-workers/src/plugins/ixo-chain/

AttributeValue
Feature keyixo-chain
RuntimeCloudflare Workers runtime only (@ixo/oracle-runtime-workers 0.28.0 and later)
Visibilityon-demand
Stabilitybeta
Categorydata
Default stateOn unless IXO_CHAIN_TOOLS=off
Depends on—
Requires (UCAN)— (everything it reads is public)

Summary

The ixo-chain plugin gives the model two read-only tools over public IXO chain data, so it can answer questions such as "what is my IXO balance?", "did I get paid for my claims in collection 8250?" or "what is did:ixo:entity:…?". It is part of BUNDLED_WORKERS_PLUGINS, so an oracle that spreads that list into the plugins of createOracleWorker loads it unless IXO_CHAIN_TOOLS=off. The Node.js runtime (@ixo/oracle-runtime) does not ship it.

ToolReadsFreshness
get_account_balancesChain REST (LCD) gateway: bank balances and denom metadataLive chain state
chain_lookupIXO Blocksync GraphQL (BLOCKSYNC_GRAPHQL_URL)The index, seconds behind the chain

Every account field defaults to the user. The user's IXO account address is the suffix of their did:ixo:ixo1… DID (see What the model is told about the user). A user with a did:key: or did:web: DID has no address, and a request that needs it answers invalid_input ("the user has no IXO account address; give one"). An account field also takes any ixo1… address or did:ixo:ixo1… DID.

Load it on every turn

The plugin is on-demand: its tools exist on a turn only after the model loads it with load_capability, or after the runtime's capability router preloads it for that turn. To bind the tools on every turn instead, override its visibility tier with manifestOverrides:

// src/index.ts
import {
  createOracleWorker,
  BUNDLED_WORKERS_PLUGINS,
} from '@ixo/oracle-runtime-workers';

export default createOracleWorker({
  config,
  plugins: [...BUNDLED_WORKERS_PLUGINS],
  manifestOverrides: {
    'ixo-chain': { visibility: 'always' },
  },
});

The override is shallow-merged onto the plugin's manifest at boot and validated like an authored manifest; see Retune a bundled plugin's manifest. The rules the model needs (live versus indexed data, units, read-only, untrusted text) are in the plugin's operating guide and in both tool descriptions, so a preload that binds the tools without the guide still carries them.

Environment variables

VarRequiredDescription
IXO_CHAIN_TOOLSnoon (default) or off. off leaves the plugin out of the oracle.
CHAIN_REST_URLnoChain REST (LCD) base URL for balances and denom metadata. https, no credentials, query or fragment (a /rest path is fine). Defaults per NETWORK, below.
BLOCKSYNC_GRAPHQL_URL—Read but not owned (core). The IXO Blocksync endpoint chain_lookup queries. A value that is not an http or https URL makes chain_lookup answer unavailable without sending a request.
NETWORK—Read but not owned (core). Selects the default REST gateway and is named in every result.
NetworkChain REST defaultBlocksync GraphQL (BLOCKSYNC_GRAPHQL_URL)
mainnethttps://impacthub.ixo.world/resthttps://blocksync-graphql.ixo.earth/graphql
testnethttps://testnet.ixo.earth/resthttps://testnet-blocksync-graphql.ixo.earth/graphql
devnethttps://devnet.ixo.earth/resthttps://devnet-blocksync-graphql.ixo.earth/graphql

What it contributes

  • Tools: get_account_balances, chain_lookup (both effect: 'read').
  • Sub-agents: none.
  • Middleware: none.
  • HTTP routes: none.
  • Shared state: none.

get_account_balances

Input: { address? }, an ixo1… address or did:ixo:ixo1… DID; default the user's account. The tool reads one page of up to 50 bank balances from the chain REST gateway and shows each denom through the chain's own bank metadata:

  • uixo is shown as IXO with 6 decimals, verified, and is never looked up.
  • Any other denom is looked up in the bank metadata. It is verified when the metadata lists its display unit in denom_units with an exponent of 0 to 18; that exponent becomes its decimals, and its symbol is the metadata symbol or, when that is empty, the display unit upper-cased. So 5,740,000 upay reads as 5.74 PAY.
  • A denom with no metadata, or whose display unit is not listed, is unverified: 0 decimals, in base units, named by its full denom.
  • A token whose metadata could not be read (a failed read, or no time left in the tool's budget) is shown unverified with metadataUnavailable: true: it may still be verified.

Answer:

{ ok, network, source: 'chain', fetchedAt, address, subject,
  tokens: [{ denom, symbol, decimals, verified, amountBaseUnits, amount, metadataUnavailable? }],
  moreDenoms?, truncated? }

amount is a decimal string in display units (9000000 uixo is "9"). Tokens are sorted uixo first, then verified tokens, then by amount. moreDenoms: true means the account holds more than 50 denoms. An empty list is an answer, not an error. This is the only tool whose amount is converted.

chain_lookup

Input: { requests: [...] }, 1 to 8 requests, each with a kind. All of them go to IXO Blocksync as one GraphQL request, and each answers on its own. Batch related questions into one call.

kindWhat it answersKey inputs
claimsClaims submitted by an account, newest first, with each claim's evaluation and payment statusagent? (default: the user), collectionId?, first?, after? (the previous page's endCursor)
claimOne claim and its current evaluationclaimId (a CID, for example bafkrei…)
claim_collectionOne claim collection: counts, state and payment termsid (for example "8250")
claim_collectionsCollections by admin or by entity, newest start date firstentity?, admin? (default: the user), first?
entitiesEntities owned by an accountowner? (default: the user), type? (for example "dao"), first?
entityOne entityid (did:ixo:entity: followed by 32 hex characters)
did_documentOne DID documentid? (any did:ixo:… DID; default: the user's DID)
token_balances1155 tokens an account holdsaddress? (default: the user), first?
transactionsTransactions an account signed, or messages it receivedaddress? (default: the user), direction? (signed, the default, or received), first?
transactionOne transaction, with its signers, fee and up to 50 messageshash (64 hex characters)

The input schema is strict. An unknown key in one request (agentDid instead of agent), an unknown kind, a first outside 1 to 20, or more than 8 requests rejects the whole call with a tool error that names the mismatch. A request whose id fails its pattern (for example a malformed hash) answers invalid_input on its own; the others still run.

Answer:

{ ok: true, network, source: 'blocksync', fetchedAt,
  results: [ { kind, ok: true, subject?, first?, data, truncated? }
           | { kind, ok: false, reason, detail, retryable } ] }

Only a failed request to IXO Blocksync as a whole (after its retry) makes the whole answer ok: false. What data holds:

  • Claim and evaluation status as words (pending, approved, rejected, disputed, invalidated, flagged); collection state open, paused or closed. claims carries hasNextPage and endCursor for after.
  • Transactions as { hash, height, time, code, succeeded, memo?, messages, messageCount? }. Message values are never returned raw: MsgSend is { from, to, amount }, MsgTransferToken is { from, to, tokens }, MsgSubmitClaim and MsgEvaluateClaim are { claimId, collectionId, status? }, an authz MsgExec is its first 5 inner summaries, and anything else is a 240-character JSON excerpt.
  • An entity's status stays the chain's number, with a statusNote: the chain gives entity status no fixed meaning. entities adds orderedBy: 'id (text)', because an entity id is a hash and its order says nothing about age.

Reading the results

  • network is in every answer. The model is told to say which network an answer is from.
  • subject is 'user' or 'other': whether the data belongs to the user. A read by account knows it before the read. A read by id learns it from the answer: a claim is the user's when its agentAddress is the user's address, a collection when its admin is, an entity when its owner is, a transaction when one of its signers is, a DID document when its id is the user's DID. claim_collections by entity alone carries no subject.
  • amount versus amountBaseUnits. Only get_account_balances converts (amount, display units). Inside chain_lookup, every bank coin (a claim's or evaluation's amount, a MsgSend amount, a transaction fee, a collection payment) and every cw20 payment amount is { denom, amountBaseUnits }: uixo ÷ 1,000,000 = IXO.
  • 1155 tokens are whole units. The IXO token module stores no decimals, so token_balances rows, MsgTransferToken amounts and cw1155 payments keep amount in whole units. token_balances rows are { tokenId, name, class, collection, amount, retired, contractAddress }.
  • Failures are results, each with one plain-sentence detail the model can relay:
reasonWhenretryable
rate_limitedHTTP 429 twice, or the upstream is marked unhealthy after a 429true
timeoutAn attempt ran out of timetrue
index_timeoutIXO Blocksync's statement timeout for that request ("ask for fewer rows or a narrower filter")false
unavailable5xx, 404, network error, redirect, unreadable body, a field error, a request the answer does not carry, no valid BLOCKSYNC_GRAPHQL_URLtrue for 502 to 504, network, transient and missing-request errors
not_foundA by-id read answered null with no errorfalse
too_largeThe body was over the read limit, or a request was left out to keep the answer smallfalse (ask for it on its own)
invalid_inputA field that fails its pattern, or no IXO account address for the userfalse

Limits

LimitValue
Requests per chain_lookup call1 to 8, sent as one GraphQL request
Rows per list (first)1 to 20, default 10. With n > 1 requests in a call, each list reads at most max(5, floor(40 / n)) (2 requests: 20, 4: 10, 8: 5); every list result reports the first it used.
Bank balancesOne page of 50 denoms (moreDenoms: true beyond that)
Nested listsA transaction in a list: its first 5 messages, with messageCount for the total. One transaction: up to 10 signers and 50 messages. Collection payment lists: 10. Entity and DID document lists: 20.
StringsClipped: ids, addresses, hashes and dates 120 characters; names and labels 200; memos 256; excerpts 240
Answer sizeAt most 12,000 JSON characters. Each result gets a share; a result over its share has its largest list halved and is marked truncated: true; if the answer is still too big, the largest results are replaced by too_large.
Time20 s budget per tool call; each attempt at most 8 s (chain REST) or 11 s (IXO Blocksync, above its 8 s statement timeout)
RetriesOne retry on HTTP 429, 502, 503 or 504, a network error or a transient GraphQL error, after the upstream's Retry-After (capped at 5 s) or a random 1 to 3 s, and only when at least 1 s of budget would be left. Never retried: other 4xx, 501, a timeout, an unreadable or oversized body, a statement timeout.
Unhealthy upstreamA 429 or 503 marks that upstream for 10 s per isolate; other calls answer rate_limited or unavailable (retryable) at once instead of asking it
Body readAt most 512 KiB (chain REST) or 1 MiB (IXO Blocksync); no redirects are followed

Read-only and security

  • Read-only. Neither tool signs or sends anything. For a transfer, the model is told to use the turn's transfer tools instead.
  • Public data only. The manifest declares no requires: nothing it reads is guarded by a User-controlled Authorization Network (UCAN) capability, and anyone can read the same data from the chain.
  • Chain text is data, never instructions. Memos, names, descriptions and excerpts are written by anyone on a public chain. The operating guide and the chain_lookup description tell the model to treat them as data.
  • Inputs are checked before they reach a query. Every id is matched against its kind's pattern and written into the GraphQL document as a literal.
  • Logs carry no user data. Each failed attempt logs one [ixo-chain] <read> attempt N failed: <status or error class> warning, never a request body or an address.

Known limits

  • Large histories can time out. IXO Blocksync has no composite index for reading claims, signed transactions or received messages of one account in order: each read filters on the account and sorts what it finds, so an address with a very large history can hit the 8 s statement timeout (index_timeout).
  • Received transactions come from each message's recipient field. A MsgMultiSend output, and a send that is not the first message inside an authz MsgExec, are not seen.
  • IBC tokens can show unverified. The IBC transfer module registers metadata whose display unit is the trace path with only the base unit listed, so on mainnet (metadata read October 29, 2026) IBC USDC shows unverified, in base units.
  • On-demand by default. The model must load the plugin, or the capability router preload it, before the tools exist on a turn. Use the override above to have them on every turn.

When to use it

  • The user asks about their IXO balance or tokens.
  • The user asks about their claims, a claim collection, or an entity or DID.
  • The user asks what happened on chain (sent or received transactions).
  • The user asks what a did:ixo:… identifier is.

When NOT to use it

  • Preparing or sending a transfer: these tools only read.
  • Anything that is not public chain data.