4242 4242 4242 4242.Single-file, zero-dependency clients for the ToolHund AI Tool Intelligence API. MIT licensed. Drop them into any project and start shipping.
Runs on Node 18+, Deno, Bun, Cloudflare Workers, and modern browsers. No dependencies.
curl -o toolhund.ts https://toolhund.com/api/public/sdk/toolhund/tsimport { ToolHund } from "./toolhund";
const th = new ToolHund({ apiKey: process.env.TH_KEY! });
const { data } = await th.tools.list({ q: "agents", limit: 10 });
console.log(data);// toolhund.ts — Official TypeScript SDK for the ToolHund API
// Zero dependencies. Works in Node 18+, Deno, Bun, Cloudflare Workers, and modern browsers.
// Docs: https://toolhund.com/api-docs
export interface ToolHundOptions {
apiKey: string;
baseUrl?: string;
fetch?: typeof fetch;
}
export interface ToolHundError extends Error {
status: number;
code?: string;
requestId?: string;
}
export class ToolHund {
private key: string;
private base: string;
private f: typeof fetch;
constructor(opts: ToolHundOptions) {
if (!opts.apiKey) throw new Error("ToolHund: apiKey is required");
this.key = opts.apiKey;
this.base = (opts.baseUrl ?? "https://toolhund.com").replace(/\/$/, "");
this.f = opts.fetch ?? fetch;
}
private async req<T>(method: string, path: string, query?: Record<string, unknown>, body?: unknown): Promise<T> {
const url = new URL(this.base + path);
if (query) for (const [k, v] of Object.entries(query)) if (v != null) url.searchParams.set(k, String(v));
const res = await this.f(url.toString(), {
method,
headers: {
"Authorization": `Bearer ${this.key}`,
"Accept": "application/json",
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok) {
const err = new Error(json?.error?.message ?? `HTTP ${res.status}`) as ToolHundError;
err.status = res.status;
err.code = json?.error?.code;
err.requestId = res.headers.get("x-request-id") ?? undefined;
throw err;
}
return json as T;
}
tools = {
list: (params?: { q?: string; category?: string; limit?: number; offset?: number }) =>
this.req<{ data: any[]; total: number }>("GET", "/api/v1/tools", params),
get: (slug: string) => this.req<any>("GET", `/api/v1/tools/${slug}`),
alternatives: (slug: string) => this.req<any>("GET", `/api/v1/tools/${slug}/alternatives`),
reviews: (slug: string, params?: { limit?: number; offset?: number }) =>
this.req<any>("GET", `/api/v1/tools/${slug}/reviews`, params),
benchmarks: (slug: string) => this.req<any>("GET", `/api/v1/tools/${slug}/benchmarks`),
};
categories = { list: () => this.req<any>("GET", "/api/v1/categories") };
deals = { list: () => this.req<any>("GET", "/api/v1/deals") };
news = { list: () => this.req<any>("GET", "/api/v1/news") };
stacks = {
build: (input: { task: string; budget?: string; team_size?: number }) =>
this.req<any>("POST", "/api/v1/stacks/build", undefined, input),
};
}
export default ToolHund;
Python 3.8+. Depends on requests.
curl -o toolhund.py https://toolhund.com/api/public/sdk/toolhund/py
pip install requestsfrom toolhund import ToolHund
th = ToolHund(api_key="th_live_...")
print(th.list_tools(q="agents", limit=10))"""toolhund.py — Official Python SDK for the ToolHund API.
Requires: requests (pip install requests)
Docs: https://toolhund.com/api-docs
"""
from __future__ import annotations
from typing import Any, Optional, Dict
import requests
class ToolHundError(Exception):
def __init__(self, message: str, status: int, code: Optional[str] = None, request_id: Optional[str] = None):
super().__init__(message)
self.status = status
self.code = code
self.request_id = request_id
class ToolHund:
def __init__(self, api_key: str, base_url: str = "https://toolhund.com"):
if not api_key:
raise ValueError("api_key is required")
self.key = api_key
self.base = base_url.rstrip("/")
self.s = requests.Session()
self.s.headers.update({"Authorization": f"Bearer {api_key}", "Accept": "application/json"})
def _req(self, method: str, path: str, params: Optional[Dict[str, Any]] = None, json: Optional[Any] = None) -> Any:
r = self.s.request(method, self.base + path, params=params, json=json, timeout=30)
try:
data = r.json() if r.text else None
except ValueError:
data = None
if not r.ok:
err = (data or {}).get("error") or {}
raise ToolHundError(err.get("message", f"HTTP {r.status_code}"), r.status_code,
err.get("code"), r.headers.get("x-request-id"))
return data
# Tools
def list_tools(self, q=None, category=None, limit=None, offset=None):
return self._req("GET", "/api/v1/tools", {"q": q, "category": category, "limit": limit, "offset": offset})
def get_tool(self, slug: str): return self._req("GET", f"/api/v1/tools/{slug}")
def alternatives(self, slug: str): return self._req("GET", f"/api/v1/tools/{slug}/alternatives")
def reviews(self, slug: str, limit=None, offset=None):
return self._req("GET", f"/api/v1/tools/{slug}/reviews", {"limit": limit, "offset": offset})
def benchmarks(self, slug: str): return self._req("GET", f"/api/v1/tools/{slug}/benchmarks")
# Discovery
def categories(self): return self._req("GET", "/api/v1/categories")
def deals(self): return self._req("GET", "/api/v1/deals")
def news(self): return self._req("GET", "/api/v1/news")
# Stack builder
def build_stack(self, task: str, budget: Optional[str] = None, team_size: Optional[int] = None):
payload = {"task": task}
if budget: payload["budget"] = budget
if team_size: payload["team_size"] = team_size
return self._req("POST", "/api/v1/stacks/build", json=payload)
The API is plain REST with Bearer auth. Generate a client in any language from our OpenAPI 3 spec using tools like openapi-generator, oapi-codegen, or orval.
npx @openapitools/openapi-generator-cli generate \
-i https://toolhund.com/openapi.json \
-g go \
-o ./toolhund-go