// 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.

NETWORK: MAINNET — .TEZ TLD

// 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

API error codes
codehttpdescription
INVALID_INPUT400Bad path param (invalid label or address format)
NOT_FOUND404Resource doesn't exist
METHOD_NOT_ALLOWED405Non-GET request
UPSTREAM_ERROR502 / 503TED 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

GET/api/v1/members

Every 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.

Parameters
paramkindtypedefaultdescription
?limitqueryinteger1000 (all)Max members to return (max 1000)
?offsetqueryinteger0Skip this many members, applied after filtering
?sortqueryname | newest | oldestnameAlphabetical, or by registration time (unregistered last)
?statusquerystring—Builder status: building, open-to-collab, available, hiring
?skillquerystring—Exact skill match, case-insensitive
?qquerystring—Substring search over label, name, bio, location, skills, project names + descriptions
?hasProjectsquery1—Only members with at least one project
?projectsquerynone—Omit project bodies for a lighter payload (counts stay accurate)
?tipsquery1—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

GET/api/v1/members/:name

One 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.

Parameters
paramkindtypedefaultdescription
:namepathstring—Label (alice) or full name (alice.hack.tez)
?tipsquery1—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

GET/api/v1/projects

The 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.

Parameters
paramkindtypedefaultdescription
?environmentquerystring—web, tezos, etherlink, tezlink, other
?statusquerystring—Project status: live, wip, archived, open-source
?memberquerystring—Only this member's projects (label or full name)
?qquerystring—Substring search over name, desc, url, repo and member label
?limitqueryinteger1000 (all)Max projects to return (max 1000)
?offsetqueryinteger0Skip 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

GET/api/v1/domains

Paginated 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).

Parameters
paramkindtypedefaultdescription
?limitqueryinteger50Results 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

GET/api/v1/domain/:name

Full 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.

Parameters
paramkindtypedefaultdescription
:namepathstring—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

GET/api/v1/availability/:label

Lightweight availability check — faster than /api/domain when you only need the boolean. Returns 400 if the label fails format validation.

Parameters
paramkindtypedefaultdescription
:labelpathstring—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

GET/api/v1/owner/:address

All hack.tez subdomains owned by a wallet. Returns an empty array (not 404) if the address owns none.

Parameters
paramkindtypedefaultdescription
:addresspathtz1… / 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

GET/api/v1/resolve/:address

Reverse-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).

Parameters
paramkindtypedefaultdescription
:addresspathtz1… / 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 null
hackTez — array of all hack.tez subdomains currently owned by this address

Tezos X Resolve

GET/api/v1/tezosx/:nameOrAddress

Resolve 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.

Parameters
paramkindtypedefaultdescription
:nameOrAddresspathname / 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 yet
balance — mutez on the michelson interface, wei-of-tez (18 decimals) on the evm interface

Contract Config

GET/api/v1/config

Current 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 register
maxCommitAgeSec — commit expires after this; must re-commit
paused — if true, on-chain registration is disabled

Recent Activity

GET/api/v1/activity

Recent 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.

Parameters
paramkindtypedefaultdescription
?limitquerynumber—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

GET/api/v1/profile/:name

Returns 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.

Parameters
paramkindtypedefaultdescription
:namepathstring—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

GET/api/v1/hackatar/:label

Generative 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.

Parameters
paramkindtypedefaultdescription
:labelpathstring—Bare subdomain label (e.g. skllz, not skllz.hack.tez)
?staticquery"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.

Profile key namespace
keysourcedescription
openid:nameTED nativeDisplay name
openid:nicknameTED nativeShort handle / alias
openid:websiteTED nativePersonal or studio site
openid:pictureTED nativeAvatar image URL — ipfs:// or https:// URI. See Avatar Fallback below.
gravatar:hashTED nativeMD5 hash for Gravatar fallback avatar. Second step in the avatar chain.
github:usernameTED nativeGitHub username
twitter:handleTED nativeTwitter/X handle
project:repository_urlTED nativePrimary repo URL
hack:biohack.tezShort bio / tagline (160 chars)
hack:locationhack.tezCity, country, or "anon" (60 chars)
hack:statushack.tezBuilder status (see below)
hack:skillshack.tezJSON string[], max 10 tags
hack:projectshack.tezJSON 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/tezlink
environment — defaults to "web" if omitted
status — defaults to "live" if omitted
subdomain — 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:

Builder status values
valuedescription
"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:

  1. Read the current data map from the domain record
  2. Update only the keys your UI touched
  3. Preserve all other keys byte-for-byte — pass through as-is, without re-encoding
  4. 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.

  1. openid:picture — if set, display this URL (resolve ipfs:// URIs via gateway)
  2. gravatar:hash — if set, construct Gravatar URL from the hash
  3. Hackatar — deterministic generative avatar served from /api/v1/hackatar/:label. Always unique per domain. See Hackatar endpoint.

// Wiki API

List Articles

GET/api/v1/wiki/articles

Paginated list of published wiki articles. Supports filtering by category or tag.

Parameters
paramkindtypedefaultdescription
?categoryquerystring—Filter by category slug
?tagquerystring—Filter by tag slug
?limitqueryinteger50Results per page
?offsetqueryinteger0Pagination 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

GET/api/v1/wiki/articles/:slug

Fetch full content and metadata for a specific article. Returns rendered HTML and raw Markdown.

Parameters
paramkindtypedefaultdescription
:slugpathstring—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
}

Categories

GET/api/v1/wiki/categories

List 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 .

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
Data proxied from Tezos Domains and TzKT. Source on GitHub.