Skip to content
HACKTEZ
<< a FAFOlab joint >>Bluesky @hacktez.comhack.tez is open source and unlicensed — public domain. copy it. use it. github.com/skullzarmy/hack-tez
skills

hack.tez API Reference

REST API and smart contract reference for the hack.tez free Tezos subdomain registrar. Covers all endpoints, registration flow, label validation, and error handling.

hack-tez-api.md

Skill: hack.tez API & Contract Reference

skill_type: hybrid
domain: Tezos / hack.tez subdomain registrar
version: 1.0
api_base: https://hacktez.com
network_default: ghostnet

Overview

hack.tez is a free Tezos subdomain registrar. Anyone can claim name.hack.tez (mainnet) or name.hack.gho (ghostnet) at no cost. Claimed subdomains are real Tezos Domains (TED) on-chain records — the owner has full TED ownership and can set addresses and manage their record.

Key properties:

  • 1 subdomain per wallet (permanent — claim slot is spent even if TED record is later removed)
  • 2-step commit-reveal registration (prevents front-running)
  • Owner = the registering wallet (not a custodian)
  • No API key required for the public REST API
  • CORS open (Access-Control-Allow-Origin: *)

Public REST API

Base URL: https://hacktez.com Local dev: http://localhost:8888 Response envelope (success): { "data": ..., "network": "ghostnet" | "mainnet" } Response envelope (error): { "error": "...", "code": "INVALID_INPUT" | "UPSTREAM_ERROR" | "METHOD_NOT_ALLOWED" } Cache: responses are CDN-cached at the edge; the /domains list is additionally cached in Upstash Redis with serve-stale-while-revalidate (60 s fresh, 10 min hard TTL)


GET /api/v1/domains

Paginated list of all hack.tez registrations. Backed by TED GraphQL (domain + profile data) and TzKT (registration timestamps and operation hashes). Response is served from Redis cache when warm (~5 ms) with background revalidation.

Query parameters:

ParamTypeDefaultMaxDescription
limitinteger501000Number of results

Response:

{
    "data": [
        {
            "name": "skllz.hack.tez",
            "label": "skllz",
            "owner": "tz1Qi77tcJn9foeHHP1QHj6UX1m1vLVLMbuY",
            "address": "tz1Qi77tcJn9foeHHP1QHj6UX1m1vLVLMbuY",
            "registeredAt": "2025-03-27T08:01:29Z",
            "opHash": "oo...",
            "profile": {
                "name": "skllz",
                "bio": "building hack.tez",
                "status": "building",
                "skills": ["typescript", "smartpy"],
                "picture": "ipfs://bafybei...",
                "github": "skullzarmy",
                "twitter": "skaborern"
            }
        }
    ],
    "count": 1,
    "limit": 50,
    "network": "mainnet"
}

Usage:

// Fetch up to 50 registrations (includes profile data)
const res = await fetch("https://hacktez.com/api/v1/domains?limit=50");
const { data, count } = await res.json();

// Each item has: name, label, owner, address, registeredAt, opHash, profile
data.forEach((d) => console.log(d.label, d.profile.status));

Cache: s-maxage=120, stale-while-revalidate=300 + Redis SWR (60 s fresh / 10 min hard TTL)


GET /api/v1/domain/:name

Fetch the TED domain record for a hack.tez subdomain. Accepts either the bare label (alice) or the full name (alice.hack.tez / alice.hack.gho).

Returns { data: null, available: true } if the domain is not registered.

Response (registered):

{
    "data": {
        "name": "alice.hack.tez",
        "label": "alice",
        "address": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
        "owner": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb"
    },
    "available": false,
    "network": "mainnet"
}

Response (not registered):

{ "data": null, "available": true, "network": "mainnet" }

Usage:

const res = await fetch("https://hacktez.com/api/v1/domain/alice");
const { data, available } = await res.json();

if (available) {
    console.log("alice.hack.tez is free to register");
} else {
    console.log("Owned by:", data.owner);
}

GET /api/v1/availability/:label

Lightweight availability check. Returns only the available boolean — faster than /api/domain when you don't need the full record.

Response:

{ "label": "alice", "available": false, "network": "mainnet" }

Validation errors (400):

  • Label shorter than 3 characters
  • Label longer than 63 characters
  • Label contains characters other than lowercase [a-z0-9-]

Usage:

const { available } = await fetch("https://hacktez.com/api/v1/availability/myname").then((r) => r.json());

GET /api/v1/owner/:address

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

Path parameter: Any valid tz1..., tz2..., tz3..., or KT1... address.

Response:

{
    "data": [
        {
            "name": "alice.hack.tez",
            "label": "alice",
            "address": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
            "owner": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb"
        }
    ],
    "count": 1,
    "network": "mainnet"
}

Usage:

const { data } = await fetch("https://hacktez.com/api/v1/owner/tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb").then((r) =>
    r.json(),
);

const hasHackTez = data.length > 0;

GET /api/v1/resolve/:address

Reverse-resolve a Tezos address to its primary domain and all owned hack.tez subdomains. primary is the TED reverse record — what the user explicitly set. hackTez is an array of all hack.tez subdomains currently owned (they are NFTs and transferable, so ownership can change).

Response:

{
    "address": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
    "primary": "alice.tez",
    "hackTez": ["alice.hack.tez", "builder.hack.tez"],
    "network": "mainnet"
}

Fields:

  • 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. Empty array if none.

Usage:

const { primary, hackTez } = await fetch(`https://hacktez.com/api/v1/resolve/${walletAddress}`).then((r) =>
    r.json(),
);

const displayName = primary ?? hackTez[0] ?? walletAddress.slice(0, 8) + "…";

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 for each. See the tezos-x skill for the underlying alias math and network reference.

Accepts a hack.tez name (label or full form), a Tezos address (tz1/tz2/tz3/KT1), or an EVM address (0x).

Resolution precedence for a name's EVM address: a declared TED etherlink:address record wins; otherwise the deterministic Tezos X alias derived from the resolved tz address. The evmSource field reports which one you got.

Response:

{
    "data": {
        "input": "alice",
        "name": "alice.hack.tez",
        "tz": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
        "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"
}

Fields:

  • evmSource — "declared" (explicit TED etherlink:address record), "derived" (computed Tezos X alias), or "native" (the input itself was an EVM address).
  • kt1Alias — the Michelson-side alias, only set when the input is a native EVM address.
  • corners[].materialized — the account exists on chain. A derived-only alias is a valid destination but has no account yet.
  • corners[].balance — raw units per interface: mutez (6 decimals) on michelson, wei-of-tez (18 decimals) on evm.
  • cornersError — set when previewnet was unreachable; derivation fields are still valid.

Usage:

const { data } = await fetch(`https://hacktez.com/api/v1/tezosx/${name}`).then((r) => r.json());
console.log(`${data.name} on Tezos X:`, data.tz, "→", data.evm, `(${data.evmSource})`);

Interactive version: the X-Ray lab at https://hacktez.com/labs/x-ray.


GET /api/v1/config

Current contract configuration. Use this before starting registration to get commit timing requirements and check if registration is paused.

Response:

{
    "data": {
        "minCommitAgeSec": 30,
        "maxCommitAgeSec": 86400,
        "maxPerWallet": 1,
        "paused": false,
        "registrarAddress": "KT1X2ZbjZBaeRnnkzLyaZ3FtGp7wKuaimbzg"
    },
    "network": "ghostnet"
}

Fields:

  • minCommitAgeSec — must wait at least this many seconds between commit and register
  • maxCommitAgeSec — commit expires after this many seconds (must re-commit if expired)
  • maxPerWallet — max subdomains per wallet (currently 1)
  • paused — if true, registration is disabled on-chain

GET /api/v1/activity

Recent on-chain claim and commit events, merged and sorted by time. Commit events have name: null since the hash is not recoverable off-chain. Used by the activity feed on the site.

Query params: limit (default 30, max 100)

Response:

{
    "data": [
        {
            "type": "claimed",
            "address": "tz1...",
            "name": "alice.hack.gho",
            "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": "ghostnet"
}

Fields:

  • type — "claimed" (register entrypoint applied) or "committed" (commit entrypoint applied)
  • name — full domain name for claims, null for commits
  • opHash — deduplication key

Cache: s-maxage=20, stale-while-revalidate=40


GET /api/v1/hackatar/:label

Returns a generative avatar GIF for the given hack.tez subdomain label. The image is deterministically generated from a salted domain name — same domain always produces the same hackatar. Cached immutably in Netlify Blobs after first generation.

Path parameters:

ParamTypeDescription
labelstringThe subdomain label (e.g. skllz, not skllz.hack.tez)

Query parameters:

ParamTypeDefaultDescription
static"1"—If set, returns a single-frame still image

Response: Binary GIF image (Content-Type: image/gif).

  • Animated (default): 30 frames at 80ms = 2.4s seamless loop, 192×192 pixels
  • Static (?static=1): Single-frame GIF, 192×192 pixels
  • Cache-Control: public, max-age=31536000, immutable

Example:

GET https://hacktez.com/api/v1/hackatar/skllz
→ animated GIF (binary)

GET https://hacktez.com/api/v1/hackatar/skllz?static=1
→ single-frame GIF (binary)

Errors:

StatusCodeReason
400INVALID_INPUTLabel fails validation
404NOT_FOUNDDomain not registered
502UPSTREAM_ERRORServer-side rendering error

How it works:

  1. Validates the label
  2. Checks Netlify Blobs cache — serves immediately if cached
  3. Verifies domain is registered via TED GraphQL
  4. Seeds the hackatar engine with a salted domain name
  5. Generates both animated + static GIFs, caches both
  6. Returns the requested variant

Usage in HTML/Markdown:

<img src="https://hacktez.com/api/v1/hackatar/skllz" alt="skllz hackatar" />

GET /api/v1/profile/:name

Returns the parsed builder profile for a hack.tez subdomain. Accepts a bare label (alice) or full domain name (alice.hack.tez). Returns profile: {} if the domain exists but has no hack: data keys.

Response:

{
    "data": {
        "name": "alice.hack.tez",
        "owner": "tz1...",
        "address": "tz1...",
        "profile": {
            "name": "Alice",
            "picture": "ipfs://bafybei...",
            "bio": "building on tezos",
            "status": "building",
            "skills": ["typescript", "smartpy"],
            "projects": [...]
        },
        "registrationHash": "op...",
        "registeredAt": "2025-03-27T08:01:29Z"
    },
    "network": "ghostnet"
}

Notes:

  • profile uses the picture field (mapped from openid:picture), not avatar
  • registrationHash / registeredAt may be null for preloaded domains
  • Returns profile: {} if the domain exists but has no hack: data keys

Errors:

StatusCodeReason
400INVALID_INPUTInvalid domain name
404—Domain not registered (returns { "error": "not found" })

Registration Flow (Contract Interaction)

The registrar uses a commit-reveal scheme to prevent front-running.

Step 1: Compute commitment hash

The commitment is a blake2b-256 hash of the packed Michelson bytes of (label, nonce, sender_address).

import { blake2b } from "blakejs";
import { encodePacked } from "@taquito/utils"; // or manual Michelson packing

// label: string (e.g. "alice")
// nonce: random 32-byte hex string
// sender: tz1... address of the registering wallet

function computeCommitment(label: string, nonce: string, sender: string): string {
    // Pack as Michelson pair(pair(bytes(label), bytes(nonce)), address(sender))
    // See src/lib/commitment.ts for the canonical implementation
    const packed = packCommitmentMichelson(label, nonce, sender);
    const hash = blake2b(packed, undefined, 32);
    return Buffer.from(hash).toString("hex");
}

Important: The commitment hash MUST match what the on-chain contract computes. Use the canonical implementation in src/lib/commitment.ts — do not re-implement.

Step 2: Submit commit transaction

// Via DAppClient (Beacon) — raw Michelson operation
await dAppClient.requestOperation({
    operationDetails: [
        {
            kind: TezosOperationType.TRANSACTION,
            destination: REGISTRAR_ADDRESS,
            amount: "0",
            parameters: {
                entrypoint: "commit",
                value: { string: commitmentHex }, // bytes in Michelson
            },
        },
    ],
});

// Store pending commit locally
localStorage.setItem(
    "hack-tez-pending-commits",
    JSON.stringify({
        label,
        nonce,
        commitHash,
        timestamp: Date.now(),
    }),
);

Step 3: Wait for commit age

const config = await fetch("https://hacktez.com/api/v1/config").then((r) => r.json());
const waitMs = config.data.minCommitAgeSec * 1000;

// Check /api/config for current values — don't hardcode
await new Promise((resolve) => setTimeout(resolve, waitMs));

Step 4: Submit register transaction

await dAppClient.requestOperation({
    operationDetails: [
        {
            kind: TezosOperationType.TRANSACTION,
            destination: REGISTRAR_ADDRESS,
            amount: "0",
            parameters: {
                entrypoint: "register",
                value: {
                    prim: "Pair",
                    args: [
                        { string: labelHex }, // label as Michelson bytes
                        { string: nonceHex }, // nonce as Michelson bytes
                        // optional: address to resolve to, optional data map
                    ],
                },
            },
        },
    ],
});

See src/lib/contract.ts for the canonical submitCommit() and submitRegister() implementations.


Label Validation Rules

Before calling any API or contract endpoint, validate the label client-side:

function validateLabel(label: string): { valid: boolean; error?: string } {
    if (label.length < 3) return { valid: false, error: "Min 3 characters" };
    if (label.length > 63) return { valid: false, error: "Max 63 characters" };
    if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(label)) {
        return { valid: false, error: "Lowercase alphanumeric and hyphens only" };
    }
    return { valid: true };
}

Reserved labels (cannot be registered — enforced on-chain): admin, support, help, www, mail, ftp, api, app, ns1, ns2, dns, mx, smtp, imap, pop, ssh, hack, tez, tezos, test, dev, staging, prod, official, bot, system, root, null, undefined


Network Configuration

PropertyGhostnetMainnet
TLD.gho.tez
Full domain formatlabel.hack.gholabel.hack.tez
RegistrarKT1X2ZbjZBaeRnnkzLyaZ3FtGp7wKuaimbzgKT1UKAt5ioGdbKb435ziP25FRDzqgC7BUeB4
TED GraphQLhttps://ghostnet-api.tezos.domains/graphqlhttps://api.tezos.domains/graphql
TzKT APIhttps://api.ghostnet.tzkt.iohttps://api.tzkt.io
RPChttps://rpc.ghostnet.teztnets.comhttps://mainnet.tezos.marigold.dev

Network is selected via VITE_TEZOS_NETWORK env var (default: ghostnet).


Error Handling

CodeHTTPWhen
INVALID_INPUT400Bad label (too short/long/invalid chars), invalid Tezos address format
METHOD_NOT_ALLOWED405Non-GET request
UPSTREAM_ERROR502/503TED GraphQL or TzKT unreachable, or registrar address not configured

All errors include a human-readable error string alongside the code.


Common Patterns

Check availability then show registration UI

async function checkAndShow(label: string) {
    const [validationResult, configResult] = await Promise.all([
        fetch(`https://hacktez.com/api/v1/availability/${label}`).then((r) => r.json()),
        fetch("https://hacktez.com/api/v1/config").then((r) => r.json()),
    ]);

    if (configResult.data.paused) return showPausedMessage();
    if (!validationResult.available) return showTakenMessage();

    showRegistrationForm({ minCommitAgeSec: configResult.data.minCommitAgeSec });
}

Resolve address for display

async function getDisplayName(address: string): Promise<string> {
    try {
        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)}`;
    } catch {
        return `${address.slice(0, 6)}…${address.slice(-4)}`;
    }
}

Fetch all registrations

// The /domains endpoint returns up to 1000 results.
// Default limit is 50; includes profile data for each domain.
const { data, count } = await fetch("https://hacktez.com/api/v1/domains?limit=200").then((r) => r.json());
console.log(`Got ${data.length} of ${count} total domains`);

// Profile data is included — no need for separate /profile calls
data.forEach((d) => {
    console.log(d.label, d.profile.bio, d.profile.skills);
});
download hack-tez-api.md