{ "openapi": "3.1.0", "info": { "title": "Hanzo Cloud API", "description": "Package validators is one-click validator onboarding: prove your Genesis NFT, get a node provisioned, queue its registration.", "version": "v1" }, "servers": [ { "url": "https://api.hanzo.ai" } ], "tags": [ { "name": "validators" } ], "paths": { "/v1/validators": { "get": { "operationId": "get_v1_validators", "summary": "Returns the validator slots the caller's org has claimed.", "description": "Returns the validator slots the caller's org has claimed.\n\nOne entry per claimed slot with its node identity, its live-ish node status and\nthe owner-gated registration queued for it, if any. Slots are org-scoped by the\nvalidated identity, so a caller can only ever see their own — a slot claimed by\nanother org is not merely hidden from this list, it is unreachable through the\nwhole surface.", "tags": [ "validators" ], "parameters": [ { "name": "limit", "in": "query", "required": false, "description": "Limit is how many slots to return, as a decimal string in the `?limit=`\nquery. Absent, unparseable or non-positive means 200; over 1000 is clamped\nto 1000. It is a string rather than a number because the parse that has\nalways served this route trims surrounding whitespace, and one parse rule is\nbetter than two.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/validatorList" } } }, "description": "ok" } } }, "post": { "operationId": "post_v1_validators", "summary": "Claims a validator slot and provisions its node, after proving the caller's wallet owns the slot's NFT.", "description": "Claims a validator slot and provisions its node, after proving the\ncaller's wallet owns the slot's NFT.\n\nThe pipeline, all server-enforced: burn the single-use challenge (so a replayed\nor forged nonce dies before any chain read), recover the signer from the message\nthis server rebuilds, require that wallet to hold Validator-tier GenesisNFT\n#tokenId on Ethereum mainnet, generate a fresh luxd staking identity and seal it\ninto KMS, write a LuxNetwork CR for a NEW node, and ENQUEUE an owner-gated\nregistration. The registration is never auto-submitted to any P-Chain — the\nowner co-signs it out of band — and the stake weight is set at co-sign time,\nnever derived from the NFT.\n\nIt fails CLOSED at every gate: a bad signature, a non-owner, a non-tier slot or\nan unavailable KMS all leave no claim persisted and no key material exposed.\nRe-claiming a slot this org already holds re-applies the node CR and returns 200\nwith the existing identity (keys and NodeID are stable); a slot held by another\norg is 409. A cluster-less deployment still claims the slot, seals the keys and\nqueues the registration, reporting the node as \"node_pending\".", "tags": [ "validators" ], "requestBody": { "content": { "application/json": { "example": { "nonce": "5f3a…", "signature": "0x…", "tokenId": 7 }, "schema": { "$ref": "#/components/schemas/validatorClaim" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/slotView" } } }, "description": "ok" } } } }, "/v1/validators/challenge": { "get": { "operationId": "get_v1_validators_challenge", "summary": "Issues the single-use nonce and the exact message a wallet must sign to claim a validator slot.", "description": "Issues the single-use nonce and the exact message a wallet must sign\nto claim a validator slot.\n\nThe nonce is bound to (validated org, slot) and stored server-side, so a\nsignature obtained for one org or one slot can never be replayed for another,\nand the message POST /v1/validators verifies is rebuilt from those same server\nfacts rather than trusted from the caller. Redeem it with\nPOST /v1/validators before it expires; it can be redeemed once.\n\nA tokenId outside the Validator tier is refused here rather than after signing.", "tags": [ "validators" ], "parameters": [ { "name": "tokenId", "in": "query", "required": false, "description": "TokenID is the Validator-tier GenesisNFT token id, as a decimal string in\nthe `?tokenId=` query. A value that is not a positive integer is 400. It is\na string rather than a number because the parse that has always served this\nroute trims surrounding whitespace, and one parse rule is better than two.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/challengeView" } } }, "description": "ok" } } } }, "/v1/validators/{tokenId}": { "get": { "operationId": "get_v1_validators_tokenId", "summary": "Returns one claimed validator slot, scoped to the caller's org.", "description": "Returns one claimed validator slot, scoped to the caller's org.\n\nA slot another org holds, and a slot nobody holds, are both 404 — never a\ndifferent status, so this route cannot be used to probe which slots are taken.", "tags": [ "validators" ], "parameters": [ { "name": "tokenId", "in": "path", "required": true, "description": "TokenID is the slot's GenesisNFT token id, from the path, as a decimal\nstring. A value that is not a positive integer is 400. It is a string\nrather than a number because the parse that has always served this route\ntrims surrounding whitespace, and one parse rule is better than two.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/slotView" } } }, "description": "ok" } } } } }, "components": { "schemas": { "challengeView": { "properties": { "expiresAt": { "description": "ExpiresAt is when the nonce stops being redeemable, as a Unix timestamp.", "type": "integer" }, "message": { "description": "Message is the EXACT text to personal_sign. It is reconstructed server-side\nfrom the validated org, the slot and the nonce at redemption, so signing\nanything else cannot claim the slot.", "type": "string" }, "nonce": { "description": "Nonce is the single-use, org-bound challenge value to send back with the\nsignature.", "type": "string" }, "tokenId": { "description": "TokenID is the slot the challenge was issued for.", "type": "integer" }, "ttlSeconds": { "description": "TTLSeconds is the challenge lifetime in seconds.", "type": "integer" } }, "type": "object" }, "registrationView": { "properties": { "id": { "description": "ID is the registration's handle.", "type": "string" }, "nodeID": { "description": "NodeID is the luxd node the registration is for.", "type": "string" }, "status": { "description": "Status is the registration's lifecycle state; \"pending_owner_approval\" until\nthe owner co-signs it out of band.", "type": "string" } }, "type": "object" }, "slotView": { "properties": { "blsPubkey": { "description": "BLSPubkey is the node's BLS public key, hex.", "type": "string" }, "crName": { "description": "CRName is the LuxNetwork custom resource that materializes the node.", "type": "string" }, "createdAt": { "description": "CreatedAt is when the slot was first claimed, as a Unix timestamp.", "type": "integer" }, "namespace": { "description": "Namespace is the Kubernetes namespace the node's CR lives in.", "type": "string" }, "network": { "description": "Network is the luxd network slug the node joins.", "type": "string" }, "nodeID": { "description": "NodeID is the luxd node id derived from the sealed staking identity. It is\nstable across re-claims of the same slot.", "type": "string" }, "nodeStatus": { "description": "NodeStatus is the provisioning state of the node: \"node_created\" once the CR\nis applied, \"node_pending\" when no cluster is reachable (the slot is still\nclaimed and the keys are still sealed).", "type": "string" }, "registration": { "$ref": "#/components/schemas/registrationView", "description": "Registration is the queued owner-gated registration, absent until one exists." }, "slot": { "description": "Slot is the validator slot number — the same value as tokenId, under the\nname the portal reads.", "type": "integer" }, "tokenId": { "description": "TokenID is the GenesisNFT token id that IS this slot.", "type": "integer" }, "updatedAt": { "description": "UpdatedAt is when the slot last changed, as a Unix timestamp.", "type": "integer" }, "wallet": { "description": "Wallet is the lowercase Ethereum address that proved ownership of the NFT.", "type": "string" } }, "type": "object" }, "validatorClaim": { "properties": { "nonce": { "description": "Nonce is the value GET /v1/validators/challenge issued for this slot.", "type": "string" }, "signature": { "description": "Signature is the wallet's personal_sign over the challenge message, hex with\na 0x prefix.", "type": "string" }, "tokenId": { "description": "TokenID is the Validator-tier GenesisNFT token id being claimed. It IS the\nvalidator slot.", "type": "integer" } }, "type": "object" }, "validatorList": { "properties": { "data": { "description": "Data is one entry per slot this org has claimed.", "items": { "$ref": "#/components/schemas/slotView" }, "type": "array" }, "network": { "description": "Network is the luxd network slug new nodes join on this deployment.", "type": "string" } }, "type": "object" } } } }