@razinmohammedpt/hackai-sdk
v2.0.0
Published
Fast, fully-typed TypeScript SDK for Hack Club AI — chat completions, responses, image generation, embeddings, OCR, moderations, Exa web search, and the full Replicate model catalog.
Maintainers
Readme
@razinmohammedpt/hackai-sdk
A fast, fully-typed TypeScript/JavaScript SDK for Hack Club AI — a free AI proxy for teens, built by Hack Club.
Wraps the entire Hack Club AI API: chat completions, the Responses API, image generation, image/PDF inputs, embeddings, model listing, token stats, content moderation, OCR, Exa-powered web search, healthchecks, and every model in the Replicate directory (text-to-speech, speech-to-text, OCR, upscaling, image utilities, music generation, and more) with typed helper methods.
- Zero runtime dependencies for the core client — built on native
fetch, no bloat. - True streaming (async generators over SSE) for chat completions, the Responses API, and Exa answers — no buffering.
- One dependency (
replicate, the official client) powersclient.replicate.*, wired to Hack Club AI's Replicate proxy. - Dual ESM + CommonJS build with full
.d.tstypes. - Automatic real USD cost on every non-streaming chat/responses/embeddings call — see Cost tracking.
Install
npm install @razinmohammedpt/hackai-sdkQuick start
Get an API key from the Hack Club AI dashboard (teens 18 and under only — see Rules below).
import { HackAI } from "@razinmohammedpt/hackai-sdk";
const client = new HackAI({ apiKey: process.env.HACKCLUB_AI_API_KEY });
const completion = await client.chat.completions.create({
model: "qwen/qwen3-32b",
messages: [{ role: "user", content: "Tell me a joke." }],
});
console.log(completion.choices[0].message.content);If you omit apiKey, the client reads it from the HACKCLUB_AI_API_KEY environment variable.
Table of contents
- Chat Completions
- Responses API
- Image generation
- Image inputs (vision)
- PDF inputs
- Embeddings
- Models
- Token stats
- Cost tracking
- Moderations
- OCR
- Exa web search
- Healthcheck
- Replicate models
- Error handling
- Rules & limits
Chat Completions
const completion = await client.chat.completions.create({
model: "qwen/qwen3-32b",
messages: [{ role: "user", content: "Hello!" }],
temperature: 0.7,
max_tokens: 500,
});Streaming
const stream = await client.chat.completions.create({
model: "qwen/qwen3-32b",
messages: [{ role: "user", content: "Write a short story." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}Responses API
Simple string input:
const response = await client.responses.create({
model: "qwen/qwen3-32b",
input: "What is the meaning of life?",
max_output_tokens: 9000,
});
console.log(response.output[0].content[0].text);Multi-turn conversations (the Responses API is stateless — always send full history, and include id/status on assistant messages):
const response = await client.responses.create({
model: "qwen/qwen3-32b",
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "Capital of France?" }] },
{
type: "message",
role: "assistant",
id: "msg_abc123",
status: "completed",
content: [{ type: "output_text", text: "Paris.", annotations: [] }],
},
{ type: "message", role: "user", content: [{ type: "input_text", text: "And its population?" }] },
],
});Streaming works the same way — pass stream: true and for await the result.
Image generation
Uses google/gemini-2.5-flash-image-preview ("Nano Banana") or google/gemini-3-pro-image-preview, via chat completions:
const completion = await client.chat.completions.create({
model: "google/gemini-2.5-flash-image-preview",
messages: [{ role: "user", content: "Make a picture of a sunset over mountains" }],
modalities: ["image", "text"],
image_config: { aspect_ratio: "16:9" },
});
const dataUrl = completion.choices[0].message.images?.[0].image_url.url; // data:image/png;base64,...Image inputs (vision)
const completion = await client.chat.completions.create({
model: "google/gemini-2.5-flash",
messages: [
{
role: "user",
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } },
],
},
],
});Base64 also works: image_url: { url: "data:image/jpeg;base64,..." }.
PDF inputs
const completion = await client.chat.completions.create({
model: "qwen/qwen3-32b",
messages: [
{
role: "user",
content: [
{ type: "text", text: "Summarize this document" },
{ type: "file", file: { filename: "bitcoin.pdf", file_data: "https://bitcoin.org/bitcoin.pdf" } },
],
},
],
plugins: [{ id: "file-parser", pdf: { engine: "pdf-text" } }],
});Reuse message.annotations from the response in follow-up requests to skip re-parsing the same PDF.
Embeddings
const result = await client.embeddings.create({
model: "qwen/qwen3-embedding-8b",
input: "The quick brown fox jumps over the lazy dog",
});
console.log(result.data[0].embedding);
const availableModels = await client.embeddings.listModels();Models
const { data: models } = await client.models.list();Token stats
const stats = await client.stats.get();
// { totalRequests, totalTokens, totalPromptTokens, totalCompletionTokens }Cost tracking
Every non-streaming call to chat.completions.create(), responses.create(), and embeddings.create() returns a cost field with the real USD price of that specific generation — computed from the response's usage and the model's live per-token pricing (pulled from GET /proxy/v1/models / GET /proxy/v1/embeddings/models, the same OpenRouter-compatible pricing HCAI exposes).
const completion = await client.chat.completions.create({
model: "qwen/qwen3-32b",
messages: [{ role: "user", content: "Tell me a joke." }],
});
console.log(completion.usage);
// { prompt_tokens: 20, completion_tokens: 15, total_tokens: 35 }
console.log(completion.cost);
// {
// currency: "USD",
// promptCost: 0.0000016,
// completionCost: 0.0000042,
// totalCost: 0.0000058,
// pricePerPromptToken: 0.00000008,
// pricePerCompletionToken: 0.00000028,
// }The first call for a given model triggers one extra request to fetch the pricing table; it's cached on the client instance after that, so subsequent calls add no latency. If pricing can't be resolved (unknown model, network hiccup), cost is simply undefined — it never fails the underlying call.
You can also look up pricing or compute cost yourself:
import { computeCost } from "@razinmohammedpt/hackai-sdk";
const pricing = await client.models.getPricing("qwen/qwen3-32b");
// { prompt: "0.00000008", completion: "0.00000028" }
const cost = computeCost(pricing, { prompt_tokens: 20, completion_tokens: 15, total_tokens: 35 });This is useful for streaming responses, where cost isn't attached automatically since usage typically only arrives (if at all) in the final chunk — capture that usage yourself and pass it to computeCost().
Moderations
const result = await client.moderations.create({ input: "some text to classify" });
console.log(result.results[0].flagged, result.results[0].categories);OCR
const result = await client.ocr.create({
document: { type: "image_url", image_url: "https://example.com/receipt.png" },
table_format: "markdown",
});
console.log(result.pages[0].markdown);Supports image_url, document_url (PDFs), and previously-uploaded file documents; JSON-schema-structured document_annotation_format output; and page selection, header/footer extraction, and embedded-image extraction.
Exa web search
const results = await client.exa.search({ query: "Hack Club projects", numResults: 5 });
const similar = await client.exa.findSimilar({ url: "https://hackclub.com/", numResults: 5 });
const contents = await client.exa.contents({ urls: ["https://hackclub.com/"] });
const answer = await client.exa.answer({ query: "What is Hack Club?" });
// Streaming answers:
const stream = await client.exa.answer({ query: "What is Hack Club?", stream: true });
for await (const chunk of stream) console.log(chunk);Healthcheck
const health = await client.up();
// { status: "up" | "down", balanceRemaining, dailyKeyUsageRemaining, timestamp }up() resolves with the health body even when Hack Club AI reports itself as down — it never throws.
Replicate models
client.replicate wraps every model in the Hack Club AI Replicate directory using the official replicate client under the hood, pointed at HCAI's proxy. Each model has a typed helper grouped by category, plus a generic run() escape hatch for anything not yet wrapped (or for future models).
Every call returns { output, metrics, cost }:
// Typed helper:
const result = await client.replicate.tts.chatterboxPro({
voice: "William (Whispering)",
prompt: "Poppin' bottles in the ice, like a blizzard.",
});
console.log(result.output); // whatever the model returns — a URL, array of URLs, etc.
console.log(result.metrics); // { predictTimeSeconds, totalTimeSeconds } once the run completes
console.log(result.cost); // always undefined today — see note below
// Generic escape hatch — works for ANY Replicate model:
const generic = await client.replicate.run("owner/model-name", { some: "input" });On
cost: unlike chat/responses/embeddings, Replicate doesn't expose per-token pricing or a dollar figure anywhere in its API — even through Hack Club AI's proxy, a completed prediction has no cost/price/billing field, only wall-clockmetrics.costis kept as an explicit field (alwaysundefined) for shape consistency with the rest of the SDK, so a real value can populate here automatically in the future without another breaking change — it is not a placeholder for a number we simply haven't computed yet.Version pinning: HCAI's Replicate proxy only supports version-pinned prediction creation, not Replicate's unversioned
owner/nameshortcut. Passing a bare"owner/name"(as the typed helpers do) triggers one extramodels.get()call to resolve and cache the latest version id on first use; pass"owner/name:version"yourself to skip that resolution.
Text to Speech — client.replicate.tts
| Method | Model |
| --- | --- |
| speechTurbo | minimax/speech-02-turbo |
| speech28Turbo | minimax/speech-2.8-turbo |
| speech28Hd | minimax/speech-2.8-hd |
| chatterboxPro | resemble-ai/chatterbox-pro |
| dia | zsxkib/dia |
| xttsV2 | lucataco/xtts-v2 |
| qwen3Tts | qwen/qwen3-tts |
| realtimeTts15Mini | inworld/realtime-tts-1.5-mini |
| realtimeTts15Max | inworld/realtime-tts-1.5-max |
Speech to Text — client.replicate.stt
| Method | Model |
| --- | --- |
| incrediblyFastWhisper | vaibhavs10/incredibly-fast-whisper |
| parakeetRnnt11b | nvidia/parakeet-rnnt-1.1b |
OCR — client.replicate.ocr
| Method | Model |
| --- | --- |
| glm4v9b | cuuupid/glm-4v-9b |
| deepseekOcr | lucataco/deepseek-ocr |
| textExtractOcr | abiruyt/text-extract-ocr |
Image upscaling — client.replicate.upscale
| Method | Model |
| --- | --- |
| magicImageRefiner | fermatresearch/magic-image-refiner |
| recraftCrispUpscale | recraft-ai/recraft-crisp-upscale |
| upscaler | google/upscaler |
Image utilities — client.replicate.imageUtils
| Method | Model |
| --- | --- |
| removeBg | lucataco/remove-bg |
| backgroundRemover | 851-labs/background-remover |
| icLightBackground | zsxkib/ic-light-background |
| robustVideoMatting | arielreplicate/robust_video_matting |
| rembgVideo | lucataco/rembg-video |
| nsfwImageDetection | falcons-ai/nsfw_image_detection |
Music generation — client.replicate.music
| Method | Model |
| --- | --- |
| lyria2 | google/lyria-2 |
| musicgen | meta/musicgen |
| music15 | minimax/music-1.5 |
Specialized image models — client.replicate.image
| Method | Model |
| --- | --- |
| rdPlus | retro-diffusion/rd-plus |
Audio — client.replicate.audio
| Method | Model |
| --- | --- |
| samAudioLarge | geopti/sam-audio-large |
| voiceCloning | minimax/voice-cloning |
Every helper accepts the same input object you'd pass to Replicate's API for that model — check the model directory for each model's exact input/output schema.
Error handling
Every non-2xx response throws a HackAIError:
import { HackAI, HackAIError } from "@razinmohammedpt/hackai-sdk";
try {
await client.chat.completions.create({ model: "qwen/qwen3-32b", messages: [] });
} catch (err) {
if (err instanceof HackAIError) {
console.error(err.status, err.statusText, err.body);
}
}Rules & limits
Hack Club AI is a free service for teens 18 and under, run as a charity. Please respect their rules: no coding agents, no reselling, no proxying access to others, and follow the Code of Conduct. Rate limits: 450 requests / 30 min for chat completions & embeddings, 600 requests / 30 min for moderations.
License
MIT
