--- url: /guide/authentication.md --- # Authentication All API requests require authentication using an API key. ## Using Your API Key Include your API key in the `Authorization` header as a Bearer token: ``` Authorization: Bearer YOUR_API_KEY ``` ## Managing API Keys * Create and manage keys from your [dashboard](https://ai.hackclub.com/dashboard) - give each key a descriptive name for easy identification (e.g. `CraftAI`) * You can have up to **50 active API keys.** * Never share your API keys or commit them to version control. It's your job to look after them! * Never use your API keys in client-side code - attackers can steal them and use them to make requests on your behalf. ## Example Request ```bash curl https://ai.hackclub.com/proxy/v1/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen/qwen3-32b", "messages": [{"role": "user", "content": "Hello!"}] }' ``` --- --- url: /api/chat-completions.md description: >- Create chat completions (aka: talk to the AI) for conversations with AI models. Supports streaming and non-streaming modes. --- # Chat Completions Create chat completions for conversations with AI models. Supports streaming and non-streaming modes. ## Endpoint ``` POST /proxy/v1/chat/completions ``` ## Request Body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `model` | string | Yes | Model ID (e.g., `qwen/qwen3-32b`) | | `messages` | array | Yes | Array of message objects | | `stream` | boolean | No | Enable streaming (default: `false`) | | `temperature` | number | No | Controls randomness, 0-2 (default: `1.0`) | | `max_tokens` | number | No | Maximum tokens to generate | | `top_p` | number | No | Nucleus sampling (default: `1.0`) | ### Message Object | Field | Type | Description | |-------|------|-------------| | `role` | string | `user`, `assistant`, or `system` | | `content` | string | The message content | ## Example Request ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen/qwen3-32b", "messages": [ {"role": "user", "content": "Tell me a joke."} ] }' ``` ```javascript [JavaScript] import { OpenRouter } from "@openrouter/sdk"; const client = new OpenRouter({ apiKey: "YOUR_API_KEY", baseURL: "https://ai.hackclub.com/proxy/v1", }); const response = await client.chat.send({ model: "qwen/qwen3-32b", messages: [ { role: "user", content: "Tell me a joke." }, ], stream: false, }); console.log(response.choices[0].message.content); ``` ```python [Python] from openrouter import OpenRouter client = OpenRouter( api_key="YOUR_API_KEY", server_url="https://ai.hackclub.com/proxy/v1", ) response = client.chat.send( model="qwen/qwen3-32b", messages=[ {"role": "user", "content": "Tell me a joke."} ], stream=False, ) print(response.choices[0].message.content) ``` ::: ## Example Response ```json { "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "model": "qwen/qwen3-32b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Why did the scarecrow win an award? Because he was outstanding in his field!" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 20, "completion_tokens": 15, "total_tokens": 35 } } ``` ## Streaming Set `stream: true` to receive responses as server-sent events (SSE). Each chunk contains a delta of the response. --- --- url: /api/embeddings.md description: >- Generate vector embeddings from text input. Use these embeddings for semantic search, clustering, or storing in vector databases like Pinecone or pgvector. --- # Embeddings Generate vector embeddings from text input. Use these embeddings for semantic search, clustering, or storing in vector databases like Pinecone or pgvector. ## Endpoint ``` POST /proxy/v1/embeddings ``` ## Request Body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `model` | string | Yes | Embedding model ID (e.g., `qwen/qwen3-embedding-8b`) | | `input` | string or array | Yes | Text to embed (single string or array of strings) | ## Example Request ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/embeddings \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen/qwen3-embedding-8b", "input": "The quick brown fox jumps over the lazy dog" }' ``` ```python [Python] from openrouter import OpenRouter client = OpenRouter( api_key="YOUR_API_KEY", ) response = client.embeddings.generate( model="qwen/qwen3-embedding-8b", input="The quick brown fox jumps over the lazy dog", ) embedding_vector = response.data[0].embedding print(len(embedding_vector), "dimensions") ``` ::: ## Example Response ```json { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [0.0023, -0.0134, 0.0421, ...] } ], "model": "qwen/qwen3-embedding-8b", "usage": { "prompt_tokens": 9, "total_tokens": 9 } } ``` ## Available Models To list available embedding models: ```bash curl https://ai.hackclub.com/proxy/v1/embeddings/models ``` This endpoint is OpenRouter compatible and requires no authentication. --- --- url: /api/exa.md description: >- Search the web, find similar pages, extract content and answer questions using Exa. --- # Exa Use Exa through the Hack Club AI proxy to search the web, find similar pages, extract content from URLs and answer questions with live web context. ## Endpoints ``` POST /proxy/v1/exa/search POST /proxy/v1/exa/findSimilar POST /proxy/v1/exa/contents POST /proxy/v1/exa/answer ``` These endpoints proxy Exa's API. Send the same JSON body you would send to Exa, but authenticate with your Hack Club AI API key instead of an Exa API key. ## Authentication ```bash Authorization: Bearer YOUR_API_KEY Content-Type: application/json ``` ## Search Search the web for pages matching a query. ```bash curl https://ai.hackclub.com/proxy/v1/exa/search \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "Hack Club projects", "numResults": 5 }' ``` ## Find Similar Find pages similar to a given URL. ```bash curl https://ai.hackclub.com/proxy/v1/exa/findSimilar \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hackclub.com/", "numResults": 5 }' ``` ## Contents Extract page contents from URLs. ```bash curl https://ai.hackclub.com/proxy/v1/exa/contents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "urls": ["https://hackclub.com/"] }' ``` ## Answer Ask a question and get an answer grounded in web results. ```bash curl https://ai.hackclub.com/proxy/v1/exa/answer \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What is Hack Club?" }' ``` ## Streaming Answers If Exa supports streaming for your request, pass `"stream": true` and read the response as server-sent events. ```bash curl https://ai.hackclub.com/proxy/v1/exa/answer \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What is Hack Club?", "stream": true }' ``` ## Billing Exa usage counts toward your normal Hack Club AI daily spending limit. The proxy records Exa's `costDollars.total` value for each request. ## Learn More For all supported request and response fields, see the [Exa API documentation](https://docs.exa.ai/). --- --- url: /api/get-models.md description: List all available models for chat completions and embeddings. --- # Get Models List all available models for chat completions and embeddings. ## All Models ``` GET /proxy/v1/models ``` List all available models. **No authentication required.** ### Example Request ```bash curl https://ai.hackclub.com/proxy/v1/models ``` ### Response Format Returns every chat and embedding model you can use, in [OpenRouter's model format](https://openrouter.ai/docs/api-reference/models/get-models): ```json { "data": [ { "id": "qwen/qwen3-32b", "name": "Qwen: Qwen3 32B", "created": 1745875945, "description": "Qwen3-32B is a dense 32.8B parameter causal language model...", "context_length": 131072, "architecture": { "modality": "text->text", "input_modalities": ["text"], "output_modalities": ["text"] }, "pricing": { "prompt": "0.00000008", "completion": "0.00000028" }, "top_provider": { "context_length": 40960, "max_completion_tokens": 16384 }, "supported_parameters": ["max_tokens", "temperature", "tools", "..."] } ] } ``` `pricing` is in USD per token. ## Embedding Models ``` GET /proxy/v1/embeddings/models ``` List all available embedding models. **No authentication required.** ### Example Request ```bash curl https://ai.hackclub.com/proxy/v1/embeddings/models ``` ### Response Format Returns only the embedding models, in the same format as above. --- --- url: /api/healthcheck.md description: Check if Hack Club AI is working and ready to serve requests. --- # Healthcheck Check if Hack Club AI is working and ready to serve requests. ## Endpoint ``` GET /up ``` ### Example Request ```bash curl https://ai.hackclub.com/up ``` ### Response Format ```json { "status": "up", "postgres": true, "clickhouse": true, "openRouter": true, "mistral": true, "exa": true, "balanceRemaining": 100.466965531, "dailyKeyUsageRemaining": 42.247146656, "keyLimitRemaining": 42.247146656, "keyUsage": 3177.113867455, "replicateUnusedCredit": 14.33175, "timestamp": 1790263354649 } ``` The response status is `200` when everything is up and `503` when anything is down. | Field Name | Description | |------------|-------------| | `status` | `up` if every check below passed, otherwise `down`. | | `postgres` | Whether the database is reachable. | | `clickhouse` | Whether the analytics database is reachable. | | `openRouter` | Whether OpenRouter (chat, embeddings, images) is reachable. | | `mistral` | Whether Mistral (OCR) is reachable. | | `exa` | Whether Exa (web search) is reachable. | | `balanceRemaining` | Credit remaining in Hack Club AI's OpenRouter account, in USD. | | `dailyKeyUsageRemaining` | Spend remaining on Hack Club AI's OpenRouter key, in USD. Subject to change. | | `keyLimitRemaining` | Same as `dailyKeyUsageRemaining`. | | `keyUsage` | Total spend on Hack Club AI's OpenRouter key, in USD. | | `replicateUnusedCredit` | Credit remaining in Hack Club AI's Replicate account, in USD. May be missing. | | `timestamp` | When the result was generated, in milliseconds since the Unix epoch. Results may be up to 30 seconds old. | --- --- url: /api/image-generation.md description: Generate images with AI --- # Image Generation Generate images using AI models via the chat completions endpoint. ## Supported Models Any model that can output images works, for example: * `google/gemini-2.5-flash-image` (aka Nano Banana) - fast and cheap * `google/gemini-3.1-flash-image` - newer, better quality * `google/gemini-3-pro-image` (aka Nano Banana Pro) - highest quality output, but also slower See the [models list](/models-list) for every model with image output. ## Request Parameters In addition to standard chat completion parameters, include: | Parameter | Type | Description | |-----------|------|-------------| | `modalities` | array | Set to `["image", "text"]` for image generation | | `image_config` | object | Optional configuration for image output | | `image_config.aspect_ratio` | string | Aspect ratio (see table below) | ### Supported Aspect Ratios For Gemini image-generation models: | Aspect Ratio | Dimensions | |--------------|------------| | `1:1` | 1024×1024 (default) | | `2:3` | 832×1248 | | `3:2` | 1248×832 | | `3:4` | 864×1184 | | `4:3` | 1184×864 | | `4:5` | 896×1152 | | `5:4` | 1152×896 | | `9:16` | 768×1344 | | `16:9` | 1344×768 | | `21:9` | 1536×672 | ## Example Request ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemini-2.5-flash-image", "messages": [ { "role": "user", "content": "Make a picture of a sunset over mountains" } ], "modalities": ["image", "text"], "image_config": { "aspect_ratio": "16:9" }, "stream": false }' ``` ```python [Python] import base64 import requests headers = { "Authorization": f"Bearer YOUR_API_KEY", "Content-Type": "application/json" } payload = { "model": "google/gemini-2.5-flash-image", "messages": [ { "role": "user", "content": "Make a picture of a sunset over mountains" } ], "modalities": ["image", "text"], "image_config": { "aspect_ratio": "16:9" } } response = requests.post( "https://ai.hackclub.com/proxy/v1/chat/completions", headers=headers, json=payload ) result = response.json() if result.get("choices"): message = result["choices"][0]["message"] if message.get("images"): image_url = message["images"][0]["image_url"]["url"] # Handle data URI prefix if "," in image_url: base64_data = image_url.split(",")[1] else: base64_data = image_url image_bytes = base64.b64decode(base64_data) # Save or process image_bytes as needed ``` ::: ## Response Format The response includes an `images` array in the assistant message: ```json { "choices": [ { "message": { "role": "assistant", "content": "Here is your image of a sunset over mountains.", "images": [ { "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } } ] }, "finish_reason": "stop" } ] } ``` * **Format**: Images are returned as base64-encoded data URLs * **Types**: Typically PNG format (`data:image/png;base64,`) * **Multiple Images**: Some models can generate multiple images in a single response ## Streaming Image generation also works with streaming responses. Set `stream: true` to receive the response as server-sent events. ## More Information Refer to the [OpenRouter image docs](https://openrouter.ai/docs/guides/overview/multimodal/image-generation) for more details. --- --- url: /api/image-inputs.md description: Send images to vision-capable models for analysis and understanding. --- # Image Inputs Send images to vision-capable models for analysis and understanding. ## Supported Formats * **URL**: Send publicly accessible images directly - use this if possible! * **Base64**: Required for local files or private images. ## Example: Image via URL ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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" } } ] } ] }' ``` ```javascript [JavaScript] const response = await fetch('https://ai.hackclub.com/proxy/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer YOUR_API_KEY`, 'Content-Type': 'application/json', }, body: JSON.stringify({ 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', }, }, ], }, ], }), }); const data = await response.json(); console.log(data.choices[0].message.content); ``` ```python [Python] import requests response = requests.post( "https://ai.hackclub.com/proxy/v1/chat/completions", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "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" } } ] }] } ) result = response.json() print(result["choices"][0]["message"]["content"]) ``` ::: ## Example: Base64-encoded Image ::: code-group ```javascript [JavaScript] // Convert image to base64 const imageBuffer = await fetch('https://example.com/image.jpg').then(r => r.arrayBuffer()); const base64Image = Buffer.from(imageBuffer).toString('base64'); const response = await fetch('https://ai.hackclub.com/proxy/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer YOUR_API_KEY`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'google/gemini-2.5-flash', messages: [ { role: 'user', content: [ { type: 'text', text: "What's in this image?", }, { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}`, }, }, ], }, ], }), }); const data = await response.json(); console.log(data.choices[0].message.content); ``` ```python [Python] import base64 import requests def encode_image(path): with open(path, "rb") as f: return base64.b64encode(f.read()).decode() image_data = encode_image("path/to/image.jpg") response = requests.post( "https://ai.hackclub.com/proxy/v1/chat/completions", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "model": "google/gemini-2.5-flash", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, { "type": "image_url", "image_url": { "url": f"data:image/jpeg;base64,{image_data}" } } ] }] } ) result = response.json() print(result["choices"][0]["message"]["content"]) ``` ::: ## Response Format ```json { "id": "chatcmpl-123", "object": "chat.completion", "model": "google/gemini-2.5-flash", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This image shows a sunset over mountains with vibrant orange and purple colors..." }, "finish_reason": "stop" } ] } ``` --- --- url: /api/jev.md description: >- Ask typed yes/no, multiple-choice and scored questions about any text with TypeSafe's Jev model. --- # Jev [Jev](https://typesafe.ai) is TypeSafe's "System One" model. Instead of generating free-form text, you send it some `state` (a string, object or array) plus a set of typed **questions**, and it returns one structured answer per question - a probability, a chosen option, or a score. Answers come back in a few hundred milliseconds and cost a fraction of an LLM call, which makes Jev a good fit for classification, routing, moderation, ranking and other "narrow decision" tasks inside your code. ## Endpoints ``` POST /proxy/v1/jev/systemone GET /proxy/v1/jev/models ``` These proxy TypeSafe's API. Send the same JSON body you would send to TypeSafe, but authenticate with your Hack Club AI API key instead of a TypeSafe key. ::: tip Using the official SDKs The proxy also serves `/proxy/v1/jev/v1/systemone` and `/proxy/v1/jev/v1/models`, which mirror TypeSafe's own paths. Point the [TypeSafe JavaScript or Python SDK](https://docs.typesafe.ai/sdk) at `https://ai.hackclub.com/proxy/v1/jev` as the base URL and everything works unchanged. See the examples below. ::: ## Authentication ```bash Authorization: Bearer YOUR_API_KEY Content-Type: application/json ``` ## Request Body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `state` | string | object | array | Yes | The content to evaluate. A plain string for text, or structured data. | | `questions` | object | Yes | A map of question id to question. Each question is a `noul`, `choice` or `score` (see below). | | `model` | string | No | Model to use. Default: `jev-latest`. | ### Question types All three question types share `type` and `instructions`. Each adds its own `criteria`. **Noul** - a yes/no question. Returns the probability the answer is yes (0 to 1). ```json { "type": "noul", "instructions": "Does this convey urgency?", "criteria": { "true": "Explicitly time-sensitive", "false": "No urgency expressed" } } ``` **Choice** - pick one option from a set you define. Returns the chosen option plus a probability for every option. ```json { "type": "choice", "instructions": "Which team should handle this?", "criteria": { "billing": "Payments, invoicing, refunds", "technical": "Bugs, outages, integrations", "sales": "Pricing, upgrades, new accounts" } } ``` **Score** - rate the state along an ordered rubric. Returns a probability-weighted value that can land between levels. `criteria` must have at least two levels. ```json { "type": "score", "instructions": "How frustrated is the customer?", "criteria": ["Calm", "Frustrated", "Very angry"] } ``` ## Example ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/jev/systemone \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "state": "Help! My payouts have been failing for 3 days.", "model": "jev-latest", "questions": { "is_urgent": { "type": "noul", "instructions": "Does this convey urgency?" }, "department": { "type": "choice", "instructions": "Which team should handle this?", "criteria": { "billing": "Payments, invoicing, refunds", "technical": "Bugs, outages, integrations", "sales": "Pricing, upgrades, new accounts" } }, "frustration": { "type": "score", "instructions": "How frustrated is the customer?", "criteria": ["Calm", "Frustrated", "Very angry"] } } }' ``` ```javascript [JavaScript] import { TypeSafeClient, noul, choice, score } from "@typesafe-ai/sdk"; const client = new TypeSafeClient({ apiKey: "YOUR_API_KEY", baseURL: "https://ai.hackclub.com/proxy/v1/jev", }); const result = await client.systemOne({ state: "Help! My payouts have been failing for 3 days.", questions: { is_urgent: noul("Does this convey urgency?"), department: choice("Which team should handle this?", { billing: "Payments, invoicing, refunds", technical: "Bugs, outages, integrations", sales: "Pricing, upgrades, new accounts", }), frustration: score("How frustrated is the customer?", [ "Calm", "Frustrated", "Very angry", ]), }, }); console.log(result.answers.is_urgent.noul); // 0.92 console.log(result.answers.department.choice); // "technical" console.log(result.answers.frustration.score); // 1.6 ``` ```python [Python] from typesafe_sdk import TypeSafeClient, Noul, Choice, Score client = TypeSafeClient( api_key="YOUR_API_KEY", base_url="https://ai.hackclub.com/proxy/v1/jev", ) response = client.system_one( state="Help! My payouts have been failing for 3 days.", questions={ "is_urgent": Noul(instructions="Does this convey urgency?"), "department": Choice( instructions="Which team should handle this?", criteria={ "billing": "Payments, invoicing, refunds", "technical": "Bugs, outages, integrations", "sales": "Pricing, upgrades, new accounts", }, ), "frustration": Score( instructions="How frustrated is the customer?", criteria=["Calm", "Frustrated", "Very angry"], ), }, ) print(response.nouls["is_urgent"].noul) # 0.92 print(response.choices["department"].choice) # "technical" print(response.scores["frustration"].score) # 1.6 ``` ::: ## Response One answer per question, keyed by the same ids you sent. Every answer carries a `type` matching its question. `choice` and `score` answers also include a `confidence` between 0 and 1 derived from the probability distribution. ```json { "model": "jev-1.13.0", "answers": { "is_urgent": { "type": "noul", "noul": 0.92 }, "department": { "type": "choice", "choice": "technical", "probabilities": { "billing": 0.08, "technical": 0.85, "sales": 0.07 }, "confidence": 0.82 }, "frustration": { "type": "score", "score": 1.6, "legend": { "0": "Calm", "1": "Frustrated", "2": "Very angry" }, "probabilities": { "0": 0.05, "1": 0.3, "2": 0.65 }, "confidence": 0.78 } }, "usage": { "input_tokens": 312, "output_tokens": 48 } } ``` ## Models `GET /proxy/v1/jev/models` lists the model names and aliases you can send in the `model` field. | Alias | Description | |-------|-------------| | `jev-latest` | The most recent stable release. This is the default. | | `jev-preview` | The most recent release, including preview builds. | The response's `model` field always reports the versioned id that answered (for example `jev-1.13.0`), so you can pin a specific version if you have tuned confidence thresholds against it. ## Errors | Status | Meaning | |--------|---------| | `400 Bad Request` | The request body was not valid JSON. | | `402 Payment Required` | You've reached your daily spending limit. | | `403 Forbidden` | Your account is banned or needs identity verification. | | `422 Unprocessable Entity` | The body failed validation (missing field, malformed question). The response names the offending field. | | `429 Too Many Requests` | You hit the rate limit. Wait for the time in the `Retry-After` header. | | `529 Overloaded` | TypeSafe is temporarily overloaded. Retry after a short delay. | ## Billing Jev usage counts toward your normal Hack Club AI daily spending limit. Jev is charged per input token only (output tokens are free), and the proxy records the cost of every request based on the `usage.input_tokens` reported by TypeSafe. ## Learn More For all supported request and response fields, question types, and design patterns, see the [TypeSafe documentation](https://docs.typesafe.ai/). --- --- url: /models-list.md --- # Models List Browse all available models on Hack Club AI. This list is updated whenever the docs are deployed; for the live list, call [`GET /proxy/v1/models`](/api/get-models). --- --- url: /api/moderations.md description: >- Classify if text or images are potentially inappropriate (e.g., hate speech, violence, NSFW content). --- # Moderations Classify if text or images are potentially inappropriate (e.g., hate speech, violence, NSFW content). ## Endpoint ``` POST /proxy/v1/moderations ``` ## Request Body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `input` | string | Yes | Text to classify | ## Example Request ```bash curl https://ai.hackclub.com/proxy/v1/moderations \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "I want to kill them." }' ``` ## Example Response ```json { "id": "modr-5558", "model": "omni-moderation-latest", "results": [ { "flagged": true, "categories": { "harassment": false, "harassment/threatening": false, "sexual": false, "hate": false, "hate/threatening": false, "illicit": false, "illicit/violent": false, "self-harm/intent": false, "self-harm/instructions": false, "self-harm": false, "sexual/minors": false, "violence": true, "violence/graphic": false }, "category_scores": { "harassment": 0.009, "harassment/threatening": 0.001, "sexual": 0.0002, "hate": 0.001, "hate/threatening": 0.00002, "illicit": 0.004, "illicit/violent": 0.00003, "self-harm/intent": 0.0002, "self-harm/instructions": 0.000003, "self-harm": 0.00001, "sexual/minors": 0.0002, "violence": 0.93, "violence/graphic": 0.000004 }, "category_applied_input_types": { "harassment": ["text"], "harassment/threatening": ["text"], "sexual": ["text"], "hate": ["text"], "hate/threatening": ["text"], "illicit": ["text"], "illicit/violent": ["text"], "self-harm/intent": ["text"], "self-harm/instructions": ["text"], "self-harm": ["text"], "sexual/minors": ["text"], "violence": ["text"], "violence/graphic": ["text"] } } ] } ``` ## Categories | Category | Description | |----------|-------------| | `harassment` | Harassing language | | `harassment/threatening` | Threatening harassment | | `sexual` | Sexual content | | `hate` | Hate speech | | `hate/threatening` | Threatening hate speech | | `illicit` | Illegal activity | | `illicit/violent` | Violent illegal activity | | `self-harm` | Self-harm content | | `self-harm/intent` | Intent to self-harm | | `self-harm/instructions` | Instructions for self-harm | | `sexual/minors` | Sexual content involving minors | | `violence` | Violent content | | `violence/graphic` | Graphic violence | --- --- url: /api/ocr.md description: Extract text and structured content from images and PDF documents using OCR. --- # OCR (Optical Character Recognition) Extract text and structured content from images and PDF documents. Powered by Mistral OCR, this endpoint can understand complex document elements including tables, mathematical expressions, and multi-column layouts. ## Endpoint ``` POST /proxy/v1/ocr ``` ## Request Body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `document` | object | Yes | The document to process (see Document Types below) | | `model` | string | No | Model to use. Default: `mistral-ocr-latest` | | `id` | string | No | Optional request identifier | | `pages` | array\ | No | Specific pages to process (0-indexed). Example: `[0, 1, 2]` | | `include_image_base64` | boolean | No | Include extracted images as base64 in response | | `image_limit` | integer | No | Maximum number of images to extract | | `image_min_size` | integer | No | Minimum height/width of images to extract | | `table_format` | string | No | Table output format: `"markdown"` or `"html"` | | `extract_header` | boolean | No | Extract document headers separately. Default: `false` | | `extract_footer` | boolean | No | Extract document footers separately. Default: `false` | | `document_annotation_format` | object | No | Response format for document annotation (see Response Formats) | | `bbox_annotation_format` | object | No | Response format for bounding box annotation (see Response Formats) | ## Document Types ### Image URL ```json { "document": { "type": "image_url", "image_url": "https://example.com/image.png" } } ``` ### Document URL (PDF) ```json { "document": { "type": "document_url", "document_url": "https://example.com/document.pdf" } } ``` ### File ID (previously uploaded) ```json { "document": { "type": "file", "file_id": "your_file_id_here" } } ``` ::: tip To process a local file, send it as a base64 data URI, e.g. `"document_url": "data:application/pdf;base64,..."` or `"image_url": "data:image/png;base64,..."`. Requests are capped at 20 MB, so for larger files, upload them to a public HTTPS URL instead. ::: ## Response Formats Use these for `document_annotation_format` or `bbox_annotation_format`: ### Text (default) ```json { "type": "text" } ``` ### JSON Object ```json { "type": "json_object" } ``` ### JSON Schema ```json { "type": "json_schema", "json_schema": { "your": "schema" } } ``` ## Example Request ```bash curl https://ai.hackclub.com/proxy/v1/ocr \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document": { "type": "image_url", "image_url": "https://example.com/receipt.png" }, "table_format": "markdown" }' ``` ## Example Response ```json { "model": "mistral-ocr-latest", "pages": [ { "index": 0, "markdown": "# Invoice\n\nDate: 2024-01-15\n\n| Item | Quantity | Price |\n|------|----------|-------|\n| Widget | 5 | $10.00 |\n| Gadget | 2 | $25.00 |\n\n**Total: $100.00**", "images": [], "dimensions": { "width": 800, "height": 1200 } } ], "document_annotation": null, "usage_info": { "pages_processed": 1, "doc_size_bytes": 102400 } } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `model` | string | The model used for OCR | | `pages` | array | List of OCR results per page | | `pages[].index` | integer | Page index (0-based) | | `pages[].markdown` | string | Extracted content in Markdown format | | `pages[].images` | array | Extracted images with bounding boxes | | `pages[].dimensions` | object | Page dimensions (width, height) | | `document_annotation` | string|null | Formatted response if annotation format was specified | | `usage_info` | object | Usage information for the request | ## Features * **Text Extraction**: Preserves document structure including headers, paragraphs, and lists * **Table Recognition**: Outputs tables in Markdown or HTML format * **Math Support**: Handles mathematical expressions and LaTeX formatting * **Multi-language**: Supports thousands of scripts and languages * **Image Extraction**: Optionally extract embedded images with bounding boxes * **Structured Output**: Use JSON schema for structured data extraction ## Supported Formats ### Images * PNG, JPEG/JPG, AVIF, WebP, and more ### Documents * PDF, PPTX, DOCX, and more ## Limitations * Maximum file size: 50 MB by URL, or about 15 MB as a base64 data URI (requests are capped at 20 MB) * Maximum pages: 1000 per request * Character formatting (bold, italic, underline) is not preserved * Footnotes and superscript text are preserved --- --- url: /api/pdf-inputs.md description: Send PDF documents to any model for analysis and summarization. --- # PDF Inputs Send PDF documents to any model for analysis and summarization. ## Supported Formats * **URL**: Send publicly accessible PDFs directly without encoding (hint: [Bucky](https://bucky.hackclub.com) may be useful) * **Base64**: Required for local files or private documents When a model supports file input natively, the PDF is passed directly to the model. Otherwise, the PDF will get parsed and the parsed results will be passed to the model. ## PDF Processing Engines Configure PDF processing using the `plugins` parameter: | Engine | Description | |--------|-------------| | `pdf-text` | Best for well-structured PDFs | | `mistral-ocr` | Best for scanned documents or PDFs with images | | `native` | Uses model's built-in file processing **(highly recommended)** | If you don't specify an engine, Hack Club AI defaults to the model's native capability first (which usually will be the best option), then falls back to `mistral-ocr`. ## Example: PDF via URL ```bash curl https://ai.hackclub.com/proxy/v1/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen/qwen3-32b", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "What are the main points in this document?"}, { "type": "file", "file": { "filename": "bitcoin.pdf", "file_data": "https://bitcoin.org/bitcoin.pdf" } } ] } ], "plugins": [ { "id": "file-parser", "pdf": {"engine": "pdf-text"} } ] }' ``` ## Example: Base64-encoded PDF ```python import base64 import requests def encode_pdf(path): with open(path, "rb") as f: return f"data:application/pdf;base64,{base64.b64encode(f.read()).decode()}" response = requests.post( "https://ai.hackclub.com/proxy/v1/chat/completions", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "model": "qwen/qwen3-32b", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "Summarize this document"}, { "type": "file", "file": { "filename": "document.pdf", "file_data": encode_pdf("path/to/document.pdf") } } ] }], "plugins": [{"id": "file-parser", "pdf": {"engine": "mistral-ocr"}}] } ) ``` ## Reusing File Annotations When you send a PDF, the response may include annotations in the assistant message. Include these in subsequent requests to skip re-parsing and save costs: ```python response = requests.post(url, headers=headers, json={...}) result = response.json() annotations = result["choices"][0]["message"].get("annotations") follow_up = requests.post(url, headers=headers, json={ "model": "qwen/qwen3-32b", "messages": [ {"role": "user", "content": [...]}, # Original message with PDF { "role": "assistant", "content": result["choices"][0]["message"]["content"], "annotations": annotations # Include these to skip re-parsing! }, {"role": "user", "content": "Can you elaborate on point 2?"} ] }) ``` --- --- url: /api/responses.md description: >- Create responses using the Responses API. Supports simple text input, structured messages, and streaming. --- # Responses API Create responses for conversations with AI models. Supports both simple string input and structured message arrays. ## Endpoint ``` POST /proxy/v1/responses ``` ## Request Body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `model` | string | Yes | Model ID (e.g., `qwen/qwen3-32b`) | | `input` | string or array | Yes | Text prompt or message array | | `stream` | boolean | No | Enable streaming (default: `false`) | | `max_output_tokens` | integer | No | Maximum tokens to generate | | `temperature` | number | No | Sampling temperature, 0-2 | | `top_p` | number | No | Nucleus sampling parameter, 0-1 | ### Message Object (Structured Input) | Field | Type | Description | |-------|------|-------------| | `type` | string | Must be `message` | | `role` | string | `user` or `assistant` | | `content` | array | Array of content objects | ### Content Object | Field | Type | Description | |-------|------|-------------| | `type` | string | `input_text` for user, `output_text` for assistant | | `text` | string | The message content | ## Simple String Input The simplest way to use the API: ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/responses \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen/qwen3-32b", "input": "What is the meaning of life?", "max_output_tokens": 9000 }' ``` ```javascript [JavaScript] const response = await fetch('https://ai.hackclub.com/proxy/v1/responses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'qwen/qwen3-32b', input: 'What is the meaning of life?', max_output_tokens: 9000, }), }); const result = await response.json(); console.log(result); ``` ```python [Python] import requests response = requests.post( 'https://ai.hackclub.com/proxy/v1/responses', headers={ 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, json={ 'model': 'qwen/qwen3-32b', 'input': 'What is the meaning of life?', 'max_output_tokens': 9000, } ) result = response.json() print(result) ``` ::: ## Structured Message Input For more complex conversations, use the message array format: ::: code-group ```bash [cURL] curl https://ai.hackclub.com/proxy/v1/responses \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen/qwen3-32b", "input": [ { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "Tell me a joke about programming" } ] } ], "max_output_tokens": 9000 }' ``` ```javascript [JavaScript] const response = await fetch('https://ai.hackclub.com/proxy/v1/responses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'qwen/qwen3-32b', input: [ { type: 'message', role: 'user', content: [ { type: 'input_text', text: 'Tell me a joke about programming', }, ], }, ], max_output_tokens: 9000, }), }); const result = await response.json(); ``` ```python [Python] import requests response = requests.post( 'https://ai.hackclub.com/proxy/v1/responses', headers={ 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, json={ 'model': 'qwen/qwen3-32b', 'input': [ { 'type': 'message', 'role': 'user', 'content': [ { 'type': 'input_text', 'text': 'Tell me a joke about programming', }, ], }, ], 'max_output_tokens': 9000, } ) result = response.json() ``` ::: ## Example Response ```json { "id": "resp_1234567890", "object": "response", "created_at": 1234567890, "model": "qwen/qwen3-32b", "output": [ { "type": "message", "id": "msg_abc123", "status": "completed", "role": "assistant", "content": [ { "type": "output_text", "text": "The meaning of life is a philosophical question...", "annotations": [] } ] } ], "usage": { "input_tokens": 12, "output_tokens": 45, "total_tokens": 57 }, "status": "completed" } ``` ## Streaming Set `stream: true` to receive responses as server-sent events (SSE): ::: code-group ```javascript [JavaScript] const response = await fetch('https://ai.hackclub.com/proxy/v1/responses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'qwen/qwen3-32b', input: 'Write a short story about AI', stream: true, max_output_tokens: 9000, }), }); const reader = response.body?.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') return; try { const parsed = JSON.parse(data); console.log(parsed); } catch (e) { // Skip invalid JSON } } } } ``` ```python [Python] import requests import json response = requests.post( 'https://ai.hackclub.com/proxy/v1/responses', headers={ 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, json={ 'model': 'qwen/qwen3-32b', 'input': 'Write a short story about AI', 'stream': True, 'max_output_tokens': 9000, }, stream=True ) for line in response.iter_lines(): if line: line_str = line.decode('utf-8') if line_str.startswith('data: '): data = line_str[6:] if data == '[DONE]': break try: parsed = json.loads(data) print(parsed) except json.JSONDecodeError: continue ``` ::: ## Multi-Turn Conversations The Responses API is stateless—include the full conversation history in each request: ```javascript const response = await fetch('https://ai.hackclub.com/proxy/v1/responses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'qwen/qwen3-32b', input: [ { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'What is the capital of France?' }], }, { type: 'message', role: 'assistant', id: 'msg_abc123', status: 'completed', content: [{ type: 'output_text', text: 'The capital of France is Paris.', annotations: [] }], }, { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'What is the population of that city?' }], }, ], max_output_tokens: 9000, }), }); ``` ::: info The `id` and `status` fields are required for any `assistant` role messages in conversation history. ::: --- --- url: /guide/rules.md --- # Rules & Rate Limiting ## Rules 1. **For teens.** This service is for teens 18 and under only. Hack Club is a charity — please do not abuse this service. 2. **No coding agents.** You are not allowed to use this service with coding agents like Cursor. 3. **No proxies.** You are not allowed to use this service to create proxies or other tools that allow others to access the API without them also abiding by these rules. 4. **No resale.** You are not allowed to resell this service or use it to create a service that resells AI to others. 5. Don't use this service with OpenClaw or similar tools like Hermes Agent or NanoClaw. They waste tokens and provide little benefit. 6. **Follow the Code of Conduct.** You are not allowed to use this service to create tools that intentionally violate the [Code of Conduct](https://hackclub.com/conduct). Don't try to generate explicit imagery or text, malware, or other harmful content. ## Daily Spending Limit Every account can spend **$3 per day** across all endpoints. The day resets at 00:00 UTC. Each request reserves its estimated cost up front and is charged what the provider actually reports when it finishes, so you aren't charged for requests that fail upstream. Requests that would take you over the limit get a `402 Payment Required` response. Need a higher limit? Email hey@mahadk.com. Your spend for today is shown at the top of your [dashboard](https://ai.hackclub.com/dashboard). ## Rate Limiting Every user can make **7,500 requests per 30 minutes**, shared across all endpoints. When you exceed the rate limit, the API returns `429 Too Many Requests` with a `Retry-After` header saying how many seconds to wait. ## Errors Errors are returned as JSON with an `error` message: ```json { "error": "Rate limit exceeded. Retry in 42s." } ``` | Status | Meaning | |--------|---------| | `400` | The request body is invalid (not JSON, or missing a required field). | | `401` | The API key is missing or invalid. | | `402` | You've reached your daily spending limit. | | `403` | Your account is banned, needs identity verification, or the request was blocked by the rules above. | | `429` | You've hit the rate limit. Wait for the time in `Retry-After`. | Errors from the upstream model provider (for example, an unknown model ID) are passed through with the provider's status code and body. --- --- url: /api/stats.md description: Get token usage statistics for your account. --- # Token Stats Get token usage statistics for your account. ## Endpoint ``` GET /proxy/v1/stats ``` ## Example Request ```bash curl https://ai.hackclub.com/proxy/v1/stats \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Example Response ```json { "totalRequests": 123456, "totalTokens": 7890, "totalPromptTokens": 1234, "totalCompletionTokens": 5678 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `totalRequests` | number | Total number of API requests made | | `totalTokens` | number | Total tokens used (prompt + completion) | | `totalPromptTokens` | number | Total tokens used in prompts | | `totalCompletionTokens` | number | Total tokens generated in responses | --- --- url: /guide/replicate.md description: Learn how to use Replicate models with the Hack Club AI API. --- # Using Replicate Models The Hack Club AI API allows you to easily integrate and use models hosted on [Replicate](https://replicate.com/). This guide will walk you through the steps to get started with Replicate models using the Hack Club AI API. ## Finding Replicate Models Take a look at [HCAI's model directory](https://ai.hackclub.com/replicate) to find models that suit your needs. Each model page contains important information such as input parameters, output format, and example usage. ## Making Requests To use a Replicate model, you'll need to make a request to the Hack Club AI API's Replicate endpoint. Below are examples of how to do this. ::: code-group ```ts [JavaScript] import Replicate from "replicate"; // Make sure REPLICATE_API_TOKEN is set to your Hack Club AI API key const replicate = new Replicate({ baseUrl: "https://ai.hackclub.com/proxy/v1/replicate" }); const input = { voice: "William (Whispering)", prompt: "Poppin' bottles in the ice, like a blizzard.\nWhen we drink, we do it right, gettin' slizzered\nSippin' sizzurp in my ride (in my ride) like Three 6.\nNow I'm feelin' so fly like a G 6" }; const output = await replicate.run("resemble-ai/chatterbox-pro", { input }); // To access the file URL: console.log(output.url()); ``` ```bash [cURL] curl --silent --show-error https://ai.hackclub.com/proxy/v1/replicate/models/minimax/speech-02-turbo/predictions \ --request POST \ --header "Authorization: Bearer $HACK_CLUB_AI_KEY" \ --header "Content-Type: application/json" \ --header "Prefer: wait" \ --data @- <<'EOM' { "input": { "text": "Speech-02-series is a Text-to-Audio and voice cloning technology that offers voice synthesis, emotional expression, and multilingual capabilities.\n\nThe HD version is optimized for high-fidelity applications like voiceovers and audiobooks. While the turbo one is designed for real-time applications with low latency.\n\nWhen using this model on Replicate, each character represents 1 token.", "emotion": "angry", "voice_id": "Deep_Voice_Man", "language_boost": "English", "english_normalization": true } } EOM ``` ::: --- --- url: /guide/using-with-vercel-ai-sdk.md description: Use Hack Club AI with Vercel AI SDK. --- # Using with Vercel AI SDK This guide will show you how to use Hack Club AI with the Vercel AI SDK. ## Installation To use Hack Club AI with the Vercel AI SDK, you will need to install the SDK and configure it with your API key. Hack Club AI is OpenRouter compatible, so we'll use the OpenRouter provider and change the base URL. ```sh npm install @openrouter/ai-sdk-provider ai // [!=npm auto] ``` ## Usage First, create a provider instance: ```ts import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { generateText } from 'ai'; const hackclub = createOpenRouter({ apiKey: process.env.HACK_CLUB_AI_API_KEY, baseUrl: 'https://ai.hackclub.com/proxy/v1', }); ``` Then, use it like any other AI SDK provider: ```ts const { text } = await generateText({ model: hackclub('qwen/qwen3-32b'), system: 'You are a helpful assistant.', prompt: "What is the capital of Peru?", }); console.log(text); ``` ## Next Steps To learn more about the Vercel AI SDK, see the [Vercel AI SDK documentation](https://ai-sdk.dev/docs/ai-sdk-core/overview). --- --- url: /guide/web-search.md description: Add real-time web search to your AI applications. --- # Web Search for AI Ground your AI responses with real-time web data using the [Hack Club Search API](https://search.hackclub.com). This guide shows how to combine Hack Club AI with web search for more accurate, up-to-date responses. ## Why Add Web Search? LLMs have knowledge cutoffs and can hallucinate facts. By incorporating live search results into your prompts, you can: * Provide current information (news, prices, events). * Ground responses in verifiable sources and increase trustworthiness. * Reduce hallucinations with real data. ## Getting Started You'll need two API keys: 1. **Hack Club AI** - for LLM inference ([get one here](/guide/authentication)) 2. **Hack Club Search** - for web search ([get one from the dashboard](https://search.hackclub.com)) ## Basic Pattern The pattern is simple: search first, then include results in your prompt. ```ts import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { generateText } from 'ai'; const hackclub = createOpenRouter({ apiKey: process.env.HACK_CLUB_AI_API_KEY, baseUrl: 'https://ai.hackclub.com/proxy/v1', }); async function searchWeb(query: string) { const res = await fetch( `https://search.hackclub.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, { headers: { Authorization: `Bearer ${process.env.HACK_CLUB_SEARCH_API_KEY}`, }, } ); return res.json(); } async function askWithSearch(question: string) { // 1. Search the web const searchResults = await searchWeb(question); // 2. Format results for the prompt const context = searchResults.web?.results ?.map((r: any) => `[${r.title}](${r.url})\n${r.description}`) .join('\n\n'); // 3. Ask the AI with search context const { text } = await generateText({ model: hackclub('qwen/qwen3-32b'), system: `You are a helpful assistant. Use the following web search results to answer questions accurately. Cite sources using markdown links. Web search results: ${context}`, prompt: question, }); return text; } // Example usage const answer = await askWithSearch("What's happening at Hack Club right now?"); console.log(answer); ``` ## Search Types The Search API supports multiple search types for different use cases: ### Web Search General web search for pages, articles, and documentation. ```ts const res = await fetch( `https://search.hackclub.com/res/v1/web/search?q=${query}&count=10`, { headers: { Authorization: `Bearer ${apiKey}` } } ); ``` ### News Search Get recent news articles with the `freshness` parameter. ```ts const res = await fetch( `https://search.hackclub.com/res/v1/news/search?q=${query}&freshness=pd`, // pd = past day { headers: { Authorization: `Bearer ${apiKey}` } } ); ``` ### Image Search Search for images to include in multimodal prompts. ```ts const res = await fetch( `https://search.hackclub.com/res/v1/images/search?q=${query}&count=5`, { headers: { Authorization: `Bearer ${apiKey}` } } ); ``` ## Python Example ```python import requests import os def search_web(query: str) -> dict: response = requests.get( 'https://search.hackclub.com/res/v1/web/search', params={'q': query, 'count': 5}, headers={'Authorization': f'Bearer {os.environ["HACK_CLUB_SEARCH_API_KEY"]}'} ) return response.json() def ask_with_search(question: str) -> str: # Search the web results = search_web(question) # Format context context = "\n\n".join([ f"[{r['title']}]({r['url']})\n{r['description']}" for r in results.get('web', {}).get('results', []) ]) # Call Hack Club AI response = requests.post( 'https://ai.hackclub.com/chat/completions', headers={'Authorization': f'Bearer {os.environ["HACK_CLUB_AI_API_KEY"]}'}, json={ 'model': 'qwen/qwen3-32b', 'messages': [ { 'role': 'system', 'content': f'Use these web results to answer accurately. Cite sources.\n\n{context}' }, {'role': 'user', 'content': question} ] } ) return response.json()['choices'][0]['message']['content'] ``` ## Tips * **Be specific with queries** - More specific search queries yield better results. * **Limit results** - 3-5 results is usually enough context without overwhelming the model. If you request too many, it can pollute the context and make it harder for the model to understand the question. * **Use freshness** - For time-sensitive topics, use `freshness=pd` (past day) or `freshness=pw` (past week). * **Cite sources** - Instruct the model to cite sources so users can verify information. * **Handle errors** - The search API has rate limits (400 requests per 30 minutes), so handle 429 errors gracefully. ## Rate Limits The Hack Club Search API allows: * 400 requests per 30 minutes per user * Maximum query length: 400 characters ## Next Steps * [Hack Club Search API Documentation](https://search.hackclub.com/docs) - Full API reference * [Brave Search API Docs](https://api.search.brave.com/app/documentation) - Detailed response schemas