Every portal call sends the session token issued at passkey login.
Docs · Portal
Owner portal.
The owner view is read, verify, and link.
Signed in at portal.nukez.xyz/agent-portal, an owner can read what their agent stored, check what it did and what it consumed, verify the system receipt against the chain, and link another provisioned keypair. Everything that mutates a locker still happens through a signed envelope from the keypair, not from this browser session.
No upload, delete, or capacity purchase exists in this surface.
Linked keypairs contribute their files to one combined list.
Entry
Provision, then bind a passkey
First entry is a provisioning flow rather than a sign-up. The keypair creates the developer record and the passkey turns it into a browser session.
import base58, httpx, jsonfrom nacl.signing import SigningKey AGENT = "https://agent.nukez.xyz" # Solana-format keypair: a 64-byte JSON array, seed first.key_bytes = json.load(open("nukez_key.json"))sk = SigningKey(bytes(key_bytes[:32]))pubkey = base58.b58encode(bytes(sk.verify_key)).decode() # Step 1 - ask the front door for a provisioning challenge.ch = httpx.post(AGENT + "/v1/provision/challenge", json={"pubkey": pubkey}).json()# -> {"message": "...", "nonce": "...", "expires_at": ...} # Step 2 - sign challenge["message"] and base58-encode the signature.# secp256k1 owners sign the same string with EIP-191 personal_sign# (eth_account encode_defunct) and send 0x-prefixed hex instead.sig = base58.b58encode(sk.sign(ch["message"].encode()).signature).decode() # Step 3 - exchange the signature for a registration token.res = httpx.post(AGENT + "/v1/provision/verify", json={ "pubkey": pubkey, "nonce": ch["nonce"], "signature": sig,}).json() print(res["developer_id"], res["status"], res["owner_pubkey"])print(res["portal_url"])# https://portal.nukez.xyz/agent-portal?register_token=<token> # Step 4 - res["status"] reads "pending_payment" until storage is paid# for: GET /v1/service/request for pricing, send the on-chain payment,# then POST /v1/service/confirm with the X402-TX header to activate. # Step 5 - open portal_url in a passkey-capable browser within# 10 minutes. The portal reads register_token, creates a discoverable# passkey bound to developer_id, and starts a session. Every later# visit is the same URL with no token and a single Connect button. # If the token expired, mint a new one without re-provisioning:# POST /v1/portal/auth/challenge {pubkey} -> {challenge}# POST /v1/portal/auth/verify-keypair {pubkey, challenge, signature}# -> {registration_token}
Code notes
The agent portal lives at https://portal.nukez.xyz/agent-portal and is backed by the front door service at https://agent.nukez.xyz. First entry is not a sign-up form. You provision your Ed25519 or secp256k1 keypair over HTTP, and POST /v1/provision/verify returns a portal_url that already carries a register_token. Provisioning leaves the account in pending_payment until storage is paid for, and opening the portal_url is what creates the passkey. Every visit after that is the same URL with no token and a single Connect button, because the credential is discoverable and the browser resolves it without a username.
The session token, developer id, and expiry are held in localStorage under nukez_session_token, nukez_developer_id, and nukez_session_expires. Every read compares the stored expiry against the current time and clears the session once it has passed, and any 401 returned by a portal call clears it as well.
Posture
Read status and alerts
Status is the identity and posture record for the signed-in owner; alerts carry lifecycle warnings. Both are session-gated reads against the front door.
GET /v1/portal/statusAuthorization: Bearer <nukez_session_token> 200 OK{ "developer_id": "453e7d09-70b8-4b3a-a54b-f33698f68e22", "owner_pubkey": "BhBeSkwKyqysZstzkqdf4qAcYfS9r27wEMmouvSVfp1U", "sig_alg": "ed25519", "network": "solana-devnet", "system_locker_id": "4b91c2f70ad3...", "system_receipt_id": "232fed413a4d83fd", "has_signing_bridge": true, "status": "active"}# The runtime also returns provider, needs_bootstrap,# has_valid_owner_session, service_expires_at, subscription,# lockers[] and capacity_summary. The portal reads only the eight# fields shown above. GET /v1/portal/alerts 200 OK{"alerts": [ {"type": "capacity_warning", "severity": "warning", "message": "Locker is at 82 percent of provisioned capacity.", "developer_id": "453e7d09-70b8-4b3a-a54b-f33698f68e22", "created_at": 1754280729.0, "data": {}, "acknowledged": false}]} # The dashboard builds three cards from status and files:# Files stored = file_count, subtitle from last_attested# Status = status, subtitle from has_signing_bridge# Network = network, subtitle from system_locker_id# Alerts are not cards - they render as callouts above them.# It re-fetches files and alerts every 30 seconds, and a Refresh# button forces the same load. # Backend severities are info, warning and critical. The dashboard# only styles the literal string "error" as an error, so in practice# every alert lands in the info callout. # With more than one keypair linked, status returns an array form:# {"results": [{"pubkey": "...", "algorithm": "ed25519",# "developer_id": "...", "data": {...flat status...}}],# "linked_but_unprovisioned": [...]}
Code notes
Once the session exists, every portal call sends it as a bearer token to https://agent.nukez.xyz. GET /v1/portal/status is the identity and posture record for the signed-in owner, and GET /v1/portal/alerts plus GET /v1/portal/files supply the rest of the dashboard. The three stat cards on /agent-portal are built from status and files; alerts render as callouts above them. There is no settings screen, no billing view, and no capacity control on the page.
The files, ops-log, usage, and alerts callers convert a 404 or 503 into an empty result rather than an error, so an empty dashboard can mean either no data or an endpoint that is not live yet. GET /v1/portal/status has no such fallback and will surface its failure.
Records
Inspect a stored file
The portal shows file metadata, not file bytes: size, content type, stored time, content hash, and which linked identity wrote it.
GET /v1/portal/filesAuthorization: Bearer <nukez_session_token> 200 OK{ "file_count": 24, "last_attested": "2026-08-04T11:02:47Z", "files": [ { "filename": "q3-forecast.md", "size_bytes": 18422, "content_type": "text/markdown", "content_hash": "1f0c8a3d...e2b49ab4", "stored_at": 1754302967, "task_context": "quarterly planning run" } ]} # stored_at is Unix seconds. last_attested sits on the response# envelope, not on any one file. task_context is truncated to 100# characters by the runtime. # Once more than one keypair is linked the response also carries an# identities[] array, and every record - including the primary key's -# is tagged with _identity_pubkey and _identity_algorithm. # The dashboard shows the first 8 files; /agent-portal/files shows all# of them, polls every 30 seconds, and sorts on filename, size_bytes# and stored_at only. Clicking a row expands the metadata above. # The portal shows file metadata only - it never serves file bytes.# /v1/portal/files is a collection route; the front door defines no# per-file content endpoint, so there is no in-portal preview. Read a# file's contents through the gateway receipt proxy or the SDK instead. # There is no download, delete, rename, upload, or share control# anywhere on this page.
Code notes
A file record in the portal is metadata, not content. GET /v1/portal/files returns a file_count, a response-level last_attested timestamp, and an array of records describing what the gateway holds for this identity. /agent-portal/files lists all of them; the dashboard shows only the first eight. Expanding a row shows the full record, and a single text preview call is the only path to any bytes.
The portal reads this list from the runtime's merged state dictionary and cached storage index rather than from the file bytes, which is why metadata is available without a signature at all. Reading the bytes themselves is a separate, receipt-scoped operation outside this surface.
Proof
Verify the system receipt
Verification from the portal is scoped to one receipt — the system receipt named in status — and checks it against the on-chain anchor.
# The portal verifies exactly one receipt - the system receipt from# status.system_receipt_id. There is no free-form receipt input. GET /v1/portal/verify/232fed413a4d83fdAuthorization: Bearer <nukez_session_token> 200 OK{ "status": "success", "receipt_id": "232fed413a4d83fd", "verification": { "attestation": { "attestation_status": "complete", "merkle_root": "9a41c7e0...5f2d8b13", "tx_signature": "5KJp9wQ2...rTxV2m", "file_count": 24 } }}# The front door forwards this to the runtime's unsigned# /v1/audit/receipts/<receipt_id> route. # The badge reads Verified only when attestation_status is "complete"# or "attested". Every other value renders as Failed. The top-level# status field is not read by the UI at all. # tx_signature is linked to the Solana explorer, cluster derived from# status.network:# solana-devnet -> explorer.solana.com/tx/<sig>?cluster=devnet# solana-testnet -> explorer.solana.com/tx/<sig>?cluster=testnet# mainnet -> explorer.solana.com/tx/<sig># Any network string containing devnet or testnet matches too, and the# page falls back to solana-devnet before status has loaded. # The files, ops-log, usage and alerts callers convert 404 and 503 into# empty states. verifyReceipt does not - and neither do getPortalStatus# or getLinkedKeys, which also surface their failures. # When system_receipt_id is null the page shows instead:# "No receipt ID available. Your agent may not have completed its# first attestation yet."
Code notes
Verification from the portal is scoped to one receipt: the system receipt named by status.system_receipt_id. There is no free-form receipt field on /agent-portal/verify, so the page verifies the identity's own attestation and nothing else. The call is unsigned, which is why it works from a passkey session that has no access to the storage key.
Verification is independent and unsigned: anyone holding the receipt id can run the same check, and the Merkle root recorded in the anchoring transaction can be read directly on Solana without going through the portal.
Activity
Read the log, usage, and linked keys
Two tabs backed by two endpoints cover what agents did and what they consumed; the keys view lists every identity linked to this portal login.
GET /v1/portal/ops-log 200 OK{"count": 2, "entries": [ {"ts": 1754302967, "agent_id": "agent_7c1f", "task_summary": "Stored quarterly forecast, then re-attested", "operations": [ {"tool": "store_file", "status": "ok", "summary": "q3-forecast.md"}, {"tool": "attest", "status": "ok", "summary": "root anchored"} ], "token_usage": {"sonnet_input_tokens": 8140, "sonnet_output_tokens": 1120}}]} GET /v1/portal/usage 200 OK{"usage": {"sonnet_input_tokens": 412880, "sonnet_output_tokens": 51204, "opus_input_tokens": 0, "opus_output_tokens": 0, "total_api_calls": 1893, "runtime_ops": 246}} # Ops Log tab is a Time / Task / Tool / Status table. An operation# with status "ok" renders a check and anything else a cross. The# per-entry token_usage block is fetched but never rendered. The# Usage tab renders four of the six counters - opus_input_tokens and# opus_output_tokens are fetched and not displayed. GET /v1/portal/auth/linked-keys 200 OK{"portal_identity": "...", "keys": [{"developer_id": "...", "pubkey": "...", "algorithm": "ed25519", "label": null, "linked_at": "2026-07-02T09:15:00Z"}]} # Linking a second keypair. That key must already be provisioned by# the flow in step 1, or the first call returns 404. A 409 means the# key is already linked to this portal identity.POST /v1/portal/auth/link-pubkey body {"pubkey": "<second base58 key or 0x address>"} -> {"challenge": "...", "expires_at": 1754303400}POST /v1/portal/auth/link-pubkey/verify body {"pubkey": "...", "challenge": "...", "signature": "..."} -> {"status": "...", "link_token": "...", "portal_url": "..."}POST /v1/portal/auth/link-pubkey/complete body {"link_token": "..."} # session Bearer required -> {"status": "...", "portal_identity": "...", "pubkeys": [...]} # Two ways to drive it: open portal_url from a script and let the# portal redeem link_token with your session, or use the Link Key form# on /agent-portal/keys, which shows the challenge with a countdown,# accepts a pasted signature, and runs verify then complete for you.
Code notes
The Activity view has two tabs backed by two endpoints: GET /v1/portal/ops-log for what agents did, and GET /v1/portal/usage for token and call counters. Neither polls; both refresh on the button. The Keys view lists the pubkeys attached to this portal identity from GET /v1/portal/auth/linked-keys and holds the one write-adjacent flow in the whole portal, which is linking a second keypair that has already been provisioned on its own.
The passkey authenticates the human and cannot sign a storage envelope. Storing and recalling files still require a signature from the Ed25519 or secp256k1 private key through the CLI or API. Linking groups the portal view only: each key keeps its own lockers, no key can sign for another, and gateway request handling is unchanged.
What the session cannot do.
The portal session authorizes reads and receipt verification. It does not authorize storage writes, deletions, or payments — those require an envelope signed by the keypair itself. To bring a second keypair under this login, follow the multi-keypair guide.
