Nukez

Docs · Portal

Multi-keypair.

linked keysed25519 · secp256k1

One login, several signing identities.

Linking lets a second provisioned keypair appear under the portal login you already have, so both identities' lockers show in one view. The cryptography happens in your terminal and the identity binding happens in the browser; nothing but a signature crosses the network. Linking never creates an account — each keypair must be provisioned on its own first.

PasskeySession only

Authenticates the browser; signs no storage envelope.

KeypairsSigning authority

Each authorizes gateway envelopes for its own lockers.

LinkSignature only

Private key material is never uploaded at any step.

Model

One portal identity, many signing keys

The passkey authenticates a browser session and cannot sign a storage envelope. Each signing keypair is provisioned separately and owns its own lockers.

model
portal identity          # exactly one, created by the passkey  credential: WebAuthn resident key, RP_ID nukez.xyz  session:    localStorage nukez_session_token, sent as Authorization: Bearer  authorizes: GET /v1/portal/status, /files, /ops-log, /usage, /alerts, /auth/linked-keys signing keypair          # one or many, each provisioned on its own  ed25519:    base58 pubkey, signature base58-encoded  secp256k1:  0x address, EIP-191 personal_sign, 0x-prefixed hex signature  authorizes: POST /v1/delegate with X-Nukez-Identity, signed storage envelopes link                     # POST /v1/portal/auth/link-pubkey  joins:      one portal identity to N already-provisioned keypairs  merges:     files, status, usage and alerts into one view (files tagged by key)  does not:   share lockers, or let one key sign for another

Code notes

The passkey is a WebAuthn credential that authenticates a browser session and nothing else — it cannot sign a storage envelope. Each signing keypair is provisioned on its own, owns its own lockers, and authorizes gateway work through the X-Nukez-Identity header plus a signature from its private key. Linking is the operation that tells the portal which already-provisioned keypairs belong to one human, so their files, status, usage and alerts render in a single view.

Linking is a portal-only concern. It groups your view; it does not change ownership, access control, or cryptographic identity. The gateway has no concept of portal identity linking.

Precondition

The key must already be provisioned

Linking joins two existing accounts; it does not create one. A keypair that has never completed provisioning is rejected outright.

precheck
import json, os, httpx, base58from nacl.signing import SigningKey SERVICE = "https://agent.nukez.xyz"NEW_KEYPAIR_PATH = "~/.keys/delegators/svm_key.json"   # the key you want to link # Load the key to be linked. A Solana-style keypair file is a 64-byte# array; the first 32 bytes are the Ed25519 seed.key_bytes = json.load(open(os.path.expanduser(NEW_KEYPAIR_PATH)))sk = SigningKey(bytes(key_bytes[:32]))pubkey = base58.b58encode(bytes(sk.verify_key)).decode() client = httpx.Client(timeout=30)resp = client.post(f"{SERVICE}/v1/portal/auth/link-pubkey", json={"pubkey": pubkey}) if resp.status_code == 404:    raise SystemExit(        "Not provisioned. Run POST /v1/provision/challenge then "        "POST /v1/provision/verify for this key, then link it."    )if resp.status_code == 409:    raise SystemExit("Already linked to this portal identity.")resp.raise_for_status() challenge = resp.json()print(challenge["challenge"])     # the exact string to signprint(challenge["expires_at"])    # unix seconds; the Keys view counts this down

Code notes

A keypair can only be linked after it has completed provisioning on its own — its own developer_id and active storage, which means running POST /v1/provision/challenge and POST /v1/provision/verify for that key first. The link-pubkey call is where the precondition is enforced, and it needs no session of its own: it is the one step in this flow you can run from a bare terminal. A 404 means the key has no NukezAgent subscription yet; a 409 means it is already linked to this portal identity. A success returns the exact challenge string to sign and the unix timestamp at which it stops being valid.

The portal's inline Keys form validates the key before calling this endpoint: base58 of 32 to 44 characters for Ed25519, or 0x followed by 40 hex characters for an EVM address. It also surfaces the 403 case as a message saying the key's account is not active.

Proof

Sign the challenge with the new key

Control of the new private key is the entire proof. Ed25519 base58-encodes the signature and secp256k1 signs the same string through EIP-191.

sign
import webbrowser# continues from the previous step: client, sk, pubkey, challenge # Ed25519 - sign the challenge string, base58-encode the 64-byte signaturesigned = sk.sign(challenge["challenge"].encode())signature = base58.b58encode(signed.signature).decode() # secp256k1 variant - EIP-191 personal_sign over the same string:#   from eth_account import Account#   from eth_account.messages import encode_defunct#   account = Account.from_key(evm_private_key)   # pubkey = account.address#   signed = account.sign_message(encode_defunct(challenge["challenge"].encode()))#   signature = signed.signature.hex()#   if not signature.startswith("0x"): signature = "0x" + signature resp = client.post(f"{SERVICE}/v1/portal/auth/link-pubkey/verify", json={    "pubkey": pubkey,    "challenge": challenge["challenge"],    "signature": signature,})resp.raise_for_status()result = resp.json() print(result["status"])print(result["link_token"])webbrowser.open(result["portal_url"])# opens https://portal.nukez.xyz/agent-portal?link_token=...

Code notes

Signing the challenge is the entire proof of control. Ed25519 signs the raw challenge bytes and base58-encodes the signature; secp256k1 signs the same string through EIP-191 personal_sign and submits 0x-prefixed hex. The request to POST /v1/portal/auth/link-pubkey/verify carries three fields — pubkey, the challenge exactly as it was issued, and the signature — and the response returns a status, a link_token, and a ready-made portal_url that already carries that token as a query parameter.

Only a signature is ever transmitted. The private key never leaves the machine running this script, and no Nukez endpoint has a field that would accept it.

Handoff

Finish in the logged-in browser

Completion needs the portal session, which the terminal does not have. Opening the returned URL lets the portal finish the link with its own credentials.

browser
https://portal.nukez.xyz/agent-portal?link_token=... # already signed in -> the portal consumes the token immediately# signed out        -> the token is stashed in sessionStorage and#                      consumed after one biometric tap POST /v1/portal/auth/link-pubkey/complete HTTP/1.1Host: agent.nukez.xyzAuthorization: Bearer <session token from localStorage nukez_session_token>Content-Type: application/json {"link_token": "..."} 200  status linked, portal_identity, pubkeys[]  -> toast, then /agent-portal/keys200  status already_linked, pubkey              -> same toast, same redirect400  invalid or expired link_token             -> re-run the verify step # The link_token parameter is stripped from the URL in every case.

Code notes

The verify call does not complete the link, because completion requires your portal session and the terminal does not have one. Opening portal_url lands on /agent-portal with a link_token query parameter. If a session is already active the portal calls link-pubkey/complete immediately; if not, it stashes the token in sessionStorage, takes you through one passkey tap, then consumes it. Both paths end on the Keys page.

If the portal reports an expired link, run the challenge and verify calls again to mint a fresh link_token. The verify step consumes the challenge, so both calls have to be repeated.

Confirm

Read back the linked set

One endpoint is authoritative for which identities are linked, returning the portal identity plus an entry per key.

confirm
GET /v1/portal/auth/linked-keys HTTP/1.1Host: agent.nukez.xyzAuthorization: Bearer <session token from localStorage nukez_session_token> {  "portal_identity": "<portal identity id>",  "keys": [    {      "developer_id": "<uuid of the original key>",      "pubkey": "<base58 ed25519 pubkey>",      "algorithm": "ed25519",      "label": null,      "linked_at": "2026-08-05T17:04:11+00:00"    },    {      "developer_id": "<uuid of the newly linked key>",      "pubkey": "<0x evm address>",      "algorithm": "secp256k1",      "label": null,      "linked_at": "2026-08-05T17:22:48+00:00"    }  ]} # Keys view columns: Public Key / Algorithm / Type / Linked.# Row one is shown as Primary, every later row as Linked.# The sidebar shows a key-count link only when more than one key is linked. # Unchanged by linking: each developer_id still owns its own lockers,# and X-Nukez-Identity still resolves to exactly the key in the header.

Code notes

GET /v1/portal/auth/linked-keys is the authoritative list. It returns portal_identity plus one entry per linked key carrying developer_id, pubkey, algorithm, label and linked_at. The portal Keys view renders exactly this, and falls back to showing only the pubkey from status when the endpoint is not yet live. Confirming here is worth the extra call because linking changes what you see and nothing else: every gateway request still resolves to the single key named in X-Nukez-Identity.

Identifiers are elided in this sample. The Keys view truncates each public key to six characters at either end and copies the full value to the clipboard on click.

Linking joins views, not authority.

A linked keypair contributes its lockers to the merged portal view. It does not gain authority over another key's lockers, and the portal session still cannot sign on any key's behalf. To enroll the passkey that gates this flow, see passkey setup.

§ next