React client — auth & modals

Full Hypery example using @hyperyai/sdk — OAuth, AuthModal, RestrictionModal, and parsing API errors that become modals.

Use the @hyperyai/sdk React package to handle OAuth against Hypery and to show modals when the API returns structured errors (spending limits, insufficient credits, and other restriction payloads). The same Authorization: Bearer access token is used for AI calls (/api/v1/*) and for gateway data the modals load (/api/user/*, /api/wallet/*).

Package

npm install @hyperyai/sdk

UI building blocks (modals & alerts)

ExportRole
HyperyProviderOAuth config, token storage, getAccessToken, login / logout
AuthModalControlled dialog that starts OAuth (login())
RestrictionModalFull-screen style modal for error objects from failed API calls; loads live wallet / authorized-app data from Hypery
SpendingLimitAlertInline spending-limit messaging
InsufficientCreditsAlertInline insufficient-credits messaging
parseError, isSpendingLimitError, isInsufficientCreditsErrorHelpers to interpret JSON error bodies

RestrictionModal expects appId = your OAuth app’s client_id (same value as NEXT_PUBLIC_OAUTH_CLIENT_ID), because it matches your app in /api/user/authorized-apps. Use the same gatewayUrl you use for API calls.


Endpoint (AI call that may open a modal)

When a chat request hits a billing or limit guard, Hypery returns a non-2xx response with a JSON body shaped like { "error": { ... } }. Your client should parse that object and pass it to RestrictionModal.

Method / URL: POST /api/v1/chat/completions
Full URL (production): https://hypery.ai/api/v1/chat/completions

Headers

NameValue
AuthorizationBearer <access_token>
Content-Typeapplication/json

Request body (minimal) — same as the Chat API; on failure, use the error shape below.


Example JSON error bodies (modal source)

Typical error payloads (inside the response JSON) — RestrictionModal reads code, message, and the extra fields:

Spending limit (HTTP 429)

{
  "error": {
    "code": "SPENDING_LIMIT_EXCEEDED",
    "message": "Daily spending limit exceeded",
    "type": "spending_limit_error",
    "limitType": "daily",
    "limit": 10,
    "current": 10,
    "requested": 0.5,
    "resetsAt": "2026-04-26T00:00:00.000Z"
  }
}

Insufficient credits (HTTP 402)

{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Not enough credits",
    "type": "insufficient_credits_error",
    "available": 0.5,
    "required": 2
  }
}

Parsing errors in code

After a failed fetch, read JSON and, if error is present, pass it to RestrictionModal (or use parseError for branching).

const response = await fetch('https://hypery.ai/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'anthropic/claude-3.5-sonnet',
    messages: [{ role: 'user', content: 'Hello' }],
  }),
});
 
const body = await response.json().catch(() => ({}));
 
if (!response.ok && body.error) {
  // Drive <RestrictionModal error={body.error} ... />
  console.log(body.error.code, body.error.message);
}

Full Next.js App Router example

Wire provider, optional AuthModal, RestrictionModal after any gateway/API call that can return structured error.

Environment variables

VariablePurpose
NEXT_PUBLIC_OAUTH_CLIENT_IDOAuth client id (also RestrictionModal appId)
NEXT_PUBLIC_REDIRECT_URIOAuth redirect, e.g. https://yourapp.com/callback
NEXT_PUBLIC_AI_GATEWAY_URLGateway base URL, e.g. https://hypery.ai

Use the same base URL for HyperyProvider.config.gatewayUrl and for RestrictionModal’s gatewayUrl. Include scopes Hypery needs for user, wallet, and authorized apps (e.g. read, write, ai:chat, billing:read).

app/layout.tsx

import { HyperyProvider } from '@hyperyai/sdk';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <HyperyProvider
          config={{
            clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!,
            redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI!,
            gatewayUrl: process.env.NEXT_PUBLIC_AI_GATEWAY_URL!,
            scopes: ['read', 'write', 'ai:chat', 'ai:completions', 'ai:models', 'billing:read'],
          }}
        >
          {children}
        </HyperyProvider>
      </body>
    </html>
  );
}

app/callback/page.tsx (finish OAuth)

'use client';
 
import { useHyperyAuth } from '@hyperyai/sdk';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
 
export default function CallbackPage() {
  const { isAuthenticated, isLoading } = useHyperyAuth();
  const router = useRouter();
 
  useEffect(() => {
    if (!isLoading && isAuthenticated) {
      router.replace('/');
    }
  }, [isAuthenticated, isLoading, router]);
 
  return <p>Signing you in…</p>;
}

app/page.tsx — auth modal + chat + restriction modal

'use client';
 
import { useState } from 'react';
import {
  useHyperyAuth,
  AuthModal,
  RestrictionModal,
  type RestrictionError,
} from '@hyperyai/sdk';
 
export default function Page() {
  const { getAccessToken, isAuthenticated, isLoading } = useHyperyAuth();
  const [authOpen, setAuthOpen] = useState(false);
  const [restrictionError, setRestrictionError] = useState<RestrictionError | null>(null);
 
  const gatewayUrl = process.env.NEXT_PUBLIC_AI_GATEWAY_URL!;
  const clientId = process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!;
 
  async function sendChat() {
    const token = await getAccessToken();
    if (!token) {
      setAuthOpen(true);
      return;
    }
 
    const res = await fetch(`${gatewayUrl}/api/v1/chat/completions`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'anthropic/claude-3.5-sonnet',
        messages: [{ role: 'user', content: 'Hello from my app' }],
      }),
    });
 
    const data = await res.json().catch(() => ({}));
 
    if (!res.ok && data?.error) {
      setRestrictionError(data.error as RestrictionError);
      return;
    }
 
    if (!res.ok) {
      throw new Error(data?.message || 'Request failed');
    }
 
    console.log(data.choices?.[0]?.message?.content);
  }
 
  if (isLoading) return <p>Loading…</p>;
 
  return (
    <>
      {!isAuthenticated && (
        <button type="button" onClick={() => setAuthOpen(true)}>
          Sign in
        </button>
      )}
 
      {isAuthenticated && (
        <button type="button" onClick={sendChat}>
          Send test chat
        </button>
      )}
 
      <AuthModal
        isOpen={authOpen}
        onClose={() => setAuthOpen(false)}
        onSuccess={() => setAuthOpen(false)}
      />
 
      <RestrictionModal
        error={restrictionError}
        appId={clientId}
        gatewayUrl={gatewayUrl}
        getAccessToken={getAccessToken}
        onClose={() => setRestrictionError(null)}
        onRetry={() => setRestrictionError(null)}
      />
    </>
  );
}

Input summary (RestrictionModal)

PropTypeDescription
errorRestrictionError | nullStructured error from gateway JSON, or null to hide
appIdstringOAuth client_id for this app
gatewayUrlstringGateway base URL (no trailing path)
getAccessToken() => Promise<string | null>From useHyperyAuth()
onClose() => voidClose / dismiss
onRetry() => void (optional)e.g. clear state so user can retry

Output / behavior

  • On error set: modal opens, fetches /api/user/me, /api/wallet/balance, /api/user/authorized-apps with the same bearer token to show live context (credits, app limits).
  • On error null: modal unmounts.
  • AuthModal: opens OAuth redirect via login(); no separate REST “modal endpoint” — it is purely client UI.

Next steps