# NOVA > NOVA is a zero-data-retention backend for AI agents on NEAR. Files are encrypted client-side > (AES-256-GCM, one random key per file), stored as ciphertext on FastFS (NEAR), and decryptable > only by members of a group listed on the NEAR contract `nova-sdk.near`. Keys are held and > released by a TEE (Shade agent, Phala/dstack) after an on-chain membership check. Every upload > is recorded on chain with its SHA-256 `file_hash` and timestamp; groups can carry a retention > window after which files are crypto-shredded, wiped from FastFS and tombstoned on chain with a > `DeletionRecord`. Mainnet, real NEAR. If this file and the live API disagree, the API wins. > **2026-11-12: new TEE and endpoint.** NOVA moved to a new CVM as part of a security rebuild. The > MCP URL below changed; the previous one is retired and will stop answering shortly. API keys and > sessions issued before 2026-11-12 no longer work: get a new key in the console. Use > `nova-sdk-js` / `nova-sdk-rs` ≥ 1.4.1. ## Use it from - **TypeScript/JavaScript**: `npm i nova-sdk-js` (≥ 1.4.1) — server-side; does encryption/decryption for you. Recommended. - **Rust**: `nova-sdk-rs` ≥ 1.4.1 (crates.io). - **HTTP**: the MCP server's REST surface, `POST {MCP}/tools/` (below). You must then do the client-side crypto yourself (prefer the SDK). - **MCP clients** (agents): the same server speaks MCP over HTTP; tools are listed below. - **IronClaw Reborn agents**: the `nova-reborn` WASM extension (host-injected API key). - **Agent Connect agents** (NEAR Intents Agent API): sign in to NOVA as their own NEAR account (NEP-413 via `native/near/sign-message`) and sign and pay their own writes (`native/near/call`). Reference module and guide: `nova-agent-connect` (below). - **Owners/humans**: the console at https://nova-sdk.com/app (groups, files, retention, deletion audit, API key). Endpoints: - Site / auth: `https://nova-sdk.com` - MCP (tools): `https://7e9ff97d35e21fdd0f5c819472217d19e772d017-8000.dstack-base-prod5.phala.network` - Contract: `nova-sdk.near` (mainnet), `nova-sdk-7.testnet` (testnet) ## Concepts - **Account**: a NEAR account. Custodial (`.nova-sdk.near`, key held for you by NOVA's TEE, you authenticate with an API key) or self-custodial (your wallet or an Agent Connect agent, authenticated by a NEP-413 signature; NOVA never holds the key). - **Group**: unit of access. One owner, many members, optional retention window. The owner adds and revokes members; revoking rotates the group key forward (files uploaded after revocation are unreadable to the ex-member; files they already decrypted cannot be recalled). - **File**: encrypted with its own random key, wrapped under the group key and stored in the TEE's KV. On chain: `trans_id`, `group_id`, `user_id` (uploader), `file_hash`, location, `backend` (`FastFS` | `Ipfs` legacy), `timestamp`, `deleted` (`DeletionRecord` or null). - **Retention**: per-group window in days (`set_group_retention`). Expiry is computed from each file's upload timestamp. A sweep destroys the file key, null-writes FastFS, and records `DeletionRecord { reason: "RetentionPolicy", deleted_at, deleted_by }`. Deletion reasons: `MemberRevocation | OwnerRequest | RetentionPolicy | ComplianceRequest`. - **Joinable groups**: groups registered `joinable` with an open window; members self-join with `join_group` (used for hackathon submissions). Their members and file list are public views. ## Who sees what - NOVA's servers: MCP and the Shade agent run together in one TEE (Phala CVM, dstack 0.5.12, KMS on Base); the agent contract `shade-agent.nova-sdk.near` registers it only with approved measurements. They handle per-file keys and ciphertext, never plaintext; FastFS stores ciphertext only. "Plaintext data and encryption keys never travel together." - Transport: TLS to the `*.phala.network` endpoints ends at Phala's gateway (itself in a TEE, but outside NOVA's attestation), so the key in `prepare_upload` / `prepare_retrieve` responses is readable there. End-to-end encryption of those responses to the client is in progress. - TEE: holds keys, not files; releases a key only to an on-chain member. - NEAR chain: group ids, members, file hashes, timestamps, deletion records (metadata is public). - Members' clients: plaintext (that is the point). - Whatever model an agent hands plaintext to (e.g. an LLM provider) sees it. NOVA's guarantee ends there: use zero-retention terms with the provider, or redact first. ## Authentication All MCP calls need one of: 1. `Authorization: Bearer ` — a JWT (HS256, 24 h, `type: "nova_session"`, `iss: https://nova-sdk.com`, `aud: https://nova-sdk.com/api/mcp`). Optional `x-account-id` header is a hint only and must match the token's `account_id`. 2. `X-API-Key: nova_sk_…` **and** `x-account-id: ` — the key is verified against that account on every call. Used by host-injected setups (nova-reborn). A Bearer, if present, wins. Getting a `nova_session`: - **API key → session** (custodial accounts and backends; a wallet account can generate a key too): `POST https://nova-sdk.com/api/auth/session-token`, header `x-api-key: nova_sk_…`, body `{"account_id": ".nova-sdk.near"}` → `{ token, … }`. The SDK does this and refreshes before expiry. Get/rotate the key in the console (Account), signed in by email or wallet. One key per account; rotation invalidates the old one. Keep it server-side only. - **Agent Connect agent → session** (NEP-413 via the Agent API's native accounts; the agent keeps its own key): 1. The agent's owner enables NOVA in the agent policy (on-chain policy update, one signature): `native: { near: { sign_message: ["nova-sdk.near"], transfer: false, call: [] }, evm: { sign_typed_data: [], sign_message: false, sign_transaction: false }, solana: { sign_message: false, sign_transaction: false } }`. `sign_message` alone is enough to sign in and read; it moves no money. To also write, list the contract in `call` as well (see "Self-custodial writes"). Keep `transfer` off: native requests skip `destinations`, `budget`, `timelock_ms`, `schedule` and `owner_approval`. 2. `POST https://nova-sdk.com/api/auth/agent-identity/challenge` (no body) → `{ message, challenge, audience, expires_at_ms, nonce }`. Challenges live 2 minutes, single use. 3. `POST /v1/agents/{agent_id}/native/near/sign-message` with `X-API-Key` + `X-Grant-Token`, body `{ "message": , "recipient": , "nonce": }` → `{ correlation_id, near_account_id, public_key, signature (base64), nonce, recipient }`. SDK: `agent.nativeNearSignMessage(agentId, { message, recipient, nonce })`. 4. `POST https://nova-sdk.com/api/auth/agent-identity/verify` body `{ "challenge": , "signature": }` → `{ token, account_id, expires_in }` (no cookie; backend use). Errors: 400 bad body, 401 `UNAUTHORIZED` (wrong nonce, signature or key) / `UNAUTHORIZED_NONCE_REPLAY` (get a new challenge), 503 `KEY_LOOKUP_UNAVAILABLE` or `CHALLENGE_CAPACITY` (retry; nothing consumed). NOVA checks the NEP-413 signature over the message AND nonce it stored, that the returned nonce is the one it issued, that `public_key` is a full-access key of `near_account_id` (or the implicit account not yet on chain whose id is the key), then consumes the challenge. Keep the token in your backend, never in a model prompt. - **Browser wallet sign-in** (NEP-413) and email login exist for the console. Email sign-up is paused during the 2026-11 rebuild; existing email accounts are being migrated. A wallet session writes with its own wallet (see "Self-custodial writes"). ## SDK (nova-sdk-js) ```ts import { NovaSdk, NovaError } from 'nova-sdk-js'; const sdk = new NovaSdk(process.env.NOVA_ACCOUNT_ID!, { apiKey: process.env.NOVA_API_KEY }); await sdk.registerGroup('acme-proofs'); // owner await sdk.addGroupMember('acme-proofs', ''); // owner const up = await sdk.upload('acme-proofs', bytes, 'order-123.jpg'); // { cid, trans_id, file_hash } const { data } = await sdk.retrieve('acme-proofs', up.cid); // member ``` Methods: `upload`, `retrieve`, `registerGroup`, `addGroupMember`, `revokeGroupMember`, `joinGroup`, `setGroupRetention`, `getMemberWindow`, `signerMode`, `authStatus`, `isAuthorized`, `getGroupOwner`, `getGroupChecksum`, `getTransactionsForGroup`, `getOwnedGroups`, `getMemberGroups`, `getGroupMembers`, `getBalance`, `estimateFee`, `refreshToken`, `computeHash`, `computeHashAsync`. `cid` is the storage location (FastFS path; the name is kept for compatibility). Testnet: pass `rpcUrl` and `contractId: 'nova-sdk-7.testnet'`. An agent that signed in (above) uses its session instead of an API key; retrieval costs it nothing: ```ts const agentSdk = new NovaSdk('', { sessionToken }); // from /verify const { data } = await agentSdk.retrieve('acme-proofs', location); // member of the group // verify: sha256(data) === the file_hash recorded on chain at upload ``` ## Self-custodial writes (browser wallets and Agent Connect agents) A self-custodial account signs and pays its own contract writes. NOVA never signs for it, sponsors none of its transactions, and never sees its key. `nova-sdk-js` ≥ 1.4.0 does this in wallet mode: pass `walletSigner: { accountId, signAndSendFunctionCall({ receiverId, methodName, args, gas, deposit }) → { txHash } }` (`nova-sdk-rs` 1.4.0: `NovaWalletSigner`, `with_wallet_signer`). Uploads are record-first: `prepare_upload` returns the FastFS `location`, the account signs `record_transaction` at that location, and `finalize_upload` carries its `tx_hash`. MCP checks that transaction on chain (signer = the session account, receiver `nova-sdk.near`, exact args, success) before storing any byte. Owner operations work the same way: the account signs, then MCP re-reads the chain before doing NOVA's side (key creation, rotation). ### Agent Connect agents (live on mainnet since 2026-10-29) The agent's own NEAR account (`wallet.near_account_id`) signs with its own key through `native/near/call` and pays gas and fees from its own NEAR balance. 1. The owner's policy (one on-chain update): `native.near.sign_message` and `native.near.call` = `["nova-sdk.near"]`; `assets` allows `nep141:wrap.near` (native deposits count as wNEAR); `limits.per_transaction["nep141:wrap.near"]` caps each deposit (the deposit is NOVA's fee: `estimate_fee`; `record_transaction` is 0.0000256 NEAR, `register_group` 0.64 NEAR); `max_actions_per_hour` ≥ the writes you expect (every write that reaches the chain counts, even one the contract refuses). 2. Fund the agent's NEAR account (any NEAR transfer creates the implicit account). An upload costs it about 0.0004 NEAR. 3. In your backend: `const { nova, signer } = await connectNovaAgent(agentApi.forGrant(ngt), agentId, { store, maxDepositYocto })` from the `nova-agent-connect` reference module; `nova` is plain `nova-sdk-js`. Persist pending calls in `store`. After a `NativeCallPendingError`, call `signer.resolvePending()`, which uses the same idempotency key; never resend under a new key. Owner operations (live 2026-10-30): the agent registers and administers its own groups. Give `register_group` its own cap (`maxDepositByMethod: { register_group: 0.64 NEAR }`, owner policy cap ≥ 0.64 NEAR), restrict what it may sign with `methods`, and keep ~0.2 NEAR above the deposit on its account (`409 custody_native_balance_required`: the provider reserves the network fee). Refusals: `403 native_method_denied` (contract not in `call`), `409 policy_denied` (deposit over the cap, or the hourly action budget used up), `provider_rate_limited` (retryable, same key). A contract panic comes back as `FAILED (native_request_failed)` with the panic text. ## MCP tools (REST: `POST {MCP}/tools/`, JSON body; success `{"result": …}`, error `{"error": "…"}` with 401 auth / 500 other) Owner / account operations: - `register_group {group_id}` — caller becomes owner; generates the group key. - `add_group_member {group_id, member_id, access_minutes?}` / `revoke_group_member {group_id, member_id}` (revoke rotates the key). `access_minutes` (custodial owners only) sells a window: NOVA's scheduler revokes the member about a minute after expiry. `get_member_window {group_id, member_id}` (owner) reads it. - Self-custodial owners sign the contract call first, then call the tool (NOVA's side only). - `set_group_retention {group_id, retention_days | null}` — null clears (keep forever). - `create_hackathon_group {group_id, expires_at, max_uses?}` / `close_hackathon_join {group_id}`. Member operations: - `join_group {group_id}` — self-join an open joinable group. - `prepare_upload {group_id, filename}` → `{upload_id, key, …}` (self-custodial sessions also get `location`); encrypt client-side with `key`; `finalize_upload {upload_id, encrypted_data (base64), file_hash (sha256 hex of plaintext), format?, tx_hash?}` → `{location, cid, trans_id, file_hash}`. upload_id expires after 5 minutes. Self-custodial sessions first sign `record_transaction {group_id, user_id, file_hash, ipfs_hash: , backend: "FastFS"}` and pass its `tx_hash`. - `prepare_retrieve {group_id, ipfs_hash: }` → `{key, encrypted_b64, location, format}`; decrypt client-side. Reads: - `auth_status {group_id?}`, `get_owned_groups {}`, `get_member_groups {}`, `get_group_members {group_id}`, `get_group_transactions {group_id}` (audit trail incl. `deleted`). ## Costs (mainnet; custodial accounts pay from their NOVA account, self-custodial accounts from their own) register group ~0.64 NEAR · upload ~0.039 · retrieve ~0.013 · add member ~0.013 · revoke ~0.001. Use one group per tenant/customer, not per record. A self-custodial upload (gas plus the 0.0000256 NEAR fee) cost an Agent Connect agent about 0.0004 NEAR. ## Limits and current gaps - ~4 MB per file (FastFS / NEAR transaction size). Compress or downscale. - Group ids: use `[A-Za-z0-9._-]`, 1–64 chars (enforced from contract v0.3.7). - Self-custodial sessions (browser wallet / Agent Connect agent) read as members (list, retrieve, members) and write with their own signature: uploads, joins and, for groups they own, owner operations (wallets: live 2026-10-28; Agent Connect agents: member writes live 2026-10-29, owning and administering their own groups live 2026-10-30). - Access windows (`access_minutes`) need a custodial owner: the scheduler revokes by signing as the owner, and NOVA never signs for a self-custodial account. - Common pattern: a custodial owner account (your backend, API key) owns and administers the group; agents sign in as themselves, read, and upload with their own signature. - Retention sweeps and access-window revocations run in NOVA's TEE scheduler (on since 2026-10-26). - Legacy IPFS files remain readable; all new uploads go to FastFS. ## Rules for agents and integrators - Never put `nova_sk_` keys, `nova_session` tokens, or Agent Connect `naa_`/`ngt_` tokens in a browser, a URL, logs or a model prompt. - Owner operations (register, add/revoke members, retention) are signed by the group's owner: never give an agent another owner's API key or wallet key, and NOVA never performs them on anyone's behalf. An agent can own groups under its own account and administer them with its own signature; restrict in your backend which owner methods it may sign (`methods`, `maxDepositByMethod`). - Verify what you retrieved: recompute SHA-256 of the plaintext and compare with the on-chain `file_hash`. - Log SDK errors only with `nova-sdk-js` / `nova-sdk-rs` ≥ 1.4.1: earlier versions kept the request headers (API key, session) inside thrown errors. ## Links - [nova-sdk.com](https://nova-sdk.com): site, console, account and API key - [nova-sdk-js on npm](https://www.npmjs.com/package/nova-sdk-js): JS/TS SDK and README - [GitHub: jcarbonnell/nova](https://github.com/jcarbonnell/nova): source, issues - [NOVA docs](https://civictech-ou.gitbook.io/nova-docs/): longer guides - [Agent API native accounts (policy `native`)](https://github.com/NEAR-Intents-Agent-API/skills/blob/main/near-intents-agent-api/references/policy.md): `native.near.sign_message`, used above - [Agent API sign-and-verify example](https://github.com/NEAR-Intents-Agent-API/examples/tree/main/07-identity-signing): the reference relying-party flow NOVA implements - [nova-agent-connect](https://github.com/jcarbonnell/nova/tree/main/nova-agent-connect): reference module and guide for Agent Connect agents (self-custodial sign-in and writes)