# Inkd Protocol > On-chain project registry for AI agents and developers. Built on Base, stored on Arweave. ## Inkd Protocol **Inkd is the ownership layer for AI agents and developers.** Projects and versions are registered on-chain on Base. Content lives permanently on Arweave. Payments happen natively in USDC — no API keys, no OAuth, no accounts. *** ### The problem Software registries today are centralized. npm, PyPI, Docker Hub — they can ban packages, go offline, change pricing, or shut down entirely. For AI agents that need to publish and discover code autonomously, this is a fundamental reliability problem. ### The solution Inkd replaces the centralized registry with on-chain state and permanent storage: * **On-chain registry** — Project ownership, version history, and metadata live on Base. Immutable, verifiable, censorship-resistant. * **Permanent content** — Files are stored on Arweave via Irys. Once uploaded, they exist forever. * **USDC payments** — Agents pay directly with USDC via [x402](/concepts/x402). No accounts, no API keys — your wallet is your identity. *** ### Core primitives #### Projects A project is an on-chain record with a unique name, owner address, license, and metadata. Once created, it exists forever on Base. ``` Project { id: uint256 // auto-incrementing name: string // unique, max 64 chars owner: address // controls the project license: string // SPDX identifier isPublic: bool isAgent: bool // AI agent flag versionCount: uint256 } ``` #### Versions A version links a project to a permanent Arweave content hash. Every version is immutable once pushed. ``` Version { projectId: uint256 arweaveHash: string // ar://... permanent content address versionTag: string // e.g. "v1.0.0" changelog: string pushedBy: address pushedAt: uint256 } ``` *** ### Who it's for **AI agents** — Autonomous agents can publish, version, and discover software without human-managed credentials. A wallet + USDC is all that's needed. **Developers** — Permanent, verifiable artifact storage. Every version is on-chain with a timestamp, owner proof, and permanent content link. *** ### Contracts (Base Mainnet) | Contract | Address | | ------------ | ----------------------------------------------------------------------------------------------------------------------- | | InkdRegistry | [`0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d`](https://basescan.org/address/0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d) | | InkdTreasury | [`0x23012C3EF1E95aBC0792c03671B9be33C239D449`](https://basescan.org/address/0x23012C3EF1E95aBC0792c03671B9be33C239D449) | | InkdBuyback | [`0xcbbf310513228153D981967E96C8A097c3EEd357`](https://basescan.org/address/0xcbbf310513228153D981967E96C8A097c3EEd357) | All contracts are open-source, verified on Basescan, and upgradeable via multisig-controlled UUPS proxies. *** ### Start building :::steps #### Install the CLI ```bash npm install -g @inkd/cli ``` #### Create a project ```bash export INKD_PRIVATE_KEY=0x... inkd project create --name my-agent --description "My agent" ``` #### Push a version ```bash inkd version push --id 1 --file ./dist/agent.js --tag v1.0.0 ``` ::: ## Quickstart This guide walks you through creating a project and pushing your first version using the Inkd CLI. ### Prerequisites * Node.js 18 or later * A wallet with Base Mainnet USDC and a small amount of ETH for gas * Your wallet's private key \:::tip Getting USDC on Base Buy USDC on [Coinbase](https://coinbase.com) and send to Base, or bridge from Ethereum at [bridge.base.org](https://bridge.base.org). \::: *** ### 1. Install the CLI ```bash npm install -g @inkd/cli ``` Verify the installation: ```bash inkd --version # @inkd/cli 0.1.0 ``` *** ### 2. Configure your wallet ```bash export INKD_PRIVATE_KEY=0xYOUR_PRIVATE_KEY export INKD_NETWORK=mainnet ``` Check that everything is connected: ```bash inkd status ``` ``` Inkd Protocol ──────────────────────────────────── Network: mainnet (Base, chain 8453) Registry: 0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d Wallet: 0xYourWallet USDC: 12.50 Projects: 9 registered ``` *** ### 3. Create a project ```bash inkd project create \ --name my-agent \ --description "My first on-chain project" \ --license MIT \ --public ``` The CLI handles the x402 payment automatically: ``` → Requesting payment terms from API... → Paying $0.10 USDC via EIP-3009... → Submitting transaction... ✓ Project created Name: my-agent Project ID: 10 Owner: 0xYourWallet TX: 0xabc... Basescan: https://basescan.org/tx/0xabc... ``` :::note Project names are **global and permanent** — once a name is taken, it cannot be reused by anyone. Names are normalized to lowercase. Max 64 characters. ::: *** ### 4. Push a version Upload a file and register it on-chain: ```bash inkd version push \ --id 10 \ --file ./dist/agent.js \ --tag v1.0.0 \ --changelog "Initial release" ``` ``` → Uploading dist/agent.js (14.2 KB) to Arweave... → Uploaded: ar://QmAbc123xyz... → Paying for version push (Arweave cost + 20% markup)... → Submitting transaction... ✓ Version pushed Tag: v1.0.0 Content: ar://QmAbc123xyz... URL: https://arweave.net/QmAbc123xyz TX: 0xdef... ``` Already have an Arweave hash? Skip the upload: ```bash inkd version push --id 10 --hash ar://QmAbc123xyz --tag v1.0.0 ``` *** ### 5. Verify on-chain ```bash inkd project get 10 ``` ``` Project #10: my-agent ──────────────────────────────────── Owner: 0xYourWallet License: MIT Public: true Versions: 1 Created: 2026-03-07 20:40 UTC ``` ```bash inkd version list 10 ``` ``` Versions for my-agent (1 total) ──────────────────────────────────────────────────────── #0 v1.0.0 ar://QmAbc123xyz… 2026-03-07 20:42 UTC ``` *** ### What's next * [SDK](/sdk/installation) — Use `ProjectsClient` in your agent or TypeScript app * [API Reference](/api/overview) — Call the HTTP API from any language * [x402 Payments](/concepts/x402) — Understand how payments work under the hood ## Security Inkd Protocol has undergone **4 internal audit passes** covering all smart contracts. This page documents the methodology, findings, and current security posture. *** ### Audit summary | Audit | Scope | Findings | Status | | ------------------------------ | ------------------- | --------------- | ------- | | Pass 1 — Initial review | All contracts | 3 Medium, 5 Low | ✅ Fixed | | Pass 2 — Slither deep scan | All contracts | 2 Medium, 6 Low | ✅ Fixed | | Pass 3 — Storage & slippage | Buyback, RegistryV2 | 1 Medium | ✅ Fixed | | Pass 4 — Professional criteria | All contracts | 1 Medium, 3 Low | ✅ Fixed | **321/321 tests passing** — unit, fuzz (256 runs each), and invariant tests. *** ### Findings (resolved) #### Medium **\[FIXED] Buyback `deposit()` permissionless — event spoofable** Anyone could call `deposit(uint256.max)` and emit a misleading `Deposited` event with a fake amount. Fix: `require(msg.sender == treasury)` — only the Treasury contract can call `deposit()`. **\[FIXED] Registry reentrancy — state updates after external calls** In `pushVersion` and `transferProject`, state was updated after external Treasury calls. Fix: CEI pattern applied — all state updates happen before external calls. **\[FIXED] Buyback slippage — `amountOutMinimum: 0`** No slippage protection on Uniswap swaps. Susceptible to sandwich attacks. Fix: `amountOutMinimum = usdcIn * (10000 - maxSlippageBps) / 10000`. Default 1%, max 10%. #### Low **\[FIXED] Empty `arweaveHash` and `versionTag` accepted** `pushVersion` allowed empty strings, storing garbage on Arweave permanently. Fix: `revert EmptyArweaveHash()` and `revert EmptyVersionTag()`. **\[FIXED] `settle(0, 0)` allowed** Treasury `settle` could be called with zero amount — wasteful on-chain call. Fix: `require(total > 0, "Nothing to settle")`. **\[FIXED] Unbounded collaborators — gas DoS** No limit on collaborators per project. `removeCollaborator` is O(n) — large arrays could hit gas limits. Fix: `revert TooManyCollaborators()` at 50 collaborators. **\[FIXED] Missing events on admin setters** `setRegistry()` and `setDefaultFee()` in Treasury had no events. Fix: `emit RegistrySet()` and `emit DefaultFeeSet()`. **\[FIXED] Timelock missing zero-address checks** `constructor`, `setPendingAdmin`, and `executeTransaction` lacked zero-address validation. Fix: `require(addr != address(0))` added to all three. **\[FIXED] No storage gap in InkdRegistryV2** Without `__gap`, future V3 upgrades would risk storage collisions. Fix: `uint256[42] private __gap` added to InkdRegistryV2 (not yet deployed). *** ### Test coverage | Suite | Tests | Method | | -------------- | ----- | -------------------- | | InkdRegistry | 77 | Unit + edge cases | | InkdTreasury | 29 | Unit + split math | | InkdBuyback | 23 | Unit + slippage | | InkdToken | 19 | Unit | | InkdTimelock | 10 | Unit | | InkdRegistryV2 | 27 | Unit | | InkdUpgrade | 16 | Upgrade safety | | InkdFuzz | 29 | Fuzz (256 runs each) | | InkdInvariant | 10 | Invariant (256 runs) | **Total: 321 tests, 0 failures.** Fuzz tests cover: token transfers, treasury split math, markup bounds, fee access control, project ID monotonicity, version count, collaborator push/transfer/remove, fee deduction. Invariants verified: supply never increases, registry holds no ETH, projectCount matches state, all projects have non-zero owner, names always marked taken, version counts match struct, markupBps ≤ 5000. *** ### Static analysis All contracts are analyzed with [Slither](https://github.com/crytic/slither) on every commit. **Current status:** 0 High, 0 Medium findings in production code. Remaining informational findings are all suppressed with documented justification: * `timestamp` detector false positives on address equality comparisons * `reentrancy-events` in Timelock (emit after external call is required for timelock semantics) * `missing-zero-check` on Buyback `inkdToken_` (intentionally allowed to be `address(0)` pre-launch) *** ### Centralization risks | Risk | Mitigation | | ---------------------- | ------------------------------------------------------------------ | | Contract upgrades | Require 2-of-2 multisig (Safe) | | Treasury withdrawals | Require 2-of-2 multisig (Safe) | | Buyback `setInkdToken` | Require 2-of-2 multisig (Safe) | | API server compromise | Settler role only calls `settle()` — cannot move funds arbitrarily | | Arweave wallet | Receives USDC only — no admin power over contracts | *** ### Known limitations * **No external audit** — the protocol has undergone rigorous internal audits but has not been reviewed by an external firm (Trail of Bits, OpenZeppelin, etc.). An external audit is recommended before significant TVL accumulates. * **Slippage model** — `amountOutMinimum` in Buyback is a fraction of USDC in, not a true price-oracle-based check. Owner should update `maxSlippageBps` based on pool depth. * **Arweave is public** — all content uploaded via the API is permanently public. Private content should be encrypted before upload. *** ### Responsible disclosure Found a vulnerability? Email **[security@inkdprotocol.com](mailto\:security@inkdprotocol.com)** with details. We will respond within 48 hours and coordinate a fix before public disclosure. Do not open public GitHub issues for security vulnerabilities. `AgentVault` lets agents store sensitive credentials (API keys, private configs) encrypted on Arweave, keyed to their wallet. Only the wallet that stored them can retrieve them. ### How it works Credentials are encrypted with ECIES using the agent's wallet public key. The ciphertext is stored on Arweave. The Arweave hash is kept locally (or in the registry). Only the wallet's private key can decrypt. ### Usage ```typescript import { AgentVault } from "@inkd/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const vault = new AgentVault({ account }); // Store credentials const { hash } = await vault.store({ openaiKey: "sk-...", discordToken: "Bot ...", }); console.log(hash); // ar://QmVault... // Later: retrieve const credentials = await vault.load(hash); console.log(credentials.openaiKey); // "sk-..." ``` ### API #### `store(data)` Encrypts `data` with the wallet's public key and uploads to Arweave. ```typescript const { hash, txId } = await vault.store({ key: "value" }); ``` #### `load(hash)` Fetches the ciphertext from Arweave and decrypts with the wallet's private key. ```typescript const data = await vault.load("ar://QmVault..."); ``` #### `seal(data)` Encrypt only (no upload). Returns the ciphertext buffer. ```typescript const ciphertext = await vault.seal({ key: "value" }); ``` #### `unseal(ciphertext)` Decrypt only (no Arweave fetch). ```typescript const data = await vault.unseal(ciphertext); ``` ## SDK Installation The `@inkd/sdk` package provides a TypeScript client for the Inkd Protocol. It handles x402 payments automatically. *** ### Install ```bash npm install @inkd/sdk # or yarn add @inkd/sdk # or pnpm add @inkd/sdk ``` **Requirements:** Node.js 18 or later, TypeScript 5.0+ (if using TypeScript). *** ### Quick setup ```typescript import { ProjectsClient } from "@inkd/sdk" const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY, }) ``` That's it. The client connects to Base Mainnet and the production API by default. *** ### Configuration ```typescript const client = new ProjectsClient({ // Required: your wallet private key (never hardcode in source) privateKey: process.env.INKD_PRIVATE_KEY, // Optional: override API base URL apiUrl: "https://api.inkdprotocol.com", // Optional: override network ("mainnet" | "testnet") network: "mainnet", }) ``` #### Environment variables | Variable | Description | | ------------------ | ------------------------------------------- | | `INKD_PRIVATE_KEY` | Your wallet private key (`0x...`) | | `INKD_NETWORK` | `mainnet` or `testnet` (default: `mainnet`) | | `INKD_API_URL` | Override API base URL | *** ### Wallet requirements Your wallet needs: * **USDC on Base Mainnet** — for paying fees (create project: $0.10+, push version: dynamic) * **ETH on Base Mainnet** — a small amount for gas on any direct contract calls To check your balance: ```typescript const status = await client.getStatus() console.log(status.usdcBalance) // e.g. "12.50" ``` *** ### Using in an AI agent For autonomous agents, load the private key from a secure environment: ```typescript import { ProjectsClient } from "@inkd/sdk" // From environment (recommended) const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY, }) // Or from AgentVault (encrypted credential storage) import { AgentVault } from "@inkd/sdk" const vault = new AgentVault(walletClient) const key = await vault.get("inkd_private_key") const client = new ProjectsClient({ privateKey: key }) ``` *** ### Next steps * [ProjectsClient](/sdk/projects-client) — Create projects, push versions, read the registry * [AgentVault](/sdk/agent-vault) — Encrypted credential storage for agents ## ProjectsClient `ProjectsClient` is the main entry point for interacting with the Inkd Protocol. It handles x402 payments automatically. ```typescript import { ProjectsClient } from "@inkd/sdk" const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY, }) ``` *** ### Projects #### `createProject` Register a new project on-chain. Costs $0.10+ USDC (paid automatically via x402). ```typescript const project = await client.createProject({ name: "my-agent", // required, max 64 chars, globally unique description: "My agent project", // optional, max 1024 chars license: "MIT", // optional, SPDX identifier isPublic: true, // optional, default true isAgent: true, // optional, marks as AI agent agentEndpoint: "https://...", // optional, agent HTTP endpoint readmeHash: "ar://...", // optional, Arweave README hash }) console.log(project.id) // "10" console.log(project.txHash) // "0xabc..." ``` **Returns:** ```typescript { id: string // on-chain project ID name: string owner: string // your wallet address txHash: string blockNumber: string } ``` **Throws:** `NameTaken`, `NameTooLong`, `InsufficientFunds` *** #### `getProject` ```typescript const project = await client.getProject(10) console.log(project.name) // "my-agent" console.log(project.owner) // "0x..." console.log(project.versionCount) // 2 ``` **Returns:** Full project object from the registry. *** #### `listProjects` ```typescript const { data, total } = await client.listProjects({ offset: 0, limit: 20, }) for (const project of data) { console.log(project.name, project.owner) } ``` *** #### `getOwnerProjects` ```typescript const projects = await client.getOwnerProjects("0xYourAddress") ``` *** ### Versions #### `pushVersion` Upload content to Arweave and record it on-chain. Fee = Arweave cost + 20% markup (min $0.10 USDC). ```typescript // With local file — uploads to Arweave automatically const version = await client.pushVersion({ projectId: 10, file: "./dist/agent.js", // path or Buffer versionTag: "v1.0.0", changelog: "Initial release", }) // With existing Arweave hash const version = await client.pushVersion({ projectId: 10, arweaveHash: "ar://QmAbc123...", versionTag: "v1.0.0", changelog: "Initial release", }) console.log(version.arweaveHash) // "ar://QmAbc123..." console.log(version.arweaveUrl) // "https://arweave.net/QmAbc123..." console.log(version.txHash) // "0xdef..." ``` **Returns:** ```typescript { projectId: string versionIndex: string versionTag: string arweaveHash: string arweaveUrl: string txHash: string blockNumber: string } ``` **Throws:** `ProjectNotFound`, `NotOwnerOrCollaborator`, `EmptyArweaveHash`, `EmptyVersionTag` *** #### `getVersion` ```typescript const version = await client.getVersion(10, 0) // projectId, versionIndex console.log(version.versionTag) // "v1.0.0" console.log(version.arweaveHash) // "ar://QmAbc123..." console.log(version.pushedBy) // "0x..." console.log(version.pushedAt) // Unix timestamp ``` *** #### `listVersions` ```typescript const { data } = await client.listVersions(10) for (const version of data) { console.log(`${version.versionTag} → ${version.arweaveHash}`) } ``` *** #### `estimateVersionCost` Get the exact USDC fee for pushing a version before paying: ```typescript const estimate = await client.estimateVersionCost({ bytes: 102400 }) console.log(estimate.totalUsd) // "$0.0055" console.log(estimate.total) // "5520" (USDC, 6 decimals) ``` *** ### Collaborators #### `addCollaborator` Grant push access to another address. Owner only. Max 50 collaborators per project. ```typescript await client.addCollaborator({ projectId: 10, collaborator: "0xCollaboratorAddress", }) ``` *** #### `removeCollaborator` ```typescript await client.removeCollaborator({ projectId: 10, collaborator: "0xCollaboratorAddress", }) ``` *** #### `isCollaborator` ```typescript const isCollab = await client.isCollaborator(10, "0xAddress") // true | false ``` *** ### Content #### `uploadContent` Upload a file to Arweave directly, without pushing a version. Returns an `ar://` hash. ```typescript import { readFileSync } from "fs" const result = await client.uploadContent({ data: readFileSync("./dist/agent.js"), contentType: "application/javascript", filename: "agent.js", // optional }) console.log(result.hash) // "ar://QmAbc123..." console.log(result.url) // "https://arweave.net/QmAbc123..." ``` *** ### Full agent example ```typescript import { ProjectsClient } from "@inkd/sdk" import { readFileSync } from "fs" const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY, }) async function deploy() { // Create project (idempotent in practice — catch NameTaken) let projectId: string try { const project = await client.createProject({ name: "my-agent", license: "MIT", isAgent: true, }) projectId = project.id console.log(`Created project #${projectId}`) } catch (err: any) { if (err.code === "NameTaken") { const existing = await client.getProject("my-agent") projectId = existing.id } else throw err } // Push version const version = await client.pushVersion({ projectId: projectId, file: "./dist/agent.js", versionTag: `v${process.env.npm_package_version}`, changelog: process.env.CHANGELOG ?? "", }) console.log(`Pushed ${version.versionTag} → ${version.arweaveHash}`) } deploy().catch(console.error) ``` ## Arweave Storage Every version pushed to Inkd stores its content on [Arweave](https://arweave.org) — a permanent, decentralized storage network. Once uploaded, content can never be modified, deleted, or taken down. *** ### How it works When you push a version, the flow is: :::steps #### Upload to Arweave Your file is uploaded via [Irys](https://irys.xyz) — a Layer 2 for Arweave that provides instant upload confirmation and pay-as-you-go pricing. ``` POST /v1/upload → File stored on Arweave → Returns ar://QmAbc123... ``` #### Register on-chain The `ar://` hash is recorded in the InkdRegistry contract on Base. This creates a permanent, immutable link between your project version and the content. ```solidity registry.pushVersion( projectId, "ar://QmAbc123...", // content hash "v1.0.0", // version tag "Initial release" // changelog ) ``` #### Content is permanent The content at `ar://QmAbc123...` is now accessible forever via: * `https://arweave.net/QmAbc123` * Any Arweave gateway ::: *** ### Addressing Arweave uses content-addressed storage. The `ar://` prefix followed by a base64-encoded transaction ID uniquely identifies content. ``` ar://QmAbc123xyz456... └─ Arweave transaction ID (43 characters) ``` The same content always produces the same ID. You can verify the content of any version by fetching it from any Arweave gateway. *** ### Pricing Arweave storage costs are based on file size. The protocol charges the exact cost plus a 20% markup. | File size | Approx. cost (USDC) | | --------- | ------------------- | | 1 KB | \~$0.0022 | | 10 KB | \~$0.022 | | 100 KB | \~$0.22 | | 1 MB | \~$2.21 | Use the estimate endpoint to get the exact fee: ```bash curl "https://api.inkdprotocol.com/v1/upload/price?bytes=102400" ``` See [Fee Model](/concepts/fees) for a full breakdown. *** ### Supported content types The Inkd API accepts any content type. Common examples: | Content type | Use case | | -------------------------- | ---------------------------- | | `application/javascript` | Agent code, CLI tools | | `application/json` | Manifests, schemas, metadata | | `application/wasm` | WebAssembly modules | | `text/plain` | Scripts, configs | | `application/octet-stream` | Binary artifacts | *** ### Uploading content #### Via CLI ```bash inkd version push --id 10 --file ./dist/agent.js --tag v1.0.0 ``` The CLI uploads the file and records the hash in one command. #### Via API ```bash # 1. Upload the file curl -X POST https://api.inkdprotocol.com/v1/upload \ -H "Content-Type: application/json" \ -d '{ "data": "", "contentType": "application/javascript", "filename": "agent.js" }' # Returns: # { "hash": "ar://QmAbc123...", "url": "https://arweave.net/QmAbc123...", ... } # 2. Push the version with the hash ``` #### Via SDK ```typescript import { ProjectsClient } from "@inkd/sdk" import { readFileSync } from "fs" const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY }) // Upload returns ar:// hash const { hash } = await client.uploadContent({ data: readFileSync("./dist/agent.js"), contentType: "application/javascript", }) // Record on-chain await client.pushVersion({ projectId: 10, arweaveHash: hash, versionTag: "v1.0.0", }) ``` *** ### Accessing stored content Once uploaded, content is accessible via any Arweave gateway: ```bash # Primary gateway https://arweave.net/QmAbc123xyz456... # Alternative gateways https://ar-io.dev/QmAbc123xyz456... https://gateway.irys.xyz/QmAbc123xyz456... ``` *** ### Important notes * **Uploads are permanent.** There is no way to delete or modify content on Arweave. * **Content is public.** By default, all uploaded content is publicly readable. Encrypt sensitive data before uploading. * **The Inkd API accepts uploads up to 50 MB.** For larger files, upload directly to Arweave and pass the `ar://` hash. ## Smart Contracts Inkd is built on three upgradeable contracts deployed on Base Mainnet. All are open-source, verified on Basescan, and controlled by Safe multisigs. *** ### InkdRegistry The core registry. Stores project ownership, metadata, and version history on-chain. **Proxy:** [`0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d`](https://basescan.org/address/0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d) #### Key functions | Function | Access | Description | | ------------------------------------------------------------------------------------ | --------------------- | ---------------------------- | | `createProject(name, description, license, isPublic, readmeHash, isAgent, endpoint)` | Anyone | Register a new project | | `pushVersion(projectId, arweaveHash, versionTag, changelog)` | Owner or collaborator | Add a version | | `transferProject(projectId, newOwner)` | Owner | Transfer project ownership | | `addCollaborator(projectId, address)` | Owner | Grant push access (max 50) | | `removeCollaborator(projectId, address)` | Owner | Revoke push access | | `setVisibility(projectId, isPublic)` | Owner | Toggle public/private | | `getProject(id)` | Anyone | Read project metadata | | `getVersion(projectId, index)` | Anyone | Read a specific version | | `getVersionCount(projectId)` | Anyone | Number of versions | | `getOwnerProjects(address)` | Anyone | All project IDs for an owner | #### Constraints | Property | Limit | | ------------------------- | --------------------------------------------- | | Project name | Max 64 characters, globally unique, immutable | | Description | Max 1024 characters | | `arweaveHash` | Must be non-empty | | `versionTag` | Must be non-empty | | Collaborators per project | Max 50 | #### Events ```solidity event ProjectCreated(uint256 indexed projectId, address indexed owner, string name, string license); event VersionPushed(uint256 indexed projectId, string arweaveHash, string versionTag, address pushedBy); event ProjectTransferred(uint256 indexed projectId, address indexed from, address indexed to); event CollaboratorAdded(uint256 indexed projectId, address indexed collaborator); event CollaboratorRemoved(uint256 indexed projectId, address indexed collaborator); event VisibilityChanged(uint256 indexed projectId, bool isPublic); ``` *** ### InkdTreasury Receives USDC payments and splits revenue between the buyback contract and the protocol treasury. **Proxy:** [`0x23012C3EF1E95aBC0792c03671B9be33C239D449`](https://basescan.org/address/0x23012C3EF1E95aBC0792c03671B9be33C239D449) #### Revenue split When `settle(total, arweaveCost)` is called by the API server: ``` total USDC received ├── arweaveCost ──────────────► arweaveWallet │ (reimburses Irys/Arweave upload cost) └── markup = total − arweaveCost ├── 50% ─────────────► InkdBuyback └── 50% ─────────────► Treasury Safe (multisig) ``` Default markup: **20%** (`markupBps = 2000`). Configurable by the Treasury owner. #### Events ```solidity event Settled(address indexed settler, uint256 total, uint256 arweaveCost, uint256 toBuyback, uint256 toTreasury); event Withdrawn(address indexed to, uint256 amount); ``` *** ### InkdBuyback Accumulates USDC from protocol revenue. When the balance reaches the configurable threshold (default $50), it automatically swaps USDC for $INKD via Uniswap V3. **Proxy:** [`0xcbbf310513228153D981967E96C8A097c3EEd357`](https://basescan.org/address/0xcbbf310513228153D981967E96C8A097c3EEd357) #### Properties | Property | Value | | ------------------- | -------------------------------------------- | | Buyback threshold | $50 USDC (configurable) | | Slippage protection | 1% default (`maxSlippageBps = 100`), max 10% | | DEX | Uniswap V3 on Base | | Pool fee tier | 0.3% | | USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | #### Events ```solidity event Deposited(address indexed from, uint256 amount, uint256 newBalance); event BuybackExecuted(address indexed caller, uint256 usdcIn, uint256 inkdOut); event InkdTokenSet(address indexed token); event ThresholdSet(uint256 oldThreshold, uint256 newThreshold); ``` *** ### Ownership All contracts are controlled by **Safe multisigs** on Base Mainnet (2-of-2 signers required): | Safe | Address | Controls | | -------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | DEV\_SAFE | [`0x52d288c6697044561F99e433F01cd3d5ed4638A1`](https://app.safe.global/home?safe=base:0x52d288c6697044561F99e433F01cd3d5ed4638A1) | InkdRegistry upgrades | | TREASURY\_SAFE | [`0x6f8D6adc77C732972541A89a88ecB76Dfc641d1D`](https://app.safe.global/home?safe=base:0x6f8D6adc77C732972541A89a88ecB76Dfc641d1D) | InkdTreasury upgrades + withdrawals | | BUYBACK\_SAFE | [`0x58822722FA012Df30c37b709Fd2f70e0F83d9536`](https://app.safe.global/home?safe=base:0x58822722FA012Df30c37b709Fd2f70e0F83d9536) | InkdBuyback upgrades + $INKD recipient | *** ### Upgrade pattern All three contracts use [OpenZeppelin UUPS](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable). Upgrades: * Require `onlyOwner` (multisig) * Preserve all storage * Are transparent on Basescan *** ### Source code All contracts are published at [github.com/inkdprotocol/inkd-protocol](https://github.com/inkdprotocol/inkd-protocol) under the MIT license. ## Fee Model Inkd fees are **dynamic and transparent**. You pay exactly the resource cost plus a 20% protocol fee. Nothing is fixed — prices adjust automatically with Arweave storage rates. *** ### How fees are calculated #### Creating a project Creating a project registers metadata on-chain. No Arweave upload is required (the `readmeHash` field is optional). ``` createProject fee = $0.10 USDC (minimum) ``` #### Pushing a version Pushing a version uploads content to Arweave and records the hash on-chain. The fee is: ``` pushVersion fee = arweaveCost + 20% markup = arweaveCost × 1.20 minimum: $0.10 USDC ``` The Arweave cost depends on file size and current market rates. At current rates: | File size | Arweave cost | 20% markup | Total | | --------- | ------------ | ---------- | --------- | | 1 KB | \~$0.0018 | \~$0.00036 | \~$0.0022 | | 100 KB | \~$0.18 | \~$0.036 | \~$0.22 | | 1 MB | \~$1.84 | \~$0.37 | \~$2.21 | Use the estimate endpoint to get the exact fee for any file size: ```bash curl "https://api.inkdprotocol.com/v1/projects/estimate?bytes=102400" ``` ```json { "bytes": 102400, "arweaveCost": "184000", "markup": "36800", "total": "220800", "totalUsd": "$0.22" } ``` All USDC values use 6 decimals. `total / 1e6` = USD amount. *** ### Where fees go Every payment is processed by `InkdTreasury.settle()`: ``` You pay: total USDC │ ▼ InkdTreasury │ ├── arweaveCost ──► arweaveWallet │ (exact Arweave upload cost) │ └── 20% markup │ ├── 50% ──► InkdBuyback │ (accumulates, then auto-buys $INKD at $50 threshold) │ └── 50% ──► Treasury Safe (multisig) ``` * **You** pay the Arweave cost — it's forwarded directly to cover storage * **20% goes to the protocol** — split between buyback and treasury * **Nothing is hidden** — all flows are on-chain and verifiable *** ### Buyback mechanism Half of the 20% protocol fee flows into `InkdBuyback`. When the accumulated USDC balance reaches **$50**, the contract automatically: 1. Calls Uniswap V3 to swap all USDC → $INKD 2. $INKD is held in the contract (controlled by the Buyback Safe multisig) This creates continuous buy pressure on $INKD proportional to protocol usage. *** ### Read operations are free All read operations have no fee: * `GET /v1/projects` — list all projects * `GET /v1/projects/:id` — get project details * `GET /v1/projects/:id/versions` — list versions * `GET /v1/projects/estimate` — estimate push cost * `GET /v1/upload/price` — estimate upload cost * `GET /v1/status` — protocol status *** ### USDC on Base All fees are paid in USDC on Base Mainnet: ``` Token: USD Coin (USDC) Contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 Chain: Base Mainnet (8453) ``` Get USDC on Base via [Coinbase](https://coinbase.com) or bridge from Ethereum at [bridge.base.org](https://bridge.base.org). All metadata URIs stored on-chain (`metadataUri`, `versionMetadataHash`) must point to Arweave-hosted JSON conforming to this schema. ### Project Metadata **On-chain field:** `InkdRegistryV2.projectMetadataUri`\ **Format:** `ar://` or `https://arweave.net/` ```json { "$schema": "https://inkdprotocol.com/schemas/project-metadata/v1.json", "name": "my-project", "description": "A short description (max 1024 chars)", "version": "1.0.0", "license": "MIT", "homepage": "https://example.com", "repository": "https://github.com/owner/repo", "tags": ["ai", "agent", "base"], "readme": "ar://", "logo": "ar://", "links": { "docs": "https://docs.example.com", "twitter": "https://twitter.com/example", "discord": "https://discord.gg/example" }, "createdAt": "2026-03-07T00:00:00Z", "updatedAt": "2026-03-07T00:00:00Z" } ``` #### Required Fields | Field | Type | Max Length | Description | | ------------- | ------ | ---------- | ----------------------------------------------------- | | `name` | string | 64 | Project name — must match on-chain `name` (lowercase) | | `description` | string | 1024 | Short description | | `version` | string | 32 | Current version tag (semver recommended) | | `license` | string | 64 | SPDX identifier (e.g. `MIT`, `Apache-2.0`) | #### Optional Fields | Field | Type | Description | | ------------ | --------- | ----------------------------------- | | `homepage` | URL | Project website | | `repository` | URL | Source code repository | | `tags` | string\[] | Max 10 tags, each max 32 chars | | `readme` | `ar://` | Full README on Arweave | | `logo` | `ar://` | Square PNG/SVG, recommended 256×256 | | `links` | object | Map of external links | *** ### Version Metadata **On-chain field:** `InkdRegistryV2.versionMetaHash`\ **Format:** `ar://` ```json { "$schema": "https://inkdprotocol.com/schemas/version-metadata/v1.json", "projectId": 6, "versionIndex": 0, "versionTag": "v1.0.0", "changelog": "Initial release", "arweaveHash": "ar://", "pushedBy": "0x210bDf52ad7afE3Ea7C67323eDcCD699598983C0", "pushedAt": "2026-03-07T00:00:00Z", "agentWallet": "0xAgentWalletAddress", "dependencies": [ { "name": "other-project", "projectId": 1, "versionTag": "v0.9.0" } ], "runtime": { "type": "nodejs", "version": ">=18" } } ``` #### Required Fields | Field | Type | Description | | -------------- | ------- | -------------------------------- | | `projectId` | number | On-chain project ID | | `versionIndex` | number | On-chain version index (0-based) | | `versionTag` | string | Version tag (e.g. `v1.0.0`) | | `arweaveHash` | `ar://` | Arweave URI of the code payload | #### Optional Fields | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------- | | `changelog` | string | What changed in this version | | `agentWallet` | address | Actual agent wallet identity (not the server wallet) | | `dependencies` | array | Other Inkd projects this depends on | | `runtime` | object | Execution environment requirements | *** ### Access Manifest **On-chain field:** `InkdRegistryV2.projectAccessManifest`\ **Format:** `ar://` Used to grant multi-wallet access to credentials stored in AgentVault. ```json { "$schema": "https://inkdprotocol.com/schemas/access-manifest/v1.json", "projectId": 6, "vaultEntries": [ { "walletAddress": "0xAgentWallet1", "encryptedKeyRef": "ar://", "grantedAt": "2026-03-07T00:00:00Z", "grantedBy": "0xOwnerWallet" } ], "updatedAt": "2026-03-07T00:00:00Z" } ``` *** ### URI Format All Arweave references use the `ar://` prefix: ``` ar://<43-char-base64url-txid> ``` Example: `ar://baME8wjzVjRfx7SfhaMHp8vgSwOqLpHJUZl1m9bHKPY` The Inkd API resolves these via `https://arweave.net/`. *** ### Validation The Inkd API validates `metadataUri` content on upload (`POST /v1/upload`). Invalid schemas return `400 Bad Request` with field-level errors. JSON Schema definitions are published at: * `https://inkdprotocol.com/schemas/project-metadata/v1.json` * `https://inkdprotocol.com/schemas/version-metadata/v1.json` * `https://inkdprotocol.com/schemas/access-manifest/v1.json` ## x402 Payments Inkd uses [x402](https://x402.org) — an open HTTP payment protocol built on the `402 Payment Required` status code. Agents and developers pay for API calls directly with USDC. No API keys, no OAuth, no accounts. *** ### How it works :::steps #### Request without payment Your client calls a paid endpoint. The server responds with `402 Payment Required` and payment instructions. ```http POST /v1/projects HTTP/1.1 Content-Type: application/json { "name": "my-agent", ... } ``` ```http HTTP/1.1 402 Payment Required X-Payment-Required: {"amount":"100000","token":"USDC","network":"base"} ``` #### Sign the authorization Your client signs an EIP-3009 `transferWithAuthorization` — a gasless USDC transfer from your wallet to the Treasury contract. ```typescript const authorization = await signTransferAuthorization({ from: yourWallet, to: TREASURY_ADDRESS, value: 100000n, // 0.10 USDC (6 decimals) validAfter: 0n, validBefore: BigInt(Math.floor(Date.now() / 1000) + 300), nonce: randomBytes32(), }) ``` #### Retry with payment header The client retries the request with the signed authorization in the `X-PAYMENT` header. ```http POST /v1/projects HTTP/1.1 X-PAYMENT: Content-Type: application/json { "name": "my-agent", ... } ``` #### Server executes and responds The server verifies the signature, executes the USDC transfer on-chain, and processes the request. ```http HTTP/1.1 201 Created { "projectId": "10", "txHash": "0xabc...", ... } ``` ::: *** ### Why x402? #### For AI agents Agents hold wallets. x402 lets them pay directly — no human needs to create an API key, renew a subscription, or handle rate limits. A wallet with USDC is all that's required. ```typescript // No configuration. Just a wallet. const client = new ProjectsClient({ privateKey: process.env.WALLET_KEY }) await client.createProject({ name: "my-agent", ... }) ``` #### For developers x402 is standard HTTP. Any language that can make HTTP requests and sign EIP-3009 authorizations can use the Inkd API. *** ### EIP-3009 Inkd uses [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) `transferWithAuthorization`, which is native to USDC on Base. Key properties: * **Gasless for the payer** — the API server submits the transaction and pays gas * **Atomic** — payment and API execution happen in the same server-side flow * **Replay-safe** — each authorization has a unique nonce and expiry USDC EIP-712 domain on Base: ``` name: "USD Coin" version: "2" chainId: 8453 verifyingContract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 ``` *** ### Pricing Prices are dynamic — charged based on actual resource cost plus a 20% protocol markup. | Operation | Cost | | ----------------- | ---------------------------------------- | | List projects | Free | | Get project | Free | | List versions | Free | | Create project | $0.10 USDC (min) | | Push version | Arweave storage + 20% markup (min $0.10) | | Upload to Arweave | Free (cost included in version push fee) | For version pushes, the exact fee depends on file size. Use `GET /v1/projects/estimate?bytes=N` to get a price estimate before paying. *** ### Revenue flow Every payment flows through the protocol: ``` Agent pays USDC │ ▼ InkdTreasury.settle(total, arweaveCost) │ ├── arweaveCost ──────────────► arweaveWallet │ (covers Arweave storage) │ └── markup (20%) │ ├── 50% ──────────────► InkdBuyback │ (auto-buys $INKD at $50 threshold) │ └── 50% ──────────────► Treasury Safe ``` *** ### Using x402 in your app #### With the Inkd SDK (recommended) The SDK handles x402 automatically: ```typescript import { ProjectsClient } from "@inkd/sdk" const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY, }) // Payment happens automatically const project = await client.createProject({ name: "my-agent" }) ``` #### With @x402/fetch ```bash npm install @x402/fetch @x402/evm viem ``` ```typescript import { wrapFetchWithPayment } from "@x402/fetch" import { viemAdapter } from "@x402/evm" import { createWalletClient, http } from "viem" import { privateKeyToAccount } from "viem/accounts" import { base } from "viem/chains" const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) const wallet = createWalletClient({ account, chain: base, transport: http() }) const fetchWithPayment = wrapFetchWithPayment(fetch, viemAdapter(wallet)) const res = await fetchWithPayment("https://api.inkdprotocol.com/v1/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "my-agent", description: "...", license: "MIT", isPublic: true }), }) ``` #### Raw (any language) 1. Make the initial request (no auth) 2. Parse the `402` response to get payment amount and recipient 3. Sign EIP-3009 `transferWithAuthorization` with your wallet 4. Base64-encode the signed payload 5. Retry with `X-PAYMENT: ` header ### Install ```bash npm install -g @inkd/cli ``` ### Configuration ```bash export INKD_PRIVATE_KEY=0xYOUR_PRIVATE_KEY # required for write operations export INKD_NETWORK=mainnet # mainnet | testnet (default: testnet) export INKD_RPC_URL=https://... # optional custom RPC export INKD_API_URL=https://... # optional (default: api.inkdprotocol.com) ``` Or scaffold a local config file: ```bash inkd init ``` *** ### `inkd status` Show network connectivity and registry info. ```bash inkd status ``` *** ### `inkd project` #### `project create` Register a new project. **Costs $5 USDC.** ```bash inkd project create \ --name my-agent \ --description "An autonomous agent" \ --license MIT \ [--agent] \ [--private] \ [--endpoint https://my-agent.example.com] ``` | Flag | Description | | --------------- | ------------------------------------- | | `--name` | Project name (required) | | `--description` | Short description | | `--license` | SPDX license (default: MIT) | | `--agent` | Mark as AI agent project | | `--private` | Private project (not publicly listed) | | `--endpoint` | Agent HTTP endpoint | | `--readme` | Arweave hash of README | #### `project get ` Fetch project details by ID. ```bash inkd project get 7 ``` #### `project list
` List all projects owned by an address. ```bash inkd project list 0xYourAddress ``` *** ### `inkd version` #### `version push` Push a new version. **Costs $2 USDC.** **With file upload** (auto-uploads to Arweave first): ```bash inkd version push \ --id 7 \ --file ./dist/agent.js \ --tag v1.0.0 ``` **With existing Arweave hash:** ```bash inkd version push \ --id 7 \ --hash ar://QmAbc123... \ --tag v1.0.0 ``` | Flag | Description | | -------- | -------------------------------------- | | `--id` | Project ID (required) | | `--file` | Local file to upload to Arweave | | `--hash` | Pre-existing Arweave hash (`ar://...`) | | `--tag` | Version tag, e.g. `v1.0.0` (required) | #### `version list ` List all versions for a project. ```bash inkd version list 7 ``` #### `version show` Show details for a specific version. ```bash inkd version show --id 7 --index 0 ``` *** ### Cost summary | Operation | Cost | | ------------------------------ | ------- | | `project create` | $5 USDC | | `version push` | $2 USDC | | `project get` / `version list` | Free | All payments are made via [x402](/concepts/x402) directly from your wallet. No API key needed. ## Error Codes All API errors return a JSON body with an `error` field (machine-readable code) and a `message` field (human-readable description). ```json { "error": "name_taken", "message": "A project with the name 'my-agent' already exists." } ``` *** ### HTTP errors #### 400 Bad Request | Code | Description | | ------------------- | --------------------------------------- | | `name_required` | `name` field is missing or empty | | `name_too_long` | Project name exceeds 64 characters | | `name_taken` | A project with this name already exists | | `hash_required` | `arweaveHash` is missing or empty | | `tag_required` | `versionTag` is missing or empty | | `invalid_id` | Project ID is not a valid number | | `content_too_large` | Upload exceeds the 50 MB limit | #### 402 Payment Required Payment is required. The response body contains x402 payment terms. ```json { "error": "payment_required", "amount": "100000", "token": "USDC", "payTo": "0x23012C3EF1E95aBC0792c03671B9be33C239D449", "network": "base" } ``` Retry the request with a valid `X-PAYMENT` header. See [x402 Payments](/concepts/x402). #### 403 Forbidden | Code | Description | | --------------------------- | ----------------------------------------------------------- | | `not_owner_or_collaborator` | Caller is not the project owner or an approved collaborator | | `payment_invalid` | The x402 payment signature is invalid or expired | | `payment_amount_mismatch` | Payment amount does not match the required fee | #### 404 Not Found | Code | Description | | ------------------- | -------------------------------------- | | `project_not_found` | No project exists with the given ID | | `version_not_found` | No version exists with the given index | | `route_not_found` | The requested route does not exist | #### 500 Internal Server Error | Code | Description | | ----------------------- | ----------------------------------------------- | | `rpc_error` | Could not reach the Base RPC endpoint | | `arweave_upload_failed` | Arweave upload via Irys failed | | `tx_failed` | On-chain transaction was submitted but reverted | *** ### Contract revert reasons When a transaction reverts on-chain, the API returns `tx_failed` with the revert reason in the `message` field. #### InkdRegistry | Revert | Cause | | -------------------------- | ---------------------------------------------- | | `EmptyName()` | Project name is empty | | `NameTooLong()` | Name exceeds 64 characters | | `NameTaken()` | Name already registered | | `DescriptionTooLong()` | Description exceeds 1024 characters | | `EmptyArweaveHash()` | Arweave hash is empty | | `EmptyVersionTag()` | Version tag is empty | | `ProjectNotFound()` | Project ID does not exist | | `NotOwnerOrCollaborator()` | Caller lacks push permission | | `CannotAddOwner()` | Cannot add the project owner as a collaborator | | `AlreadyCollaborator()` | Address is already a collaborator | | `NotCollaborator()` | Address is not a collaborator | | `TooManyCollaborators()` | Project already has 50 collaborators | | `ZeroAddress()` | Zero address passed where disallowed | #### InkdTreasury | Revert | Cause | | ------------------- | ------------------------------------- | | `Unauthorized()` | Caller is not the settler or registry | | `ZeroAddress()` | Zero address passed where disallowed | | `Nothing to settle` | `total` is 0 | | `Insufficient USDC` | Treasury USDC balance \< `total` | #### InkdBuyback | Revert | Cause | | ------------------------------------ | ------------------------------------------------------ | | `InkdTokenNotSet()` | `inkdToken` is `address(0)` — set after Clanker launch | | `BelowThreshold(balance, threshold)` | USDC balance has not reached buyback threshold | | `NothingToWithdraw()` | No USDC balance to withdraw | | `ZeroAddress()` | Zero address passed where disallowed | | `Max 10% slippage` | `maxSlippageBps` exceeds 1000 | | `InkdBuyback: only treasury` | `deposit()` called by non-treasury address | *** ### Debugging tips **`tx_failed` with `NameTaken`** A project with that name already exists on-chain. Choose a different name. **`payment_invalid`** Your EIP-3009 authorization has expired (valid window is 5 minutes) or the nonce was already used. Generate a fresh authorization and retry. **`not_owner_or_collaborator`** The wallet signing the request is not the project owner and has not been added as a collaborator. Check with `GET /v1/projects/:id` that your address matches the `owner` field. ## API Reference **Base URL:** `https://api.inkdprotocol.com` The Inkd API is a REST API over HTTPS. Read operations are free and require no authentication. Write operations require [x402 USDC payment](/concepts/x402). *** ### Endpoints | Category | Description | | ------------------------- | ------------------------------------------------- | | [Projects](/api/projects) | Create projects, push versions, read the registry | | [Upload](/api/upload) | Upload files to Arweave | | [Errors](/api/errors) | Error codes reference | *** ### Authentication Inkd uses [x402](/concepts/x402) for payment and identity — no API keys, no accounts. For write operations, your wallet address becomes your identity. The API server verifies your EIP-3009 signature on-chain. **Read operations** — no authentication required: ```bash curl https://api.inkdprotocol.com/v1/projects ``` **Write operations** — x402 payment required: ```typescript import { ProjectsClient } from "@inkd/sdk" const client = new ProjectsClient({ privateKey: process.env.INKD_PRIVATE_KEY }) await client.createProject({ name: "my-agent" }) ``` Or use `@x402/fetch` directly to make raw HTTP requests with automatic payment handling. *** ### Response format All responses are JSON. Successful responses use `2xx` status codes. **Success:** ```json { "projectId": "10", "txHash": "0xabc...", "status": "success" } ``` **Error:** ```json { "error": "name_taken", "message": "A project with the name 'my-agent' already exists." } ``` *** ### Rate limits | Type | Limit | | -------------- | --------------------- | | Read requests | 100 req/min per IP | | Write requests | 20 req/min per wallet | *** ### Health & status ```bash # Health check curl https://api.inkdprotocol.com/v1/health # Protocol status (network, contracts, project count) curl https://api.inkdprotocol.com/v1/status ``` ```json { "ok": true, "network": "mainnet", "rpcUrl": "https://base.publicnode.com", "rpcReachable": true, "contracts": { "registry": "0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d", "treasury": "0x23012C3EF1E95aBC0792c03671B9be33C239D449", "deployed": true }, "protocol": { "projectCount": "9", "totalSupply": "100000000000.0000 INKD" } } ``` *** ### USDC on Base All payments use USDC on Base Mainnet: ``` Token: USD Coin (USDC) Address: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 Chain: Base Mainnet (8453) ``` ## Projects API Base URL: `https://api.inkdprotocol.com` Read operations are free. Write operations require [x402 payment](/concepts/x402) in USDC. *** ### List projects ```http GET /v1/projects ``` Returns all public projects. Free, no authentication required. **Query parameters** | Parameter | Type | Default | Description | | --------- | -------- | ------- | ---------------------------- | | `offset` | `number` | `0` | Pagination offset | | `limit` | `number` | `20` | Results per page (max `100`) | **Response** `200 OK` ```json { "data": [ { "id": "9", "name": "my-agent", "description": "An autonomous agent", "license": "MIT", "owner": "0x210bDf52ad7afE3Ea7C67323eDcCD699598983C0", "isPublic": true, "isAgent": true, "agentEndpoint": "https://my-agent.example.com", "createdAt": "1741383600", "versionCount": "2" } ], "total": 9, "offset": 0, "limit": 20 } ``` *** ### Get project ```http GET /v1/projects/:id ``` **Response** `200 OK` ```json { "id": "9", "name": "my-agent", "description": "An autonomous agent", "license": "MIT", "owner": "0x210bDf52ad7afE3Ea7C67323eDcCD699598983C0", "isPublic": true, "isAgent": true, "agentEndpoint": "https://my-agent.example.com", "createdAt": "1741383600", "versionCount": "2" } ``` **Errors** | Status | Error | Description | | ------ | ------------------- | ----------------------- | | `404` | `project_not_found` | No project with that ID | *** ### Create project ```http POST /v1/projects ``` Registers a new project on-chain. Requires x402 payment. **Cost:** $0.10 USDC (minimum, dynamic) **Request body** ```json { "name": "my-agent", "description": "An autonomous agent that manages deployments", "license": "MIT", "isPublic": true, "isAgent": true, "agentEndpoint": "https://my-agent.example.com", "readmeHash": "ar://QmReadme..." } ``` | Field | Type | Required | Constraints | Description | | --------------- | --------- | -------- | -------------------- | ---------------------------------------- | | `name` | `string` | ✅ | Max 64 chars, unique | Project name (normalized to lowercase) | | `description` | `string` | — | Max 1024 chars | Short description | | `license` | `string` | — | — | SPDX license identifier (default: `MIT`) | | `isPublic` | `boolean` | — | — | Default: `true` | | `isAgent` | `boolean` | — | — | Mark as an AI agent project | | `agentEndpoint` | `string` | — | — | HTTP endpoint for the agent | | `readmeHash` | `string` | — | `ar://` prefix | Arweave hash of README content | **Response** `201 Created` ```json { "txHash": "0x25642785bb06de399e102e3e943c6e93572fb8b65d3c9cceee45a3e430e413b9", "projectId": "10", "name": "my-agent", "owner": "0xYourWallet", "status": "success", "blockNumber": "25841234" } ``` **Errors** | Status | Error | Description | | ------ | --------------- | --------------------------------------- | | `400` | `name_required` | `name` field is missing | | `400` | `name_too_long` | Name exceeds 64 characters | | `400` | `name_taken` | A project with this name already exists | | `402` | — | Payment required (x402 flow) | *** ### List versions ```http GET /v1/projects/:id/versions ``` Returns all versions for a project. Free. **Response** `200 OK` ```json { "data": [ { "index": "0", "projectId": "9", "arweaveHash": "ar://QmAbc123xyz...", "versionTag": "v1.0.0", "changelog": "Initial release", "pushedBy": "0x210bDf52ad7afE3Ea7C67323eDcCD699598983C0", "pushedAt": "1741383900" } ] } ``` *** ### Get version ```http GET /v1/projects/:id/versions/:index ``` Returns a specific version by its index (0-based). *** ### Push version ```http POST /v1/projects/:id/versions ``` Uploads content to Arweave (if `file` provided) and records the Arweave hash on-chain. Requires x402 payment. **Cost:** Arweave storage cost + 20% markup (min $0.10 USDC). Use the [estimate endpoint](#estimate-version-cost) to get the exact amount before paying. **Request body** ```json { "arweaveHash": "ar://QmAbc123xyz...", "versionTag": "v1.0.0", "changelog": "Add support for streaming responses", "contentSize": 14336 } ``` | Field | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------------------------------- | | `arweaveHash` | `string` | ✅ | Permanent `ar://` content hash. Use `/v1/upload` first if you have a local file. | | `versionTag` | `string` | ✅ | Version identifier (e.g. `v1.0.0`) | | `changelog` | `string` | — | Description of changes | | `contentSize` | `number` | — | File size in bytes (used for fee display) | **Response** `201 Created` ```json { "txHash": "0x7a63bd374bd439a73f9090ac8431cbb338bc4693574e7663f6f7aff595eadf18", "projectId": "9", "versionIndex": "1", "versionTag": "v1.0.0", "arweaveHash": "ar://QmAbc123xyz...", "arweaveUrl": "https://arweave.net/QmAbc123xyz", "pusher": "0xYourWallet", "status": "success", "blockNumber": "25841300" } ``` **Errors** | Status | Error | Description | | ------ | --------------------------- | ------------------------------------------------- | | `400` | `hash_required` | `arweaveHash` is missing or empty | | `400` | `tag_required` | `versionTag` is missing or empty | | `403` | `not_owner_or_collaborator` | Caller is not the project owner or a collaborator | | `404` | `project_not_found` | No project with that ID | | `402` | — | Payment required (x402 flow) | *** ### Estimate version cost ```http GET /v1/projects/estimate?bytes=102400 ``` Returns the USDC cost to push a version for a given file size. **Query parameters** | Parameter | Type | Description | | --------- | -------- | ------------------ | | `bytes` | `number` | File size in bytes | **Response** `200 OK` ```json { "bytes": 102400, "arweaveCost": "4600", "markup": "920", "total": "5520", "totalUsd": "$0.0055" } ``` All USDC values use 6 decimals. Divide by `1e6` for USD. :::tip 1 KB of Arweave storage costs approximately $0.0018 USDC at current rates. ::: ### Upload content ```http POST /v1/upload ``` Upload any content to Arweave via Irys. Returns a permanent `ar://` hash. **Free** — the Arweave storage cost is covered by the $2 USDC paid in `pushVersion`. **Request body** (`application/json`): ```json { "data": "", "contentType": "application/json", "filename": "manifest.json" } ``` | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------- | | `data` | string | ✅ | Base64-encoded file content | | `contentType` | string | ✅ | MIME type | | `filename` | string | — | Optional filename tag on Arweave | **Max size:** 50 MB **Response:** ```json { "hash": "ar://QmAbc123...", "txId": "QmAbc123...", "url": "https://arweave.net/QmAbc123...", "bytes": 1024, "cost": { "usdc": "1844", "usd": "$0.0018" } } ``` #### Example ```typescript import { readFileSync } from "fs"; const data = readFileSync("./dist/agent.js"); const res = await fetch("https://api.inkdprotocol.com/v1/upload", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: data.toString("base64"), contentType: "application/javascript", filename: "agent.js", }), }); const { hash, url } = await res.json(); console.log(hash); // ar://QmAbc123... ``` *** ### Estimate upload cost ```http GET /v1/upload/price?bytes=4096 ``` Returns the Arweave cost estimate for a given upload size. **Query params:** | Param | Type | Description | | ------- | ------ | -------------------- | | `bytes` | number | Upload size in bytes | **Response:** ```json { "bytes": 4096, "costUsdc": "1844", "costUsd": "$0.0018" } ``` `costUsdc` is in USDC with 6 decimals. Divide by `1e6` for USD.