@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()Read/write the optional persistent memory engine.
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-agentuseLiveAgentEncrypted 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).