// DEVELOPERS
Build on hack.tez
Public REST API. No key. No auth. Just HTTP. Query subdomain ownership, availability, and contract config for hack.tez programmatically.
// Base URL
https://hacktez.com// Conventions
All responses are JSON. Success includes a data field and a network string.
Errors return { "error": "...", "code": "..." } with a non-200 HTTP status.
Responses are CDN-cached at the edge. The /domains list is additionally cached in Redis with stale-while-revalidate. Data reflects on-chain state with a short delay.
CORS: Access-Control-Allow-Origin: * — safe to call from any origin.
// Rate Limits
No API key required. Edge CDN caching means most requests are served without hitting a function — please don't poll faster than the cache TTL (30–60 seconds).
For high-volume use, query Tezos Domains or TzKT directly.
// Error Codes
| code | http | description |
|---|---|---|
INVALID_INPUT | 400 | Bad path param (invalid label or address format) |
NOT_FOUND | 404 | Resource doesn't exist |
METHOD_NOT_ALLOWED | 405 | Non-GET request |
UPSTREAM_ERROR | 502 / 503 | TED GraphQL or TzKT unreachable |
// Directory
The whole community in one call. Where /domains is the registry view — one row per registration — the directory endpoints are the people view: every member with their complete profile, and every project carrying the slug and canonical page URL the site itself resolves it at. Nothing is truncated and no field is dropped, so you never have to re-derive a slug or issue a second call per member.
All three read one Redis-cached snapshot of the directory (60 s fresh, 10 min hard TTL, serve-stale-while-revalidate) and filter it in memory, so filters cost nothing extra. The X-Cache response header reports HIT, STALE or MISS, and generatedAt tells you when the snapshot was built.
All Members
/api/v1/membersEvery hack.tez member with their full profile — bio, location, status, skills, every social handle, tip jar config, and every project with all of its metadata. Returns all members by default (no paging needed); use limit / offset if you want to page anyway.
| param | kind | type | default | description |
|---|---|---|---|---|
?limit | query | integer | 1000 (all) | Max members to return (max 1000) |
?offset | query | integer | 0 | Skip this many members, applied after filtering |
?sort | query | name | newest | oldest | name | Alphabetical, or by registration time (unregistered last) |
?status | query | string | — | Builder status: building, open-to-collab, available, hiring |
?skill | query | string | — | Exact skill match, case-insensitive |
?q | query | string | — | Substring search over label, name, bio, location, skills, project names + descriptions |
?hasProjects | query | 1 | — | Only members with at least one project |
?projects | query | none | — | Omit project bodies for a lighter payload (counts stay accurate) |
?tips | query | 1 | — | Include chain-verified tip counters as tipCounters |
GET https://hacktez.com/api/v1/members{
"data": [
{
"name": "alice.hack.tez",
"label": "alice",
"owner": "tz1...",
"address": "tz1...",
"registeredAt": "2025-03-27T08:01:29Z",
"opHash": "oo...",
"urls": {
"profile": "https://hacktez.com/u/alice",
"api": "https://hacktez.com/api/v1/members/alice",
"avatar": "https://hacktez.com/api/v1/avatar/alice",
"hackatar": "https://hacktez.com/api/v1/hackatar/alice",
"shareCard": "https://hacktez.com/api/v1/share-card/alice",
"tips": "https://hacktez.com/api/v1/tips/alice"
},
"profile": {
"name": "Alice",
"picture": "ipfs://bafybei...",
"bio": "building on tezos",
"location": "berlin",
"status": "building",
"skills": [
"typescript",
"smartpy"
],
"github": "alice",
"tips": {
"enabled": true,
"amounts": [
"1",
"5",
"10"
]
},
"projects": [
{
"name": "Cold Milk",
"desc": "on-chain generative art",
"url": "https://coldmilk.xyz",
"repo": "https://github.com/alice/coldmilk",
"environment": "tezos",
"address": "KT1...",
"status": "live",
"logo": "ipfs://bafybei...",
"tips": {
"enabled": true,
"amounts": [
"5"
]
},
"slug": "cold-milk",
"urls": {
"page": "https://hacktez.com/u/alice/p/cold-milk"
}
}
]
},
"counts": {
"projects": 1,
"skills": 2
}
}
],
"count": 1,
"total": 1,
"limit": 1000,
"offset": 0,
"network": "mainnet",
"generatedAt": "2025-03-27T08:05:00.000Z"
}Notes. profile is the same object /api/v1/profile/:name returns, with slug and urls added to each project — every other key is byte-for-byte the on-chain value. Keys the member never set are simply absent. count is the size of this page, total the size of the filtered set. Nested subdomains (a.b.hack.tez) belong to a member and are never listed as one. With ?tips=1, tipCounters is null when the counter store is unreachable — distinguishable from a real zero.
Get Member
/api/v1/members/:nameOne member, in exactly the shape the list returns — so code written against /members works unchanged on a single record. Reads through to TED rather than the directory snapshot, so a profile edit shows up immediately. Prefer this over /api/v1/profile/:name when you want project slugs and URLs resolved for you.
| param | kind | type | default | description |
|---|---|---|---|---|
:name | path | string | — | Label (alice) or full name (alice.hack.tez) |
?tips | query | 1 | — | Include chain-verified tip counters as tipCounters |
GET https://hacktez.com/api/v1/members/alice?tips=1{
"data": {
"name": "alice.hack.tez",
"label": "alice",
"owner": "tz1...",
"address": "tz1...",
"registeredAt": "2025-03-27T08:01:29Z",
"opHash": "oo...",
"urls": {
"profile": "https://hacktez.com/u/alice",
"…": "…"
},
"profile": {
"name": "Alice",
"projects": [
"…"
]
},
"counts": {
"projects": 1,
"skills": 2
},
"tipCounters": {
"count": 4,
"totals": [
{
"asset": "tez",
"symbol": "tez",
"total": "21.5"
}
],
"projects": [
{
"slug": "cold-milk",
"count": 2,
"totals": [
{
"asset": "tez",
"symbol": "tez",
"total": "10"
}
]
}
]
}
},
"network": "mainnet"
}Returns 404 NOT_FOUND when the name is not registered.
All Projects
/api/v1/projectsThe same directory pivoted so projects are the rows: every project every member has published, with its full metadata plus an embedded member block. Use it to build an ecosystem showcase without walking members yourself.
| param | kind | type | default | description |
|---|---|---|---|---|
?environment | query | string | — | web, tezos, etherlink, tezlink, other |
?status | query | string | — | Project status: live, wip, archived, open-source |
?member | query | string | — | Only this member's projects (label or full name) |
?q | query | string | — | Substring search over name, desc, url, repo and member label |
?limit | query | integer | 1000 (all) | Max projects to return (max 1000) |
?offset | query | integer | 0 | Skip this many projects, applied after filtering |
GET https://hacktez.com/api/v1/projects?environment=tezos&status=live{
"data": [
{
"name": "Cold Milk",
"desc": "on-chain generative art",
"url": "https://coldmilk.xyz",
"repo": "https://github.com/alice/coldmilk",
"environment": "tezos",
"address": "KT1...",
"status": "live",
"logo": "ipfs://bafybei...",
"tips": {
"enabled": true,
"amounts": [
"5"
]
},
"slug": "cold-milk",
"urls": {
"page": "https://hacktez.com/u/alice/p/cold-milk"
},
"member": {
"name": "alice.hack.tez",
"label": "alice",
"address": "tz1...",
"owner": "tz1...",
"displayName": "Alice",
"picture": "ipfs://bafybei...",
"urls": {
"profile": "https://hacktez.com/u/alice",
"api": "https://hacktez.com/api/v1/members/alice",
"avatar": "https://hacktez.com/api/v1/avatar/alice"
}
}
}
],
"count": 1,
"total": 1,
"limit": 1000,
"offset": 0,
"network": "mainnet",
"generatedAt": "2025-03-27T08:05:00.000Z"
}Slugs. slug is derived from the project name — lowercased, runs of non-alphanumerics collapsed to a single dash, trimmed to 60 characters. It is unique per member only if their project names are: two projects that slugify identically share a URL, and /u/:label/p/:slug resolves to the first. Key on member.label + slug, not on slug alone.
// Endpoints
List Registrations
/api/v1/domainsPaginated list of all hack.tez registrations with inline profile data. Backed by TED GraphQL and TzKT — includes profile, registration timestamp, and op hash. Served from Redis cache when warm (~5 ms).
| param | kind | type | default | description |
|---|---|---|---|---|
?limit | query | integer | 50 | Results per page (max 1000) |
GET https://hacktez.com/api/v1/domains?limit=3{
"data": [
{
"name": "alice.hack.tez",
"label": "alice",
"owner": "tz1...",
"address": "tz1...",
"registeredAt": "2025-03-27T08:01:29Z",
"opHash": "oo...",
"profile": {
"name": "Alice",
"bio": "building on tezos",
"status": "building",
"skills": [
"typescript",
"smartpy"
]
}
}
],
"count": 1,
"limit": 3,
"network": "mainnet"
}Get Domain Record
/api/v1/domain/:nameFull TED record for a subdomain. Accepts a bare label (alice) or the full name (alice.hack.tez). Returns data: null with available: true if unclaimed.
| param | kind | type | default | description |
|---|---|---|---|---|
:name | path | string | — | Label (alice) or full name (alice.hack.tez) |
GET https://hacktez.com/api/v1/domain/alice{
"data": {
"name": "alice.hack.tez",
"label": "alice",
"address": "tz1...",
"owner": "tz1..."
},
"available": false,
"network": "mainnet"
}Check Availability
/api/v1/availability/:labelLightweight availability check — faster than /api/domain when you only need the boolean. Returns 400 if the label fails format validation.
| param | kind | type | default | description |
|---|---|---|---|---|
:label | path | string | — | Bare label (3–63 chars, lowercase alphanumeric + hyphens) |
GET https://hacktez.com/api/v1/availability/alice{
"label": "alice",
"available": false,
"network": "mainnet"
}Domains by Owner
/api/v1/owner/:addressAll hack.tez subdomains owned by a wallet. Returns an empty array (not 404) if the address owns none.
| param | kind | type | default | description |
|---|---|---|---|---|
:address | path | tz1… / KT1… | — | Tezos wallet or contract address |
GET https://hacktez.com/api/v1/owner/tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb{
"data": [
{
"name": "alice.hack.tez",
"label": "alice",
"address": "tz1...",
"owner": "tz1..."
}
],
"count": 1,
"network": "mainnet"
}Reverse Resolve
/api/v1/resolve/:addressReverse-resolve a wallet to its primary domain and all owned hack.tez subdomains. primary is the TED reverse record if set, otherwise the first owned hack.tez subdomain, otherwise null. hackTez is an array of all hack.tez subdomains currently owned by the address (they're NFTs and transferable).
| param | kind | type | default | description |
|---|---|---|---|---|
:address | path | tz1… / KT1… | — | Tezos wallet or contract address |
GET https://hacktez.com/api/v1/resolve/tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb{
"address": "tz1...",
"primary": "alice.tez",
"hackTez": [
"alice.hack.tez",
"builder.hack.tez"
],
"network": "mainnet"
}primary — TED reverse record if set, else first owned hack.tez subdomain, else nullhackTez — array of all hack.tez subdomains currently owned by this address
Tezos X Resolve
/api/v1/tezosx/:nameOrAddressResolve a hack.tez name or any address to its identity on Tezos X previewnet: the native address on one interface plus its deterministic alias on the other, with live chain state per address. For names, a declared etherlink:address record takes precedence over the derived alias (evmSource tells you which you got). Also available interactively as the X-Ray lab.
| param | kind | type | default | description |
|---|---|---|---|---|
:nameOrAddress | path | name / tz1… / KT1… / 0x… | — | hack.tez name (label or full) or any Tezos / EVM address |
GET https://hacktez.com/api/v1/tezosx/alice{
"data": {
"input": "alice",
"name": "alice.hack.tez",
"tz": "tz1...",
"evm": "0x16142132dd616dd8f61b8972ae4b9fcf8a22a450",
"evmSource": "derived",
"kt1Alias": null,
"corners": [
{
"role": "native",
"address": "tz1...",
"interface": "michelson",
"materialized": true,
"balance": "1250000"
},
{
"role": "alias",
"address": "0x1614...a450",
"interface": "evm",
"materialized": false,
"balance": "0"
}
],
"cornersError": null
},
"network": "tezosx-previewnet"
}evmSource — "declared" (TED record), "derived" (computed alias), or "native" (input was an EVM address)materialized — the account exists on chain; a derived-only alias has no account yetbalance — mutez on the michelson interface, wei-of-tez (18 decimals) on the evm interface
Contract Config
/api/v1/configCurrent contract configuration. Check before starting a registration flow to get commit timing and verify registration is not paused.
GET https://hacktez.com/api/v1/config{
"data": {
"minCommitAgeSec": 30,
"maxCommitAgeSec": 86400,
"maxPerWallet": 1,
"paused": false,
"registrarAddress": "KT1..."
},
"network": "mainnet"
}minCommitAgeSec — wait at least this long between commit and registermaxCommitAgeSec — commit expires after this; must re-commitpaused — if true, on-chain registration is disabled
Recent Activity
/api/v1/activityRecent on-chain activity — both claimed (register) and committed events, merged and sorted by time. Commit events have name: null since the commitment hash is not recoverable off-chain.
| param | kind | type | default | description |
|---|---|---|---|---|
?limit | query | number | — | Max events to return (default 30, max 100) |
GET https://hacktez.com/api/v1/activity?limit=30{
"data": [
{
"type": "claimed",
"address": "tz1...",
"name": "alice.hack.tez",
"timestamp": "2025-01-01T00:00:00Z",
"opHash": "op..."
},
{
"type": "committed",
"address": "tz1...",
"name": null,
"timestamp": "2025-01-01T00:00:00Z",
"opHash": "op..."
}
],
"count": 2,
"limit": 30,
"network": "mainnet"
}// Profiles & Identity
Get Profile
/api/v1/profile/:nameReturns the parsed builder profile for a hack.tez subdomain. Accepts a bare label (alice) or full name (alice.hack.tez). Returns profile: {} if the domain exists but has no hack: data set.
| param | kind | type | default | description |
|---|---|---|---|---|
:name | path | string | — | Label (alice) or full name (alice.hack.tez) |
GET https://hacktez.com/api/v1/profile/alice{
"data": {
"name": "alice.hack.tez",
"owner": "tz1...",
"address": "tz1...",
"profile": {
"name": "alice",
"picture": "ipfs://bafybei...",
"bio": "building tezos tooling",
"github": "alice",
"twitter": "alice",
"website": "https://alice.xyz",
"location": "Berlin",
"status": "building",
"skills": [
"SmartPy",
"TypeScript"
],
"projects": [
{
"name": "my-dapp",
"url": "https://my-dapp.xyz",
"desc": "a decentralized app"
}
]
},
"registrationHash": "op...",
"registeredAt": "2025-03-27T08:01:29Z"
},
"network": "mainnet"
}Returns 404 if the domain doesn't exist. Returns 200 with profile: {} if the domain exists but has no hack: data.
Hackatar
/api/v1/hackatar/:labelGenerative avatar for a hack.tez subdomain. Deterministically generated from a salted domain name — same domain always produces the same hackatar. Returns a GIF image (not JSON). Cached immutably after first generation.
| param | kind | type | default | description |
|---|---|---|---|---|
:label | path | string | — | Bare subdomain label (e.g. skllz, not skllz.hack.tez) |
?static | query | "1" | — | If set, returns a single-frame still instead of animated loop |
GET https://hacktez.com/api/v1/hackatar/skllz→ image/gif (binary). Animated: 30 frames at 80ms (2.4s loop), 192×192px. Static: single frame.Cache-Control: public, max-age=31536000, immutable
Usage in HTML:
<img src="https://hacktez.com/api/v1/hackatar/skllz" alt="hackatar" />Add ?static=1 for a single-frame still. Useful for grid views, chat avatars, or anywhere animation is unwanted.
Returns 400 for invalid labels, 404 if the domain is not registered.
// Profile Spec
Key Namespace
Every hack.tez domain stores profile data in the TED record's on-chain data map. We use TED's canonical keys where they exist. The hack: prefix is reserved for fields TED doesn't define — data set via the official TED app is automatically visible in hack.tez profiles, and vice versa.
| key | source | description |
|---|---|---|
openid:name | TED native | Display name |
openid:nickname | TED native | Short handle / alias |
openid:website | TED native | Personal or studio site |
openid:picture | TED native | Avatar image URL — ipfs:// or https:// URI. See Avatar Fallback below. |
gravatar:hash | TED native | MD5 hash for Gravatar fallback avatar. Second step in the avatar chain. |
github:username | TED native | GitHub username |
twitter:handle | TED native | Twitter/X handle |
project:repository_url | TED native | Primary repo URL |
hack:bio | hack.tez | Short bio / tagline (160 chars) |
hack:location | hack.tez | City, country, or "anon" (60 chars) |
hack:status | hack.tez | Builder status (see below) |
hack:skills | hack.tez | JSON string[], max 10 tags |
hack:projects | hack.tez | JSON ProjectEntry[], see below |
ProjectEntry Schema
Projects are first-class. The hack:projects key stores a JSON array of ProjectEntry objects. Only name and desc are required — everything else is optional to keep the barrier low.
interface ProjectEntry {
// Required
name: string; // project name, max 60 chars
desc: string; // one-line description, max 120 chars
// At least one recommended
url?: string; // live site / app URL
repo?: string; // source repo URL
// Where it lives
environment?: "web" | "tezos" | "etherlink" | "tezlink" | "other";
address?: string; // contract or account address
// Sub-subdomain reference
subdomain?: string; // label only, no dots (e.g. "myproject")
// Project state
status?: "live" | "wip" | "archived" | "open-source";
// Media
logo?: string; // image URL or ipfs:// URI (square icon)
}address — interpreted by environment: KT1… for tezos, 0x… for etherlink/tezlinkenvironment — defaults to "web" if omittedstatus — defaults to "live" if omittedsubdomain — references a sub-subdomain under your domain (e.g. myproject → myproject.name.hack.tez)
Builder Status
The hack:status key accepts one of these self-reported values:
| value | description |
|---|---|
"building" | Actively working on something |
"open-to-collab" | Looking for collaborators |
"available" | Open for work or projects |
"hiring" | Looking to hire builders |
Encoding Rules
hack:* keys — stored as JSON-encoded strings. Scalars are plain JSON string values; arrays (hack:skills, hack:projects) are JSON.stringify() of the array. When reading: parse as JSON. When writing: JSON.stringify(value) → bytes.
TED native keys — these are owned by the TED app. When reading, values come back as plain strings. When writing, our editor preserves them as-is from the existing record. Do not modify TED native keys unless you know their encoding.
⚠ Critical: Never re-encode TED native values through JSON.stringify(). TED native keys may be stored as raw bytes or in a different encoding. If you read a TED native value and write it back through JSON.stringify(), you corrupt it. Only write keys your editor owns.
# hack:* keys — JSON-encoded
hack:bio → "building tezos tooling" (JSON string literal)
hack:skills → ["SmartPy","TypeScript"] (JSON array)
hack:projects → [{"name":"...","desc":"..."}] (JSON array of objects)
# TED native keys — preserved from existing record
github:username → "alice" (JSON-encoded string)
twitter:handle → "alice" (JSON-encoded string)Safe Merge Rule
When updating a profile, always follow this merge strategy to avoid corrupting data written by other apps:
- Read the current
datamap from the domain record - Update only the keys your UI touched
- Preserve all other keys byte-for-byte — pass through as-is, without re-encoding
- Delete keys whose new value is empty string or null
Avatar Fallback Chain
Avatars resolve through a three-step fallback chain. Every profile always has a visual identity, even if the user never uploads an image.
openid:picture— if set, display this URL (resolveipfs://URIs via gateway)gravatar:hash— if set, construct Gravatar URL from the hash- Hackatar — deterministic generative avatar served from
/api/v1/hackatar/:label. Always unique per domain. See Hackatar endpoint.
// Wiki API
List Articles
/api/v1/wiki/articlesPaginated list of published wiki articles. Supports filtering by category or tag.
| param | kind | type | default | description |
|---|---|---|---|---|
?category | query | string | — | Filter by category slug |
?tag | query | string | — | Filter by tag slug |
?limit | query | integer | 50 | Results per page |
?offset | query | integer | 0 | Pagination offset |
GET https://hacktez.com/api/v1/wiki/articles?limit=3{
"articles": [
{
"slug": "getting-started",
"title": "Getting Started with Tezos",
"summary": "A brief intro...",
"author": "tz1...",
"lastEditor": "tz1...",
"category": {
"slug": "guides",
"name": "Guides"
},
"createdAt": "2025-04-01T12:00:00Z",
"updatedAt": "2025-04-02T12:00:00Z",
"revision": 2
}
],
"total": 1,
"limit": 3,
"offset": 0
}Get Article
/api/v1/wiki/articles/:slugFetch full content and metadata for a specific article. Returns rendered HTML and raw Markdown.
| param | kind | type | default | description |
|---|---|---|---|---|
:slug | path | string | — | Article URL slug |
GET https://hacktez.com/api/v1/wiki/articles/getting-started{
"slug": "getting-started",
"title": "Getting Started with Tezos",
"summary": "A brief intro...",
"content": "<p>Welcome...</p>",
"markdown": "Welcome...",
"author": "tz1...",
"lastEditor": "tz1...",
"category": {
"slug": "guides",
"name": "Guides"
},
"tags": [
{
"slug": "beginner",
"name": "Beginner"
}
],
"status": "published",
"createdAt": "2025-04-01T12:00:00Z",
"updatedAt": "2025-04-02T12:00:00Z",
"revision": 2,
"lockedBy": null,
"lockReason": null,
"lockExpires": null
}Search Articles
/api/v1/wiki/searchFull-text search across article titles, summaries, and markdown content using FTS5.
| param | kind | type | default | description |
|---|---|---|---|---|
?q | query | string | — | Search query |
?limit | query | integer | 20 | Max results |
GET https://hacktez.com/api/v1/wiki/search?q=smartpy{
"query": "smartpy",
"results": [
{
"slug": "smartpy-basics",
"title": "SmartPy Basics",
"summary": "Learn SmartPy...",
"excerpt": "...write a <b>SmartPy</b> contract...",
"author": "tz1...",
"updatedAt": "2025-04-01T12:00:00Z"
}
]
}Categories
/api/v1/wiki/categoriesList all wiki categories and their published article counts.
GET https://hacktez.com/api/v1/wiki/categories{
"categories": [
{
"id": "cat_123",
"slug": "guides",
"name": "Guides",
"description": "Tutorials and guides",
"parentId": null,
"articleCount": 15
}
]
}// Chat
Overview
hackchat is currently retired — our server host pulled support for its infrastructure. The /chat route now shows a status notice while we rethink the communication layer.
Your domain name is your chat identity — not your wallet address. Transfer the domain and the new owner inherits the chat identity. Wallets with multiple domains get an identity selector.
Rooms: global chat room + direct messages. All messages persist. Domain ownership is re-verified every 15 minutes — lose the domain, lose access.
Backend: Cloudflare Worker (auth API) + PartyKit (WebSocket rooms) + D1 (SQLite persistence). The chat backend is self-contained in the chat/ directory.
Authentication
Auth endpoint: POST /auth on the chat worker. The client signs a challenge message with the connected wallet (no on-chain transaction). The worker verifies the signature and queries TED for domain ownership. No domains = 403 rejected. Domains found = JWT issued.
Challenge format: hack.tez-chat:{unix_timestamp}:{16_byte_hex_nonce}. The signature is Micheline-encoded (0501 + length prefix + UTF-8 bytes). JWT is valid for 1 hour and contains the wallet address + owned domains array.
// POST /auth — request body
{
"address": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
"publicKey": "edpkvGfY...",
"signature": "edsigt...",
"timestamp": 1719500000,
"nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890"
}
// 200 OK — response
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"domains": ["alice.hack.gho", "bob.hack.gho"],
"activeDomain": "alice.hack.gho"
}WebSocket Protocol
Connect to PartyKit with the JWT as a query parameter. Global room: wss://{PARTYKIT_HOST}/party/main?room=global&token=.... DM room: wss://{PARTYKIT_HOST}/party/dm?room=dm:alice.hack.gho+bob.hack.gho&token=.... PartyKit verifies the JWT before accepting the connection.
Client → Server messages:
{ type: "message", content: "..." }— send a message{ type: "typing", active: true/false }— typing indicator{ type: "history", before?: "ISO8601" }— load older messages{ type: "switch-identity", domain: "..." }— switch active domain{ type: "read" }— mark messages as read (DM only)
Server → Client events:
{ type: "message", id, sender, content, timestamp }— new message{ type: "presence", domain, status: "online"|"offline" }— user status{ type: "typing", domain, active }— someone typing{ type: "history", messages: [...], hasMore }— history page{ type: "system", content, timestamp }— system notice{ type: "error", code, message }— error
// Example: connect and send a message
ws = new WebSocket("wss://<PARTYKIT_HOST>/party/main?room=global&token=eyJ...")
// Send
ws.send(JSON.stringify({ type: "message", content: "gm hackers" }))
// Receive
{
"type": "message",
"id": "msg_01J...",
"sender": "alice.hack.gho",
"content": "gm hackers",
"timestamp": "2025-01-15T12:00:00.000Z"
}// Hackcade
Hackcade is the native arcade platform for hack.tez. Builders submit their HTML/JS/CSS game bundles which are hosted on Netlify Blobs and served in sandboxed iframes. Players compete for high scores on a shared Neon Postgres-backed leaderboard, with identities mapped strictly to their `.hack.tez` subdomains.
The lobby is at /arcade. Full SDK reference, postMessage protocol, anti-cheat constraints, and a worked example live in the Hackcade SDK skill doc.
SDK + Template
The SDK is auto-injected into your bundle on submission, but you can grab it (and a starter template) directly from the repo:
# scaffold a new game
mkdir my-cool-game && cd my-cool-game
for f in index.html style.css game.js; do
curl -O "https://raw.githubusercontent.com/skullzarmy/hack-tez/main/hackcade/template/$f"
done
curl -O https://raw.githubusercontent.com/skullzarmy/hack-tez/main/hackcade/sdk/hackcade-sdk.js
# build, then zip (index.html must be at the root)
zip -r ../my-cool-game.zip .- SDK source: hackcade/sdk
- Template: hackcade/template
- Builder docs: hackcade/README.md
REST Endpoints
Base path: /api/v1/arcade. JWT-gated routes use the same auth layer as chat (see Chat → Authentication).
# Public reads
GET /api/v1/arcade/games # active games
GET /api/v1/arcade/games/:slug # game detail + mini leaderboard
GET /api/v1/arcade/leaderboard/:slug # top 100 (best per player)
GET /api/v1/arcade/recent # recent plays
GET /api/v1/arcade/player/:domain # player stats
# Authenticated (JWT — domain holders)
POST /api/v1/arcade/submit # multipart zip upload
POST /api/v1/arcade/games/:slug/edit # edit metadata; pending allows zip swap
POST /api/v1/arcade/games/:slug/rescind # creator deletes own pending submission
POST /api/v1/arcade/games/:slug/update # new pending version (active games)
POST /api/v1/arcade/games/:slug/flag # community flag
POST /api/v1/arcade/session # start a play session
POST /api/v1/arcade/score # submit final score
GET /api/v1/arcade/my-games # caller's submissions
# Admin-only (admin.hack.tez)
GET /api/v1/arcade/pending # pending new games
GET /api/v1/arcade/pending-updates # pending version updates
GET /api/v1/arcade/flagged # flagged games
POST /api/v1/arcade/games/:slug/approve
POST /api/v1/arcade/games/:slug/reject
POST /api/v1/arcade/games/:slug/remove
POST /api/v1/arcade/games/:slug/approve-update// Quick Start
# Check availability
curl https://hacktez.com/api/v1/availability/yourname
# Fetch domain record
curl https://hacktez.com/api/v1/domain/alice
# Domains owned by a wallet
curl https://hacktez.com/api/v1/owner/tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb
# Reverse-resolve an address
curl https://hacktez.com/api/v1/resolve/tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb// JavaScript / TypeScript
const { available } = await fetch('https://hacktez.com/api/v1/availability/yourname')
.then(r => r.json());
// Resolve address for display
async function getDisplayName(address) {
const { primary, hackTez } = await fetch(`https://hacktez.com/api/v1/resolve/${address}`)
.then(r => r.json());
return primary ?? hackTez[0] ?? `${address.slice(0,6)}…${address.slice(-4)}`;
}// LLM Skill
Building an AI agent or LLM-powered tool that interacts with hack.tez? Drop the skill reference into your context window — it documents the full API, contract addresses, commit-reveal flow, and TypeScript patterns in a single compact file.
hack-tez-api.md