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 name:
@hyperyai/sdk(source: github.com/hyperyai/hypery-sdk)
npm install @hyperyai/sdkUI building blocks (modals & alerts)
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
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);
}import requests
r = requests.post(
"https://hypery.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"model": "anthropic/claude-3.5-sonnet",
"messages": [{"role": "user", "content": "Hello"}],
},
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok and isinstance(data, dict) and data.get("error"):
err = data["error"]
print(err.get("code"), err.get("message"))# Example: inspect error body when Hypery returns 402/429
curl -sS -D - 'https://hypery.ai/api/v1/chat/completions' \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic/claude-3.5-sonnet","messages":[{"role":"user","content":"Hello"}]}'Full Next.js App Router example
Wire provider, optional AuthModal, RestrictionModal after any gateway/API call that can return structured error.
Environment variables
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)
Output / behavior
- On
errorset: modal opens, fetches/api/user/me,/api/wallet/balance,/api/user/authorized-appswith the same bearer token to show live context (credits, app limits). - On
errornull: modal unmounts. AuthModal: opens OAuth redirect vialogin(); no separate REST “modal endpoint” — it is purely client UI.