@ixo/oracles-client-sdk

The React client SDK for QiForge oracles. useChat() hook, browser tools, and event rendering.

Overview

@ixo/oracles-client-sdk is the official React client for a QiForge oracle. It wraps:

  • The streaming chat endpoint over SSE.
  • The WebSocket event channel (tool calls, render components, browser tools, AG-UI actions).
  • UCAN auth — signing invocations (authentication) and delegations (downstream authorization).
  • Browser-side tools and AG-UI actions (so the agent can act on the user's tab / UI).

If you're building a Portal-like web UI for your oracle, use this. If you're building a non-React client (CLI, mobile app), implement the HTTP/WS protocol directly — the API reference is API endpoints.

Install

pnpm add @ixo/oracles-client-sdk

react >= 18 and zod ^3 are peer dependencies.

OraclesProvider (required wrapper)

Every hook calls useOraclesContext(), which throws useOraclesContext must be used within a OraclesProvider if there's no provider above it. Wrap your app once. The provider holds the wallet, signs transactions, and turns your createDelegation / createInvocation callbacks into the cached auth artifacts the hooks attach to requests.

import { OraclesProvider } from '@ixo/oracles-client-sdk';

function Root({ children }) {
  return (
    <OraclesProvider
      initialWallet={{
        address: 'ixo1...',
        did: 'did:ixo:ixo1...',
        matrix: { accessToken: 'syt_...', homeServer: 'https://matrix.ixo.world' },
      }}
      transactSignX={async (messages, memo) => {
        // sign + broadcast the chain tx with your wallet; return the result
        return undefined;
      }}
      createDelegation={async (oracleDid) => {
        // sign a user→oracle UCAN delegation (downstream authorization)
        return { serialized: '<base64-car>', expiresAt: Date.now() + 60 * 60 * 1000 };
      }}
      createInvocation={async (oracleDid) => {
        // sign a short-lived UCAN invocation (primary authentication)
        return { serialized: '<base64-car>', expiresAt: Date.now() + 15 * 60 * 1000 };
      }}
    >
      {children}
    </OraclesProvider>
  );
}
initialWallet{ address, did, matrix: { accessToken, homeServer } }required
transactSignX(messages, memo?) => Promise<unknown>required
createDelegation(oracleDid: string) => Promise<{ serialized: string; expiresAt: number }>required
createInvocation(oracleDid: string) => Promise<{ serialized: string; expiresAt: number }>

useChat

The primary hook. Provides the message thread, the send function, and live streaming state.

import { useChat, useOracleSessions, renderMessageContent } from '@ixo/oracles-client-sdk';

function Chat({ oracleDid }: { oracleDid: string }) {
  const { sessions, createSession } = useOracleSessions(oracleDid);
  const sessionId = sessions?.[0]?.sessionId ?? '';

  const { messages, sendMessage, isSending, status } = useChat({
    oracleDid,
    sessionId,
    onPaymentRequiredError: (claimIds) => {
      // the oracle needs payment before answering — surface a paywall
      console.log('Payment required:', claimIds);
    },
  });

  return (
    <div>
      <button onClick={() => createSession()}>New chat</button>
      {messages.map((m) => (
        <div key={m.id} className={m.type}>{renderMessageContent(m.content)}</div>
      ))}
      <form onSubmit={(e) => { e.preventDefault(); sendMessage(e.currentTarget.message.value); }}>
        <input name="message" />
        <button type="submit" disabled={isSending}>
          {status === 'streaming' ? 'Streaming…' : 'Send'}
        </button>
      </form>
    </div>
  );
}

Options (IChatOptions):

oracleDidstringrequired
sessionIdstringrequired
onPaymentRequiredError(claimIds: string[]) => voidrequired
browserToolsRecord<string, { toolName, description, schema, fn }>
uiComponentsUIComponents
overrides{ baseUrl?, wsUrl? }
streamingMode'batched' | 'immediate'

Returned values:

FieldTypeNotes
messagesIMessage[]Thread history, including in-flight streamed content.
sendMessage(message: string) => voidSubmit a user message.
abortStream() => Promise<void>Abort the in-flight response.
status'submitted' | 'streaming' | 'ready' | 'error'Current chat state.
isSendingbooleantrue while submitting or streaming.
isLoadingbooleanInitial history fetch in flight.
error / sendMessageErrorError | undefinedLoad error / send error.
refetchMessages() => Promise<…>Re-pull the thread from the server.
isRealTimeConnectedbooleanWebSocket connection status.
isConfigReadybooleanThe oracle's config has resolved.

There is no isStreaming or events field — use status / isSending for streaming state.

Browser tools

Browser tools let the agent call DOM/UI actions on the user's tab. Pass them as the browserTools option to useChat — a map keyed by tool name. The SDK advertises them to the agent, forwards each call to your fn, and returns the result over the WebSocket.

import { useChat } from '@ixo/oracles-client-sdk';
import { z } from 'zod';

const browserTools = {
  open_portal_route: {
    toolName: 'open_portal_route',
    description: 'Navigate to a route inside the Portal.',
    schema: z.object({ route: z.string() }),
    fn: async ({ route }: { route: string }) => {
      router.push(route);
      return { ok: true };
    },
  },
};

const { messages, sendMessage } = useChat({
  oracleDid,
  sessionId,
  onPaymentRequiredError: () => {},
  browserTools,
});

There is no registerBrowserTools export — browser tools are the browserTools option above.

AG-UI actions

useAgAction registers an agent-invokable action with an optional render. The provider tracks registered actions; useChat advertises them and renders their output inline.

import { useAgAction } from '@ixo/oracles-client-sdk';
import { z } from 'zod';

useAgAction({
  name: 'create_data_table',
  description: 'Render a data table from structured rows.',
  parameters: z.object({
    title: z.string().optional(),
    columns: z.array(z.any()),
    data: z.array(z.any()),
  }),
  handler: async ({ data }) => ({ ok: true, rowCount: data.length }),
  render: ({ status, args }) =>
    status === 'done' && args ? <DataTable {...args} /> : null,
});

Events

Streamed and WebSocket events are surfaced as the typed AnyEvent union and rendered through renderMessageContent / your uiComponents map. The event eventName values match the wire names:

eventNameWhat it carries
tool_callA tool invocation (and its result).
render_componentA structured UI component the agent wants displayed.
browser_tool_callA call to one of your browserTools.

The full server/client WebSocket event catalog is in API endpoints → WebSocket.

Other exports

ExportPurpose
useOracleSessions(oracleDid)Create, list, and delete chat sessions.
useOraclesConfig(oracleDid)The oracle's resolved public config (API URL, model, price).
useContractOracle()Pay to contract (subscribe to) an oracle.
useMemoryEngine(oracleDid)Invite the Memory Engine's Matrix bot into the user's oracle room (power level 50, so it can log its ixo.memory.* events there) and list or invite room members. It does not read or write memories; the oracle's memory plugin does that.
useGetOpenIdToken() / getOpenIdToken()Mint a Matrix OpenID token for the user.
renderMessageContent(content)Turn stored message content into React nodes.
OraclesProvider, useOraclesContextThe provider + its context accessor.
@ixo/oracles-client-sdk/live-agent → useLiveAgentEncrypted voice/video live-agent calls (separate entry point).

Auth lifecycle

The provider owns auth. From your createDelegation / createInvocation callbacks it:

  • Caches the signed delegation and invocation per (userDid, oracleDid).
  • Attaches Authorization: Bearer <invocation> + X-Auth-Type: ucan (primary) and x-ucan-delegation (downstream / fallback) to every request.
  • Passes the same artifacts on the WebSocket handshake (auth.invocation / auth.ucanDelegation).

When a stored delegation is missing or expired, the oracle nudges the user to re-authorize; your app calls createDelegation again to mint a fresh one (the client-sdk POSTs it to /delegation).