# Objekt.sh > Decentralized media storage, encryption, and pay-to-reveal for ENS names. ## Features | Capability | CLI | REST | MCP | | -------------------------- | :-: | :--: | :-: | | Upload file (CDN) | ✅ | ✅ | ✅ | | Upload file (Arweave) | ✅ | ✅ | - | | Upload file (IPFS) | ✅ | ✅ | - | | Download file | ✅ | ✅ | ✅ | | Deploy static site | ✅ | ✅ | - | | ENS image uploads | ✅ | ✅ | - | | Set contenthash | ✅ | - | - | | ENS metadata (ERC-8004) | ✅ | - | - | | Encrypt for self | 🚧 | - | - | | Encrypt for address | 🚧 | - | - | | Encrypt for cross-chain | 🚧 | - | - | | Encrypt for many | 🚧 | - | - | | Multi-recipient encryption | 🚧 | - | - | | View keys | 🚧 | - | - | | Pay-to-reveal | 🚧 | - | - | | x402 payment (USDC) | ✅ | ✅ | - | | Lightning payment (BTC) | 🚧 | 🚧 | - | | Pay with (SOL) | 🚧 | 🚧 | - | | Pricing query | ✅ | ✅ | ✅ | **Legend:** ✅ available — 🚧 in progress — `-` not supported ### Auth | Method | CLI | REST | MCP | | ---------------------- | :-: | :--: | :-: | | OWS wallet | ✅ | - | - | | EIP-712 (viem, ethers) | - | ✅ | - | | API key | - | ✅ | ✅ | MCP API keys cannot upload to ENS-specific endpoints — ENS uploads require wallet-based ownership proof. See [MCP Server](/getting-started/mcp) for details. ## Storage Tiers Paid tiers use [x402](https://x402.org) with USDC on Base — no accounts or API keys needed. | Tier | Durability | Payment | | --------- | ----------------- | ------------ | | `cdn` | 90-day edge cache | Free | | `ipfs` | 12-month pin | USDC on Base | | `arweave` | Permanent | USDC on Base | :::info Pricing is dynamic (Arweave tracks the live AR/USD rate). Check current rates at [`api.objekt.sh/pricing`](https://api.objekt.sh/pricing) or run `objekt pricing`. ::: ### Usage ```bash # IPFS (default, 12-month pin) objekt upload file.png -w my-wallet # Free CDN (90-day cache) objekt upload file.png -w my-wallet --storage cdn # Arweave (permanent) objekt upload file.png -w my-wallet --storage arweave ``` ### Estimate Cost Check the price before uploading: ```bash objekt upload file.png -w my-wallet --storage arweave --estimate ``` ### Live Pricing ```bash objekt pricing ``` Arweave pricing is dynamic based on current AR/USD rates. ## Deposit & Purchase ### Deposit a View Key ```bash objekt reveal deposit 1a35e1.eth phone \ --view-key objekt_vk_abc123... \ --price 5.00 \ --content-uri ipfs://Qm... \ --ttl 7d \ -w my-wallet ``` | Option | Description | Default | | --------------- | --------------------------------------- | -------- | | `--view-key` | The view key to deposit | Required | | `--price` | Price in USD | Required | | `--content-uri` | Where the encrypted content lives | Required | | `--ttl` | Time to live (`30m`, `2h`, `1d`, `1w`) | `1d` | | `-w` | OWS wallet name (must own the ENS name) | Required | The CLI signs a `RevealDeposit` EIP-712 message. The server verifies the signature and checks ENS ownership (for `.eth` names) or address match (for `0x` addresses). ### List Available Keys ```bash objekt reveal list 1a35e1.eth ``` Returns all deposited keys with prices, content URIs, and expiry times. View keys are **never** included in list responses. ### Purchase a View Key ```bash objekt reveal buy 1a35e1.eth phone -w buyer-wallet ``` The CLI wraps the request with [x402](https://x402.org) payment. USDC is paid on Base directly to the content owner's address. On success, the view key is returned. ### Remove a Key ```bash objekt reveal remove 1a35e1.eth phone -w my-wallet ``` The current ENS owner (or address owner) can remove any deposited key. This allows cleanup after ENS transfers. ### TTL Entries auto-expire after the TTL. Supported formats: | Format | Duration | | ------ | ---------- | | `30m` | 30 minutes | | `2h` | 2 hours | | `1d` | 1 day | | `1w` | 1 week | Default is `1d`. Expired entries are automatically cleaned up by KV. ## Envelope Metadata Objekt encrypted envelopes are self-describing. Anyone who encounters a file on IPFS, Arweave, or ENS can inspect it without decrypting. ### Inspect an Envelope ```ts import { inspectEnvelope, isEncrypted } from "@objekt.sh/ecies"; if (isEncrypted(data)) { const info = inspectEnvelope(data); console.log(info.mime); // "image/png" console.log(info.access); // "reveal.objekt.sh/1a35e1.eth/phone" console.log(info.recipients); // [{ curve: 1, caip10: "eip155:1:0xabc..." }, ...] } ``` ### What's Public vs Private | Field | Visibility | Purpose | | ------------------- | ------------------ | --------------------------------------------------------------------------------- | | CBOR tag ("objekt") | Public | Format detection — 9-byte prefix check | | `mime` | Public | Original content type | | `access` | Public | Where to buy a view key (x402 URL) | | `r[].id` (CAIP-10) | Public | Who can decrypt (chain + address) | | `r[].pub` | Public | Recipient public key (needed for decryption matching) | | `r[].w` | Public but useless | Wrapped AES key — encrypted per recipient, can't be unwrapped without private key | | `ct` | Public but useless | Encrypted content — can't be decrypted without the AES key | The metadata is intentionally public. The security comes from the AES-256-GCM encryption of the content and the ECIES wrapping of the AES key per recipient. ### Discovery Flow When a client encounters an Objekt encrypted file: :::steps #### Detect Check the 9-byte prefix `[0xDB, 0x00, 0x00, 0x6F, 0x62, 0x6A, 0x65, 0x6B, 0x74]`. #### Inspect Parse the CBOR envelope to read metadata: MIME type, recipients (CAIP-10 addresses), and access URL. #### Try to decrypt If the client has a matching keypair (wallet-derived or view key), decrypt directly. #### Buy access If no keypair matches but `access` is present, fetch the reveal URL to see the price and pay via x402. #### Decrypt with view key After purchasing, use the returned view key to decrypt. ::: ### Embedding in ENS The `access` URL can be published as an ENS text record (e.g., `sh.objekt.reveal`) so wallets and dApps can discover paid content associated with an ENS name without fetching the encrypted file first. ## Reveal — Pay-to-Decrypt `reveal.objekt.sh` is a key escrow service that lets content owners sell access to encrypted content. Buyers pay USDC on Base via [x402](https://x402.org), and the view key is released. ### How It Works ``` Owner Reveal Service Buyer │ │ │ │ 1. encrypt content + view key │ │ │ 2. upload to IPFS/Arweave │ │ │ 3. deposit view key + price ────►│ │ │ │◄──── 4. pay USDC (x402) ────│ │ │───── 5. return view key ────►│ │ │ │ │ │ 6. decrypt content │ ``` ### Key Properties * **Owner sets the price** — quality signal for inbound contact * **Payment goes to the owner** — x402 `payTo` is the owner's address, not the protocol * **Content lives independently** — on IPFS or Arweave, persists even if the reveal service goes down * **Self-describing envelopes** — the encrypted file contains an `access` URL pointing to the reveal endpoint * **Dual namespace** — works with ENS names (`1a35e1.eth/phone`) or wallet addresses (`0xabc.../email`) * **TTL** — entries expire after a configurable duration (default 1 day) ### Endpoints | Endpoint | Description | | ----------------------------------- | ---------------------------------------------- | | `reveal.objekt.sh/1a35e1.eth` | List available keys and prices | | `reveal.objekt.sh/1a35e1.eth/phone` | Purchase a specific view key (x402) | | `reveal.objekt.sh/attestation` | TEE attestation proof (when deployed on Phala) | ## Trust Model The reveal service is a key escrow. Understanding its trust properties is important. ### Current: Encrypted-at-Rest Escrow (v1) The service encrypts view keys at rest using AES-256-GCM with a service secret. During a purchase, the key is decrypted in memory (\~1ms) and returned to the buyer. :::warning The service operator **can** read view keys. This is the same trust model as Gumroad, Stripe, or any payment-gated content service. ::: **Mitigations:** * View keys are encrypted at rest (KV breach alone doesn't leak keys) * Key exists in plaintext memory only during the response (\~1ms) * `sha256(viewKey)` commitment hash lets buyers verify they got the correct key * The service is separate from the storage gateway (different trust boundary) ### Future: Hardware TEE (v2) The service architecture is portable. The same code runs on Cloudflare Workers (v1) or inside a [Phala Cloud](https://phala.com) TEE (v2). When deployed on Phala: * **Intel TDX** hardware isolation — the operator **cannot** read view keys * **Remote attestation** — buyers cryptographically verify the TEE is genuine and running the expected code * **Sealed storage** — view keys are encrypted by the TEE, only decryptable inside the enclave The `GET /attestation` endpoint returns the TDX attestation proof when running on Phala, or `{ tee: false }` on CF Workers. ### Future: Merkle-Gated Access (idea) Merkle trees could gate *who* can purchase from the reveal service. The owner deposits a Merkle root of allowed addresses alongside the view key. Buyers submit a membership proof with their payment — the service verifies the proof before releasing the key. This separates access control (who can buy) from confidentiality (whether the processor can read the secret). In v1, the operator still sees the key. In v2 (TEE), Merkle verification happens inside the enclave — giving both allowlist gating and operator blindness. ### Considered Approaches Other approaches we evaluated: | Approach | Status | Notes | | ------------------ | -------------- | ------------------------------------------------------------------------------- | | **Umbral PRE** | Not viable | Single-proxy collusion recovers owner's master key. Unaudited WASM. | | **Ferveo DKG** | Not viable | Single-node is pointless. Multi-node on own infra is security theater. GPL-3.0. | | **TACo** | Not viable | No Base chain conditions. CF Worker incompatible. \~$4.4K/year. | | **Lit Protocol** | Not viable | V1 sunsetting. V3 unstable. Trust model shifting to single-TEE. | | **CF Workers TEE** | Does not exist | V8 isolates only. No SGX, no SEV, no attestation. | ## CAIP-10 Chains Objekt uses [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) namespaces for chain identification and [CAIP-10](https://chainagnostic.org/CAIPs/caip-10) for account addresses. Each namespace maps to an elliptic curve used for encryption key derivation. ### Supported Namespaces | Namespace | CAIP-2 | Curve | Key Size | | --------- | ---------- | --------- | --------------------- | | EIP155 | `eip155:*` | secp256k1 | 33 bytes (compressed) | | BIP122 | `bip122:*` | secp256k1 | 33 bytes (compressed) | | Cosmos | `cosmos:*` | secp256k1 | 33 bytes (compressed) | | Tron | `tron:*` | secp256k1 | 33 bytes (compressed) | | Spark | `spark:*` | secp256k1 | 33 bytes (compressed) | | Filecoin | `fil:*` | secp256k1 | 33 bytes (compressed) | | Solana | `solana:*` | X25519 | 32 bytes | | TON | `ton:*` | X25519 | 32 bytes | | Sui | `sui:*` | X25519 | 32 bytes | ### CAIP-10 Address Format ``` :: ``` #### Examples | Chain | CAIP-10 | | ---------------- | -------------------------------------------------------------------------------------- | | Ethereum mainnet | `eip155:1:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` | | Base | `eip155:8453:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` | | Bitcoin mainnet | `bip122:000000000019d6689c085ae165831e93:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4` | | Solana mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU` | ### Key Derivation Encryption keypairs are derived deterministically from a wallet signature: 1. Sign a fixed message: `"objekt.sh encryption key v1"` 2. Hash the signature with SHA-256 to get a 32-byte seed 3. Use the seed as a private key for the namespace's curve This means a single OWS wallet can derive encryption keys for every supported chain. ```bash # Show all derived encryption public keys objekt wallet encryption-key my-wallet ``` ### In the Envelope Recipients in an encrypted envelope are identified by their CAIP-10 address (optional `id` field) and their public key (always present). This allows clients to determine which chains can decrypt a given file without needing external metadata. ## EIP-712 Types All signatures use the same domain: ```ts domain: { name: "Objekt", version: "1" } ``` ### Upload (Storage Gateway) ```ts primaryType: "Upload" types: { Upload: [ { name: "upload", type: "string" }, // Upload type / key { name: "expiry", type: "string" }, // Timestamp ms { name: "name", type: "string" }, // ENS name or address { name: "hash", type: "string" }, // SHA-256 of file bytes ] } ``` ### RevealDeposit ```ts primaryType: "RevealDeposit" types: { RevealDeposit: [ { name: "action", type: "string" }, // "deposit" { name: "ensName", type: "string" }, // Namespace (ENS name or 0x address) { name: "keyName", type: "string" }, // Key name (e.g. "phone") { name: "commitment", type: "string" }, // sha256(viewKey) { name: "price", type: "string" }, // USD price (e.g. "5.00") { name: "expiry", type: "string" }, // Timestamp ms ] } ``` ### RevealRemove ```ts primaryType: "RevealRemove" types: { RevealRemove: [ { name: "action", type: "string" }, // "remove" { name: "ensName", type: "string" }, // Namespace { name: "keyName", type: "string" }, // Key name { name: "expiry", type: "string" }, // Timestamp ms ] } ``` ### Verification Server-side verification uses viem's `verifyTypedData`: ```ts import { verifyTypedData } from "viem/actions"; const valid = await verifyTypedData(client, { ...typedDataParameters, address: unverifiedAddress, signature: sig, message: { ... }, }); ``` After signature verification, the server checks that the recovered address owns the ENS name (via ensjs `getOwner`) or matches the `0x` namespace directly. ## Installation :::code-group ```bash [pnpm] pnpm add -g @objekt.sh/cli ``` ```bash [npm] npm install -g @objekt.sh/cli ``` ::: Verify the installation: ```bash objekt --help ``` ### Common Options These options are available on most commands: | Option | Description | | ----------- | --------------------------------------- | | `--network` | `mainnet` or `sepolia` | | `--testnet` | Use testnet (Base Sepolia for payments) | ## MCP Server MCP support is experimental as there are limited capablities in a sandbox. ### How we use it currently ```sh Publish that digram to objekt ``` ```sh Generate an SVG diagram of XX and upload it to objekt ``` Claude generates the SVG, uploads it via `upload_file` with `encoding: "raw"` — the SVG source goes straight through as content, no base64 waste. You get back a permalink instantly. This works for anything Claude can generate as text: SVG diagrams, HTML pages, CSS files, JSON data, Markdown docs. The content IS the payload — zero overhead. :::info **Binary files** (PNG, JPEG, PDF) in the sandbox require `api.objekt.sh` in the domain allowlist (Settings → Capabilities → Domain Allowlist). Use the `check_connection` tool to verify egress is working. ::: ### Setup ```bash pnpm add -g @objekt.sh/mcp-upload ``` #### Claude Desktop / Claude Code config ```json { "mcpServers": { "objekt": { "command": "npx", "args": ["-y", "@objekt.sh/mcp-upload"], "env": { "OBJEKT_API_KEY": "objekt_mcp_..." } } } } ``` Get a free key at [objekt.sh/mcp](https://objekt.sh/mcp). #### Node not found? (nvm / fnm users) Claude Desktop launches MCP servers with a minimal `$PATH` that doesn't include version managers like **nvm** or **fnm**. If you see `env: node: No such file or directory`, add your node's bin directory to the `PATH` env: ```bash # Run this to get your node bin directory: echo "\"PATH\": \"$(dirname $(realpath $(which node))):/usr/local/bin:/usr/bin:/bin\"" ``` Then paste the output into your config: ```json { "mcpServers": { "objekt": { "command": "npx", "args": ["-y", "@objekt.sh/mcp-upload"], "env": { "OBJEKT_API_KEY": "objekt_mcp_...", "PATH": "/Users/you/.../bin:/usr/local/bin:/usr/bin:/bin" } } } } ``` *** ### Tools | Tool | Description | | --------------------- | --------------------------------------------------------------- | | `upload_file` | Upload by host path, content (base64/raw), or generated text. | | `upload_from_sandbox` | Upload a file from a sandbox/container path directly. | | `get_file` | Get metadata for a previously uploaded file. | | `get_pricing` | Current storage tier pricing and limits. | | `check_connection` | Test egress to api.objekt.sh — diagnose sandbox network issues. | | `get_version` | Returns the installed MCP server version. | #### upload\_file | Parameter | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------- | | `path` | string | no | Absolute path on the **host** filesystem (e.g. `/Users/you/photo.png`). Works in Claude Code and Cursor. | | `content` | string | no | File content — base64 or raw UTF-8. | | `content_type` | string | no | MIME type (required with `content`). | | `encoding` | string | no | `"base64"` (default) or `"raw"`. Use `"raw"` for SVG, HTML, CSS, JSON. | | `name` | string | no | Filename. Auto-detected from `path`. **Required** when using `content`. | Provide either `path` or `content` + `content_type`. :::warning **Sandbox paths like `/mnt/user-data/` do not work with `path`.** The MCP server runs on the host, not inside the sandbox. For sandbox files, use the `content` parameter or `upload_from_sandbox`. ::: #### upload\_from\_sandbox | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------------------------------- | | `sandbox_path` | string | yes | Path inside the sandbox (e.g. `/mnt/user-data/uploads/photo.png`) | | `name` | string | no | Custom filename. | Reads the file from the sandbox filesystem and uploads it directly. Requires `api.objekt.sh` in the domain allowlist (Settings → Capabilities → Domain Allowlist). Use `check_connection` to verify egress is working. *** ### Best practices #### Text formats — use raw encoding SVG, HTML, CSS, JSON, Markdown — upload as `encoding: "raw"`. No base64, no inflation, no wasted tokens. ```json { "content": "...", "content_type": "image/svg+xml", "encoding": "raw", "name": "architecture.svg" } ``` #### Binary formats — use base64 PNG, JPEG, WebP, GIF, PDF — must be base64-encoded. Inline `content` is capped at 500KB to avoid burning tokens. For larger files, use `path` mode instead. #### Host files — use path If you're in Claude Code or Cursor and the file is on your real filesystem: ```json { "path": "/Users/you/Downloads/photo.png" } ``` Fastest option. Reads from disk, no encoding overhead. *** ### Remote server (alternative) For hosted agents and web-based MCP clients. Content is sent over the network via `mcp-remote`. Server URL: `https://mcp.objekt.sh/mcp` ```json { "mcpServers": { "objekt": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.objekt.sh/mcp", "--header", "Authorization: Bearer objekt_mcp_..." ] } } } ``` Same tools as the local server, but `path` mode is not available — use `content` + `content_type` only. *** ### MCP Pricing Due to environment contrains, The MCP interface requires an API key obtained from [objekt.sh/mcp](https://objekt.sh/mcp). | Plan | Cost | Storage | Retention | | ------- | ---------------------------------- | ------------------ | ---------------- | | Free | $0 | 25MB | 24 hours | | Starter | $4 | 1GB | 30-day CDN cache | | Pro | $8 | Unlimited fair use | 90-day CDN cache | | Custom | [DM @1a35e1](https://x.com/1a35e1) | Custom | Custom | #### Environment variable ```bash export OBJEKT_API_KEY=objekt_mcp_... ``` Or pass via `env` in the config. For the remote server, use `Authorization: Bearer` header. #### Check usage ```bash curl -H "Authorization: Bearer objekt_mcp_..." \ https://keys.objekt.sh/keys/objekt_mcp_.../balance ``` *** ### Supported formats JPEG, PNG, WebP, GIF, SVG, PDF — up to 5MB via `path`, 500KB via inline `content`. ## Quick Start ### Wallet Create or import a wallet using the [Open Wallet Standard](https://openwallet.sh/) to create an encrypted *keystore*. No more private keys in shell environments. [Read the spec](https://docs.openwallet.sh/). ```sh # create a local OWS wallet objekt wallet create my-wallet # or import an existing private key objekt wallet import my-wallet --private-key 0x... ``` ### Upload files Intuitive APIs for uploading files and deploying static sites. ```sh # upload a file objekt upload ./whitepaper.pdf -w my-wallet \ --storage arweave # or ipfs, cdn ``` ### Deploy folders Deploy a directory as a website. Free 7-day preview or permanent IPFS hosting. ```sh # preview (7 days, free) objekt deploy ./dist -w my-wallet # => https://tmp.objekt.sh/calm-fox-k7m/ # permanent (IPFS, paid) objekt deploy ./dist -w my-wallet --storage ipfs ``` ### Pricing Three storage tiers: **cdn** (free), **ipfs**, and **arweave** (permanent). Pricing is dynamic — check live rates at [`api.objekt.sh/pricing`](https://api.objekt.sh/pricing) or run `objekt pricing`. ### ENS support Upload avatars and headers for any ENS name you own. ```sh # set your ENS avatar on arweave objekt ens avatar upload 1a35e1.eth \ -f ./avatar.png -w my-wallet \ --storage arweave ``` ```json { "name": "1a35e1.eth", "kind": "image/png", "bytes": 84221, "uri": "ar://BzIbGE9Nl6Wqly...", "permalink": "https://ar.objekt.sh/BzIbGE9Nl6Wqly...", "payment": { "txHash": "0xfc88c3...", "explorerUrl": "https://basescan.org/tx/0xfc88c3..." } } ``` Point your ENS name to an IPFS site. Visit it at `yourname.eth.limo`. ```sh # set contenthash objekt ens contenthash set myname.eth \ "ipfs://QmRootCID" -w my-wallet # => https://myname.eth.limo ``` ## React / viem Upload to objekt.sh using `React + viem`. Works with Privy, wagmi, injected providers, or raw private keys. You can now offer users the ability to choose their durability preference. Use it at [https://ensmetadata.app/](https://ensmetadata.app/) ![Upload tier selector](/upload-tiers.png) ### Free tier (CDN) :::steps #### Create a wallet client ```ts import { createWalletClient, custom } from "viem"; import { mainnet } from "viem/chains"; import { sha256 } from "viem/utils"; const walletClient = createWalletClient({ account, // from useWallets(), useAccount(), privateKeyToAccount(), etc. chain: mainnet, transport: custom(window.ethereum), }); ``` #### Read the file and sign ```ts const buffer = await file.arrayBuffer(); const bytes = new Uint8Array(buffer); const dataURL = `data:${file.type};base64,${btoa(String.fromCharCode(...bytes))}`; const hash = sha256(bytes); const expiry = String(Date.now() + 60_000); const sig = await walletClient.signTypedData({ account, domain: { name: "Objekt", version: "1" }, types: { Upload: [ { name: "upload", type: "string" }, { name: "expiry", type: "string" }, { name: "name", type: "string" }, { name: "hash", type: "string" }, ], }, primaryType: "Upload", message: { upload: "avatar", expiry, name: "yourname.eth", hash, }, }); ``` #### Upload ```ts const res = await fetch("https://ens.objekt.sh/yourname.eth/avatar", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ expiry, dataURL, sig, unverifiedAddress: account.address, }), }); const { name, kind, bytes, permalink } = await res.json(); // permalink → https://ens.objekt.sh/yourname.eth/avatar ``` No payment, no API keys. That's it for the free tier. ::: ### Paid tier (Arweave / IPFS) For permanent storage, add `?storage=arweave` or `?storage=ipfs`. The server returns HTTP 402 and the client handles payment automatically. ```bash pnpm add @x402/core @x402/evm @x402/fetch ``` :::steps #### Create a payment-wrapped fetch ```ts import { x402Client } from "@x402/core/client"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { wrapFetchWithPayment } from "@x402/fetch"; const signer = { address: account.address, signTypedData: (msg) => walletClient.signTypedData({ account, ...msg }), }; const client = new x402Client(); client.register("eip155:8453", new ExactEvmScheme(signer)); const paidFetch = wrapFetchWithPayment(fetch, client); ``` #### Upload with payment ```ts const res = await paidFetch( "https://ens.objekt.sh/yourname.eth/avatar?storage=arweave", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ expiry, dataURL, sig, unverifiedAddress: account.address }), }, ); const { permalink, uri, contenthash } = await res.json(); // permalink → https://ar.objekt.sh/{txId} // uri → ar://{txId} ``` ::: ### Notes * **Expiry** — Signatures are valid for 60 seconds. Generate `expiry` right before uploading. * **Hash** — SHA-256 of the raw file bytes (not the base64 string). * **ENS ownership** — Server checks that the signer owns the ENS name on-chain. For `api.objekt.sh` general uploads, no ownership check is performed. * **CORS** — All endpoints allow `*` origins. `PAYMENT-REQUIRED` and `X-PAYMENT` headers are exposed for browser access. ## contenthash | CLI | REST | MCP | | :-: | :--: | :-: | | ✅ | - | - | Read and write the contenthash record on an ENS name. This is how ENS-native websites resolve via IPFS. ### get ```bash objekt ens contenthash get ``` ### set ```bash objekt ens contenthash set -w ``` Setting a contenthash requires an on-chain transaction — the CLI will prompt for confirmation. ## images | CLI | REST | MCP | | :-: | :--: | :-: | | ✅ | ✅ | - | Upload, retrieve, and set ENS image records. Both `avatar` and `header` follow the same interface. | Type | Formats | Max Size | | ------ | --------------- | -------- | | avatar | JPEG, PNG, WebP | 512KB | | header | JPEG, PNG, WebP | 1MB | #### Options | Option | Description | Default | | --------------- | ------------------------------------ | -------- | | `-f` | Path to image file | Required | | `-w` | OWS wallet name | Required | | `--storage` | `cdn`, `ipfs`, or `arweave` | `ipfs` | | `--encrypt` | Encrypt for self | `false` | | `--encrypt-for` | Recipient public keys or ENS names | — | | `--view-key` | Generate a shareable view key | `false` | | `--estimate` | Show cost estimate without uploading | — | ### upload ```bash objekt ens upload -f -w [options] ``` ### get ```bash objekt ens get ``` ### set Set the ENS text record on-chain: ```bash objekt ens set -w ``` ## ENS Metadata Manage AI agent metadata on ENS names using the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) standard. View, set, validate, and template metadata records. Powered by [`@ensmetadata/cli`](https://www.npmjs.com/package/@ensmetadata/cli), mounted as `objekt ens metadata`. ### View metadata ```bash objekt ens metadata view 1a35e1.eth ``` ```bash objekt ens metadata view 1a35e1.eth --json ``` ### Set metadata Dry run (shows what would be set, estimates gas): ```bash objekt ens metadata set 1a35e1.eth agent.json --private-key 0x... ``` Broadcast on-chain: ```bash objekt ens metadata set 1a35e1.eth agent.json --private-key 0x... --broadcast ``` ### Validate a payload ```bash objekt ens metadata validate agent.json ``` ### Generate a template ```bash objekt ens metadata template > agent.json ``` Edit the generated file, then validate and set. ### Payload format The payload follows the [ERC-8004 v2.0 agent registration spec](https://best-practices.8004scan.io/docs/01-agent-metadata-standard.html). Key fields: ```json { "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", "name": "My Agent", "description": "What this agent does", "services": [ { "name": "MCP", "endpoint": "https://mcp.example.com", "version": "2025-11-25" }, { "name": "A2A", "endpoint": "https://example.com/.well-known/agent-card.json", "version": "0.3.0" }, { "name": "web", "endpoint": "https://example.com" } ], "active": true, "x402Support": true } ``` #### Supported service types | Service | Description | | ------------- | -------------------------------------- | | `MCP` | Model Context Protocol server endpoint | | `A2A` | Agent-to-Agent protocol card URL | | `OASF` | OASF schema endpoint | | `agentWallet` | CAIP-10 wallet address | | `web` | Human-facing web UI | | `email` | Support email | | `ENS` | ENS name | | `DID` | Decentralized identifier | Custom service types are also supported. ### How it works Metadata is stored as ENS text records on-chain. The `set` command writes key-value pairs to your ENS name's resolver via `setText`. Any ENS-aware application can read these records. The schema CID is automatically injected from the [published registry](https://best-practices.8004scan.io), linking your metadata to the standard for discoverability. :::info The `set` command requires a private key for signing the on-chain transaction. This is separate from OWS — it uses a raw private key directly for ENS record writes. ::: ## Envelope Format Objekt encrypted envelopes use [CBOR](https://cbor.io) (RFC 8949) with a registered tag for instant detection. ### Detection Every envelope starts with a 9-byte CBOR tag prefix: ``` 0xDB 0x00 0x00 0x6F 0x62 0x6A 0x65 0x6B 0x74 ^tag(8-byte) ^---------- "objekt" ----------^ ``` This encodes CBOR tag `122,511,826,820,980` — the ASCII bytes of "objekt" as a 48-bit integer. Any standard CBOR decoder reads this as `Tag(122511826820980, )`. Detection is a 9-byte prefix check — no CBOR decoding needed: ```ts import { isEncrypted } from "@objekt.sh/ecies"; if (isEncrypted(data)) { // It's an Objekt encrypted envelope } ``` ### Structure ```sh Tag(122511826820980) { v: 2, // Version mime: "image/png", // Original content MIME type access: "reveal.objekt.sh/1a35e1.eth/phone", // x402 reveal URL (optional) r: [ // Recipient stanzas { c: 1, // CurveId (1 = secp256k1, 2 = X25519) pub: Uint8Array(33), // Recipient compressed public key epk: Uint8Array(33), // Ephemeral public key (ECDH) w: Uint8Array(60), // Wrapped AES key (nonce + ciphertext + GCM tag) id: "eip155:1:0xabc...", // CAIP-10 address (optional) }, ], ct: Uint8Array, // Content ciphertext (nonce + encrypted + GCM tag) } ``` ### Fields | Field | Type | Description | | --------- | --------- | ---------------------------------------------------------------- | | `v` | `number` | Envelope version (currently `2`) | | `mime` | `string` | Original MIME type of the plaintext content | | `access` | `string?` | URL where a view key can be purchased via x402 | | `r` | `array` | Recipient stanzas — one per authorized decryptor | | `r[].c` | `number` | Curve ID: `1` = secp256k1, `2` = X25519 | | `r[].pub` | `bytes` | Recipient's compressed public key | | `r[].epk` | `bytes` | Ephemeral public key used in ECDH key agreement | | `r[].w` | `bytes` | Wrapped AES-256 key (12B nonce + 32B ciphertext + 16B GCM tag) | | `r[].id` | `string?` | Recipient's CAIP-10 address | | `ct` | `bytes` | AES-256-GCM encrypted content (12B nonce + ciphertext + 16B tag) | ### Why CBOR? * **Self-describing** — parseable decades later without external schema * **Binary-native** — `Uint8Array` values encode directly (no base64 overhead) * **Extensible** — add fields without breaking old parsers * **IPFS-native** — CBOR is the encoding used by IPLD, the data model behind IPFS * **Compact** — smaller than JSON, smaller than the previous hand-rolled binary format * **Tagged** — CBOR tags are specifically designed for format identification ## Encryption :::warning Experimental — under heavy R\&D. Do not use for sensitive workloads. ::: Objekt provides end-to-end, client-side encryption via the `@objekt.sh/ecies` package. Content is encrypted before it leaves your machine — the server never sees plaintext. ### Key Features * **Multi-recipient** — Encrypt once, wrap the key for multiple recipients * **Multi-curve** — secp256k1 (EVM, Bitcoin, Cosmos) and X25519 (Solana, TON, Sui) * **View keys** — Generate shareable decryption keys without sharing wallets * **CBOR envelope** — Self-describing binary format with CAIP-10 recipient addressing * **Access metadata** — Embedded x402 reveal URLs for pay-to-decrypt ### Supported Chains | Namespace | Curve | Chains | | --------- | --------- | -------------------------------------- | | EIP155 | secp256k1 | Ethereum, Base, Polygon, Arbitrum, ... | | BIP122 | secp256k1 | Bitcoin | | Cosmos | secp256k1 | Cosmos Hub, Osmosis, ... | | Tron | secp256k1 | Tron | | Solana | X25519 | Solana | | TON | X25519 | TON | | Sui | X25519 | Sui | ### How It Works ``` Plaintext + nonce → [AES-256-GCM] → Content ciphertext Recipient 1 pubkey + AES key → [ECIES secp256k1] → Wrapped key #1 Recipient 2 pubkey + AES key → [ECIES X25519] → Wrapped key #2 View key pubkey + AES key → [ECIES secp256k1] → Wrapped key #3 CBOR envelope = Tag("objekt") + version + MIME + recipients + ciphertext ``` A random AES-256 key encrypts the content once. That key is then wrapped individually for each recipient using curve-appropriate ECIES (ECDH + HKDF-SHA256 + AES-256-GCM). Any recipient can independently decrypt. ## Multi-Recipient Encryption Objekt encrypts content once and wraps the AES key individually for each recipient. Recipients decrypt independently — no coordination needed. ### Encrypt for Multiple Recipients ```bash objekt upload secret.pdf -w my-wallet \ --encrypt \ --encryptFor 0x02abc...def \ --encryptFor 1a35e1.eth ``` Recipients can be specified as: * **Hex public key** — `0x02...` (33 bytes secp256k1) or `0x...` (32 bytes X25519) * **ENS name** — resolves the `sh.objekt.encpubkey` text record ### Cross-Chain Recipients A single file can be encrypted for recipients on different chains simultaneously: ``` Recipient 1: eip155:1:0xabc... (Ethereum, secp256k1) Recipient 2: solana:5eykt:7xKX... (Solana, X25519) Recipient 3: bip122:000...000:bc1q... (Bitcoin, secp256k1) ``` The CLI derives chain-specific encryption keypairs from your wallet signature: ```bash # Show your encryption public keys per chain objekt wallet encryption-key my-wallet ``` ### CAIP-10 Addressing Recipients are identified by [CAIP-10](https://chainagnostic.org/CAIPs/caip-10) addresses in the envelope: ``` :: ``` Examples: * `eip155:1:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` (Ethereum mainnet) * `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU` (Solana) * `bip122:000000000019d6689c085ae165831e93:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4` (Bitcoin) This is embedded in the `id` field of each recipient stanza, making the envelope self-describing across all chains. ### How Decryption Works The decryptor provides their keypairs. The envelope is scanned for a matching `(curve, publicKey)` pair: ```ts import { decryptEnvelope } from "@objekt.sh/ecies"; const { plaintext, mime, access } = decryptEnvelope(envelope, keypairs); ``` If no keypair matches but an `access` URL is present, the decryptor can purchase a view key from the reveal service. ## View Keys View keys let anyone decrypt content without needing a wallet. They're shareable, single-purpose decryption keys. ### How They Work A view key is a random secp256k1 keypair. The public key is added as a recipient during encryption. The private key (the "view key") is shared with whoever should have access. ``` objekt_vk_a1b2c3d4e5f6... ← 32-byte private key, hex-encoded with prefix ``` ### Generate a View Key ```bash objekt upload secret.txt -w my-wallet --encrypt --view-key --storage ipfs ``` Output: ```json { "uri": "ipfs://Qm...", "permalink": "https://ipfs.objekt.sh/Qm...", "viewKey": "objekt_vk_a1b2c3d4e5f6..." } ``` ### Decrypt with a View Key ```bash objekt get --view-key objekt_vk_a1b2c3d4e5f6... ``` ### Programmatic Usage ```ts import { generateViewKey, parseViewKey, encryptForRecipients, decryptEnvelope } from "@objekt.sh/ecies"; // Generate const { viewKey, recipient, keypair } = generateViewKey(); // Add as recipient during encryption const envelope = encryptForRecipients(plaintext, [ { pubKey: ownerKey.publicKey, curve: ownerKey.curve }, recipient, // view key recipient ], { mime: "text/plain" }); // Decrypt with view key const parsed = parseViewKey(viewKey); const { plaintext: decrypted } = decryptEnvelope(envelope, [parsed]); ``` ### View Keys + Reveal View keys are the bridge between encryption and the reveal service. The flow: :::steps #### Encrypt with a view key ```bash objekt upload secret.txt -w my-wallet --encrypt --view-key --storage ipfs ``` #### Deposit the view key for sale ```bash objekt reveal deposit 1a35e1.eth phone \ --view-key objekt_vk_... \ --price 5.00 \ --content-uri ipfs://Qm... \ -w my-wallet ``` #### Buyer purchases access ```bash objekt reveal buy 1a35e1.eth phone -w buyer-wallet # → Returns the view key after USDC payment ``` #### Buyer decrypts ```bash objekt get ipfs://Qm... --view-key objekt_vk_... ``` ::: ### Commitment Hash When depositing a view key, the reveal service computes `sha256(viewKey)` as a commitment hash. This can be published as an ENS text record so buyers can verify they received the correct key after purchase. ## deploy | CLI | REST | MCP | | :-: | :--: | :-: | | ✅ | - | - | Deploy a static site to a temporary preview URL or permanently to IPFS. ```bash objekt deploy -w [options] ``` ### Options | Option | Description | Default | | ----------- | ------------------------------------------- | -------- | | `-w` | OWS wallet name | Required | | `--storage` | `tmp` (7-day preview) or `ipfs` (permanent) | `tmp` | ### list List previous deployments: ```bash objekt deploy list ``` ### Examples Free preview (7 days): ```bash objekt deploy ./dist -w my-wallet ``` Permanent on IPFS: ```bash objekt deploy ./dist -w my-wallet --storage ipfs ``` ### Publish on ENS Publish on ENS by setting the contenthash on an ENS node: ```bash objekt ens contenthash set name.eth "ipfs://QmRootCID..." -w my-wallet ``` Once set, your site is live at `https://name.eth.limo` via [eth.limo](https://eth.limo). :::info * SSL certs may take up to 30mins to resolve. * You can publish websites many subnames deep! ::: ### Gateways | Gateway | URL | Description | | ----------------- | ---------------------- | -------------------- | | eth.limo | `yourname.eth.limo` | Public ENS gateway | | objekt.sh IPFS | `ipfs.objekt.sh/{cid}` | Direct CID access | | objekt.sh preview | `tmp.objekt.sh/{slug}` | 7-day preview (free) | ## get | CLI | REST | MCP | | :-: | :--: | :-: | | ✅ | ✅ | ✅ | Download media by key: ```bash objekt get [options] ``` ### Options | Option | Description | | ------------ | --------------------------------------------- | | `-w` | OWS wallet (for decrypting encrypted content) | | `--view-key` | View key string (`objekt_vk_...`) | | `--output` | Save to file path | ### Examples Download a file: ```bash objekt get proposals/0x.../media/abc ``` Decrypt with your wallet: ```bash objekt get proposals/0x.../media/abc -w my-wallet ``` Decrypt with a view key: ```bash objekt get proposals/0x.../media/abc --view-key objekt_vk_... ``` Save to a file: ```bash objekt get proposals/0x.../media/abc --output ./downloaded.pdf ``` ## reveal | CLI | REST | MCP | | :-: | :--: | :-: | | 🚧 | - | - | Pay-to-reveal key escrow. Deposit encrypted view keys for sale, and let buyers purchase access with USDC on Base via [x402](https://x402.org). ### deposit Deposit a view key for sale: ```bash objekt reveal deposit [options] ``` #### Options | Option | Description | Default | | --------------- | -------------------------------------- | -------- | | `--view-key` | View key to deposit | Required | | `--price` | Price in USD (e.g. `5.00`) | Required | | `--content-uri` | Content URI (`ar://...`, `ipfs://...`) | Required | | `--ttl` | Time to live (`30m`, `2h`, `1d`, `1w`) | `1d` | | `-w` | OWS wallet (must own the namespace) | Required | #### Example ```bash objekt reveal deposit 1a35e1.eth phone \ --view-key objekt_vk_abc123... \ --price 5.00 \ --content-uri ipfs://Qm... \ --ttl 7d \ -w my-wallet ``` ### buy Purchase a view key: ```bash objekt reveal buy -w ``` ### list List available keys for a namespace: ```bash objekt reveal list ``` ### remove Remove a deposited key: ```bash objekt reveal remove -w ``` ## upload | CLI | REST | MCP | | :-: | :--: | :-: | | ✅ | ✅ | ✅ | Upload a file: ```bash objekt upload -w [options] ``` ### Options | Option | Description | Default | | --------------- | ----------------------------------------- | -------- | | `-k` | Custom storage key (defaults to filename) | — | | `--storage` | `cdn`, `ipfs`, or `arweave` | Required | | `--encrypt` | Encrypt for self | `false` | | `--encrypt-for` | Recipient public keys or ENS names | — | | `--view-key` | Generate a shareable view key | `false` | | `--estimate` | Show cost estimate without uploading | — | | `-w` | OWS wallet name | Required | ### Examples Upload to IPFS: ```bash objekt upload photo.png -w my-wallet --storage ipfs ``` Upload with encryption and a view key: ```bash objekt upload secret.pdf -w my-wallet --encrypt --view-key --storage ipfs ``` Encrypt for a specific recipient: ```bash objekt upload doc.pdf -w my-wallet --encrypt --encrypt-for vitalik.eth ``` Estimate cost before uploading: ```bash objekt upload large-file.zip -w my-wallet --storage arweave --estimate ``` ## wallet | CLI | REST | MCP | | :-: | :--: | :-: | | ✅ | - | - | Objekt uses [Open Wallet Standard](https://openwallet.sh) (OWS) for key management. Keys are encrypted locally in `~/.ows/`. ### create Create a new wallet: ```bash objekt wallet create ``` ### import Import an existing private key: ```bash objekt wallet import --private-key 0x... ``` ### list List all wallets: ```bash objekt wallet list ``` ### encryption-key Show encryption public keys per chain: ```bash objekt wallet encryption-key objekt wallet encryption-key --chain eip155 ```