# Inkd Protocol > On-chain project registry for AI agents and developers. Built on Base, stored on Arweave. ## What is Inkd? Inkd is a **permanent, on-chain project registry** on Base. Think npm or GitHub — but without a server that can be shut down. * Projects and versions are stored on-chain via smart contracts * Content (code, artifacts, data) lives permanently on Arweave * Agents and developers pay to publish via [x402](/concepts/x402) — no API keys, wallet = identity ### How it works ### Create a project Register a project on-chain. Costs $5 USDC. Your wallet address is the owner. ### Upload content Push files to Arweave via the API. Returns a permanent `ar://` hash. ### Push a version Record the Arweave hash on-chain with a version tag. Costs $2 USDC. Permanent and immutable. ### Who is it for? **AI agents** that need to publish, version, and discover code autonomously — without OAuth, API keys, or human approval flows. **Developers** who want permanent, censorship-resistant artifact storage with on-chain provenance. ### Key properties #### Permanent Content on Arweave is stored forever. No takedowns, no 404s. #### On-chain Project ownership, version history, and metadata live on Base. Verifiable by anyone. #### Agent-native x402 payments via EIP-3009. Agents pay USDC directly — no OAuth, no API keys. #### Open All public projects are readable without authentication. ### 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) | ### Next steps #### Quickstart Create your first project in 5 minutes with the CLI. #### SDK Use the TypeScript SDK in your agent or application. ### Prerequisites * Node.js >= 18 * An EVM wallet with Base Mainnet ETH (for gas) and USDC * \~$8 USDC on Base Mainnet ($5 create + $2 first version) :::info Get USDC on Base via [Coinbase](https://coinbase.com) or bridge from Ethereum via [Base Bridge](https://bridge.base.org). ::: ### 1. Install the CLI ```bash npm install -g @inkd/cli inkd --version ``` ### 2. Configure Set your wallet private key and network: ```bash export INKD_PRIVATE_KEY=0xYOUR_PRIVATE_KEY export INKD_NETWORK=mainnet ``` Verify connectivity: ```bash inkd status ``` ``` Inkd Protocol Status ──────────────────────────────────────── Network: mainnet (Base) Registry: 0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d Projects: 7 ``` ### 3. Create a project ```bash inkd project create --name my-agent --description "My first on-chain project" ``` ``` → Creating project my-agent via x402... → Paying $5.00 USDC from 0xYourWallet... ✓ Project my-agent created! → Project ID: 8 → Owner: 0xYourWallet → TX: 0xabc... → Basescan: https://basescan.org/tx/0xabc... ``` :::note The `--agent` flag marks a project as an AI agent, making it discoverable via the agents API. The `--private` flag creates a private project (not listed publicly). ::: ### 4. Push a version Upload a file and record it on-chain in one step: ```bash inkd version push --id 8 --file ./dist/agent.js --tag v1.0.0 ``` ``` → Uploading dist/agent.js to Arweave (12.3 KB)... → Uploaded → https://arweave.net/QmAbc123... → Hash: ar://QmAbc123... → Pushing version v1.0.0 to project #8... → Paying $2.00 USDC from 0xYourWallet... ✓ Version v1.0.0 pushed! → Content hash: ar://QmAbc123... → TX: 0xdef... → Basescan: https://basescan.org/tx/0xdef... ``` Already have an Arweave hash? Pass it directly: ```bash inkd version push --id 8 --hash ar://QmAbc123... --tag v1.0.0 ``` ### 5. Verify ```bash inkd version list 8 ``` ``` Versions for Project #8 (1 total) ─────────────────────────────────────────────────────── #0 v1.0.0 QmAbc123… 2026-03-06 17:00:00 UTC ``` *** ### What's next #### SDK Use `ProjectsClient` in your agent or app — same flow, all in TypeScript. #### API Reference Call the HTTP API directly from any language. ## Inkd Protocol — Security Audit Report **Date:** 2026-03-07\ **Auditor:** Internal\ **Scope:** InkdRegistry.sol, InkdRegistryV2.sol, InkdTreasury.sol, InkdBuyback.sol\ **Commit:** `c65f4e7`\ **Fixes Commit:** `d3b4de7`, `c781354`, `c65f4e7`\ **Network:** Base Mainnet *** ### Summary | Severity | Count | Status | | ----------- | ----- | ---------------------------- | | 🔴 Critical | 1 | ✅ Fixed | | 🟠 High | 0 | — | | 🟡 Medium | 3 | ✅ Fixed | | 🔵 Low | 5 | ✅ Fixed (4), ⚠️ Accepted (1) | | ℹ️ Info | 3 | ✅ Fixed (2), ⚠️ Accepted (1) | *** ### 🔴 Critical #### C-01: InkdBuyback — No Slippage Protection (`amountOutMinimum: 0`) **Contract:** `InkdBuyback.sol` — `_executeBuyback()`\ **Impact:** Loss of funds via sandwich attack **Description:**\ The Uniswap swap uses `amountOutMinimum: 0`, meaning it accepts any output amount including near-zero. An attacker can sandwich the `executeBuyback()` transaction: 1. Attacker sees buyback TX in mempool 2. Front-runs: buys $INKD, pushing price up 3. Buyback executes at inflated price → receives very little $INKD 4. Attacker back-runs: sells $INKD at elevated price, extracts value Since `executeBuyback()` is callable by anyone (public function), any MEV bot can trigger this when threshold is met. **Code:** ```solidity ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ ... amountOutMinimum: 0, // ← CRITICAL: accepts any output sqrtPriceLimitX96: 0 }); ``` **Fix:** ```solidity // Use Uniswap V3 TWAP oracle for minimum out, or at minimum a % slippage floor uint256 minOut = _getMinAmountOut(usdcIn); // e.g. 95% of TWAP price ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ ... amountOutMinimum: minOut, sqrtPriceLimitX96: 0 }); ``` **Short-term mitigation:** Restrict `executeBuyback()` to `onlyOwner` until TWAP-based slippage is implemented. *** ### 🟡 Medium #### M-01: V2 Project Owner = Settler Wallet (Not User) **Contract:** `InkdRegistryV2.sol` — `createProjectV2()`\ **Impact:** On-chain project ownership assigned to server wallet, not actual user **Description:**\ `createProjectV2` calls `_createProjectCore` which sets `owner: msg.sender`. Since `createProjectV2` is `onlySettler`, `msg.sender` is the settler (API server wallet). All projects created via V2 are owned by `0x210bDf52...` on-chain, regardless of who paid. This means: * Users cannot call `transferProject`, `addCollaborator`, `setVisibility` etc. directly * If the settler wallet is compromised, attacker can transfer all projects * No on-chain provenance of actual project owner **Fix:** Add an explicit `owner_` parameter to `createProjectV2` and use it in `_createProjectCore`: ```solidity function createProjectV2( ... address owner_, // actual project owner (payer address from x402) ... ) external onlySettler { // Set owner to payer, not settler uint256 id = _createProjectCoreWithOwner(owner_, name, ...); } ``` #### M-02: Treasury `settle()` Has No Balance Validation **Contract:** `InkdTreasury.sol` — `settle()`\ **Impact:** Silent failures if USDC not pre-transferred **Description:**\ `settle(total, arweaveCost)` assumes USDC is already in the contract. There's no check that `usdc.balanceOf(address(this)) >= total`. If the API server calls `settle()` before the USDC transfer lands (race condition or bug), `safeTransfer` will revert with a confusing error rather than a clear "insufficient balance" message. **Fix:** ```solidity function settle(uint256 total, uint256 arweaveCost) external onlyTrusted { require(usdc.balanceOf(address(this)) >= total, "Insufficient USDC balance"); _split(total, arweaveCost); } ``` #### M-03: V2 Metadata Setters Missing `exists` Check **Contract:** `InkdRegistryV2.sol` — `setMetadataUri`, `setAccessManifest`, `setTagsHash`\ **Impact:** Metadata can be set on non-existent project IDs (griefing) **Description:**\ The V2 setter functions check `projects[projectId].owner != msg.sender` but do not verify `projects[projectId].exists`. For a non-existent project, `owner` is `address(0)`, so the check passes for no one — but this also means: * Anyone who knows the next `projectId` can pre-set metadata on it before creation * Creates confusing state where metadata exists without a project **Fix:** ```solidity function setMetadataUri(uint256 projectId, string calldata uri) external { if (!projects[projectId].exists) revert ProjectNotFound(); // ← add this if (projects[projectId].owner != msg.sender && !isCollaborator[projectId][msg.sender]) revert NotOwnerOrCollaborator(); ... } ``` *** ### 🔵 Low #### L-01: Treasury — ETH Permanently Stuck **Contract:** `InkdTreasury.sol`\ **Description:** `receive() external payable` accepts ETH but there's no ETH withdrawal function. Any ETH accidentally sent to the treasury is permanently locked.\ **Fix:** Add `function withdrawEth(address payable to) external onlyOwner { to.transfer(address(this).balance); }` #### L-02: Reentrancy in `Treasury._split` **Contract:** `InkdTreasury.sol` — `_split()`\ **Description:** `totalSettled += total` is updated AFTER external calls to `arweaveWallet` and `buybackContract.deposit()`. A malicious buyback contract could re-enter `settle()` before state is updated. Currently low risk since buybackContract is owner-controlled (Safe multisig), but violates checks-effects-interactions.\ **Fix:** Move `totalSettled += total` to before any external calls. #### L-03: Unbounded Iteration in `getAgentProjects` **Contract:** `InkdRegistry.sol` — `getAgentProjects()`\ **Description:** O(n) iteration over all `projectCount`. At 10,000+ projects, this will hit gas limits for on-chain calls. Safe for off-chain view calls now, but will break at scale.\ **Fix:** Maintain a separate `uint256[] agentProjectIds` array updated on project creation/agent registration. Or use The Graph for indexing. #### L-04: `approve` Instead of `forceApprove` in Buyback ⚠️ **Contract:** `InkdBuyback.sol` — `_executeBuyback()`\ **Description:** Uses `IERC20(USDC).approve(SWAP_ROUTER, usdcIn)` directly. If a previous approval is still pending (shouldn't happen for USDC but can for non-standard tokens), the approve would fail on tokens that require resetting to 0 first.\ **Fix:** Use `SafeERC20.forceApprove(IERC20(USDC), SWAP_ROUTER, usdcIn)` (OZ utility). #### L-05: No Input Length Limits on String Fields **Contract:** `InkdRegistry.sol`\ **Description:** `name`, `description`, `agentEndpoint`, `arweaveHash` have no max length validation. A malicious actor could push very large strings, making transactions expensive and bloating on-chain storage.\ **Recommendation:** Add `require(bytes(name).length <= 64, "Name too long")` and similar for other fields. *** ### ℹ️ Info #### I-01: `pushVersionV2` Does Not Return Version Index **Contract:** `InkdRegistryV2.sol`\ **Description:** Unlike `_pushVersionCore` which returns `versionIndex`, `pushVersionV2` does not return the new version index. Callers must derive it from the `VersionPushedV2` event.\ **Recommendation:** Add `returns (uint256 versionIndex)` to `pushVersionV2`. #### I-02: NPM Dev Dependency Vulnerabilities **Scope:** `vitest`, `vite`, `esbuild` (dev dependencies only)\ **Description:** 5 moderate severity vulnerabilities in dev tooling. None affect production code or deployed contracts.\ **Recommendation:** Run `npm audit fix` to resolve where possible. #### I-03: Deployer Key in Repo Git History **Scope:** Operational\ **Description:** From MEMORY.md — deployer key `0xD363864...` was reportedly in public repo history. Even if removed from current tree, it exists in git history and should be considered compromised.\ **Action Required:** Generate new deployer wallet, transfer any assets, never use `0xD363864...` for new deployments. *** ### Recommendations by Priority #### Immediate (before public launch): 1. **\[C-01]** Fix `amountOutMinimum: 0` in InkdBuyback — restrict to `onlyOwner` or implement TWAP 2. **\[I-03]** Rotate deployer key #### Before $INKD Launch: 3. **\[M-01]** Add explicit `owner_` param to `createProjectV2` 4. **\[M-02]** Add balance check to `settle()` 5. **\[L-01]** Add ETH withdrawal to Treasury 6. **\[L-02]** Fix reentrancy ordering in `_split` #### Before Scale (10k+ projects): 7. **\[L-03]** Replace `getAgentProjects` O(n) loop with indexed array or subgraph 8. **\[M-03]** Add `exists` check to V2 setters 9. **\[L-05]** Add string length limits *** ### What's Good ✅ * **UUPS Upgrade Safety:** All contracts use `_disableInitializers()`, owner is Safe multisig * **SafeERC20:** Used consistently for all USDC transfers * **Access Control:** Well-structured; settler/registry separation in Treasury * **Storage Layout:** V2 appends storage after V1 — no collision risk * **Test Coverage:** 1279 tests, 291 contract tests — solid foundation * **No Reentrancy on Registry:** V1 `_pushVersionCore` external calls are to trusted treasury * **Zero Address Checks:** Consistent across initialize functions and setters * **Event Emission:** Comprehensive events for indexing `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); ``` ### Install ```bash npm install @inkd/sdk viem # or yarn add @inkd/sdk viem # or pnpm add @inkd/sdk viem ``` **Peer dependency:** `viem >= 2.0.0` ### Setup ```typescript import { ProjectsClient } from "@inkd/sdk"; import { createWalletClient, createPublicClient, http } from "viem"; import { base } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const wallet = createWalletClient({ account, chain: base, transport: http("https://base.publicnode.com"), }); const reader = createPublicClient({ chain: base, transport: http("https://base.publicnode.com"), }); const client = new ProjectsClient({ wallet, publicClient: reader }); ``` ### Custom API URL By default the SDK targets `https://api.inkdprotocol.com`. Override for local development: ```typescript const client = new ProjectsClient({ wallet, publicClient: reader, apiUrl: "http://localhost:3000", }); ``` ### Network Inkd is live on **Base Mainnet** (chain ID `8453`). Always use `base` from `viem/chains`, not `baseSepolia`. ### Next #### ProjectsClient Create projects, push versions, upload files. #### AgentVault Store encrypted credentials on Arweave. `ProjectsClient` handles all write operations. Payments are made via [x402](/concepts/x402) — no API keys required. ### Constructor ```typescript import { ProjectsClient } from "@inkd/sdk"; const client = new ProjectsClient({ wallet, // viem WalletClient with account publicClient, // viem PublicClient for reading state apiUrl?, // default: "https://api.inkdprotocol.com" }); ``` *** ### createProject Register a new project on-chain. **Costs $5 USDC.** ```typescript const { projectId, txHash, owner } = await client.createProject({ name: "my-agent", description: "An autonomous trading agent", license: "MIT", isPublic: true, isAgent: true, agentEndpoint: "https://my-agent.example.com", }); console.log(`Project #${projectId} → https://basescan.org/tx/${txHash}`); ``` #### Parameters | Field | Type | Default | Description | | --------------- | --------- | -------- | ------------------------------------------------ | | `name` | `string` | required | Unique project name (lowercase, no spaces) | | `description` | `string` | `""` | Short project description | | `license` | `string` | `"MIT"` | SPDX license identifier | | `isPublic` | `boolean` | `true` | Publicly listed in the registry | | `isAgent` | `boolean` | `false` | Marks this as an AI agent project | | `agentEndpoint` | `string` | `""` | HTTP endpoint for the agent (if `isAgent: true`) | | `readmeHash` | `string` | `""` | Arweave hash of README content | #### Returns ```typescript { projectId: number; // On-chain project ID txHash: string; // Transaction hash owner: string; // Owner address (your wallet) blockNumber: number; } ``` *** ### upload Upload content to Arweave. Returns a permanent `ar://` hash. **Free** — cost is covered by the `pushVersion` fee. ```typescript import { readFileSync } from "fs"; const data = readFileSync("./dist/agent.js"); const { hash, url, bytes } = await client.upload(data, { contentType: "application/javascript", filename: "agent.js", }); console.log(hash); // "ar://QmAbc123..." console.log(url); // "https://arweave.net/QmAbc123..." ``` #### Parameters | Field | Type | Description | | ------------------ | -------------------------------- | ----------------------------------------------- | | `data` | `Uint8Array \| Buffer \| string` | File content | | `opts.contentType` | `string` | MIME type (default: `application/octet-stream`) | | `opts.filename` | `string` | Optional filename tag on Arweave | #### Returns ```typescript { hash: string; // "ar://TxId" txId: string; // Raw Arweave transaction ID url: string; // "https://arweave.net/TxId" bytes: number; // Upload size in bytes } ``` *** ### pushVersion Record a version on-chain with an Arweave content hash. **Costs $2 USDC.** ```typescript const { txHash, tag } = await client.pushVersion(projectId, { tag: "v1.0.0", contentHash: "ar://QmAbc123...", // from upload() contentSize: 12345, // optional, used for dynamic pricing }); ``` #### Typical flow ```typescript // 1. Upload content const { hash, bytes } = await client.upload(data, { contentType: "application/json" }); // 2. Push version with the hash const { txHash } = await client.pushVersion(projectId, { tag: "v2.0.0", contentHash: hash, contentSize: bytes, }); ``` #### Parameters | Field | Type | Description | | -------------- | -------- | ----------------------------------- | | `tag` | `string` | Version tag (e.g. `"v1.0.0"`) | | `contentHash` | `string` | Arweave hash (`ar://...`) | | `metadataHash` | `string` | Optional metadata hash | | `contentSize` | `number` | Bytes (for dynamic pricing display) | #### Returns ```typescript { tag: string; contentHash: string; txHash: string; blockNumber: number; } ``` *** ### getProject Fetch project details (free, no payment). ```typescript const project = await client.getProject(7); console.log(project.name); // "my-agent" console.log(project.owner); // "0x..." console.log(project.versionCount); // 3 ``` *** ### listProjects List all projects (free). ```typescript const projects = await client.listProjects({ offset: 0, limit: 20 }); ``` *** ### estimateUploadCost Estimate the Arweave cost for a given upload size. ```typescript const { total, arweaveCost, markup } = await client.estimateUploadCost(1024 * 100); // 100 KB console.log(total); // "1844" (USDC with 6 decimals) ``` *** ### Full example ```typescript import { ProjectsClient } from "@inkd/sdk"; import { createWalletClient, createPublicClient, http } from "viem"; import { base } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { readFileSync } from "fs"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const wallet = createWalletClient({ account, chain: base, transport: http() }); const reader = createPublicClient({ chain: base, transport: http() }); const inkd = new ProjectsClient({ wallet, publicClient: reader }); // Create project const { projectId } = await inkd.createProject({ name: "my-autonomous-agent", isAgent: true, license: "MIT", }); // Upload artifact const data = readFileSync("./dist/agent.js"); const { hash, bytes } = await inkd.upload(data, { contentType: "application/javascript", filename: "agent.js", }); // Push version const { txHash } = await inkd.pushVersion(projectId, { tag: "v1.0.0", contentHash: hash, contentSize: bytes, }); console.log(`Published! https://basescan.org/tx/${txHash}`); ``` ### Why Arweave? On-chain storage of large files is prohibitively expensive. Inkd solves this by: * Storing **metadata and ownership** on Base (cheap, verifiable) * Storing **content** (code, artifacts, data) on Arweave (permanent, decentralized) The on-chain record links to the Arweave hash (`ar://TxId`). Anyone can verify the content matches the on-chain record. ### Permanent by design Arweave charges a one-time fee to store data forever — no recurring hosting costs, no takedowns. Once uploaded, the content is permanently available at `https://arweave.net/TxId`. This makes Inkd useful for: * Publishing agent binaries that need to run years from now * Storing audit trails and version history that must be immutable * Sharing datasets or models with guaranteed availability ### Irys Inkd uses [Irys](https://irys.xyz) (formerly Bundlr) as the upload layer. Irys handles the Arweave upload mechanics — bundling, payment, and confirmation. The API server holds an ETH wallet on Base that pays Irys for uploads. This cost is covered by the $2 USDC paid when pushing a version. ### Content addressing Every upload returns an Arweave transaction ID, formatted as `ar://TxId`. This hash is: * Content-addressed: the same content always produces the same hash * Immutable: the content at a given hash can never change * Permanent: stored on Arweave forever ```typescript const { hash } = await client.upload(data, { contentType: "application/json" }); // hash = "ar://QmAbc123..." // Access at any time: // https://arweave.net/QmAbc123... ``` ### Cost Arweave storage costs are very low and scale with file size: ``` GET /v1/upload/price?bytes=102400 → { "costUsdc": "4600", "costUsd": "$0.0046" } // ~100 KB ``` For reference: 1 MB ≈ $0.04. The cost is dynamic and based on the Arweave network price at upload time. All contracts are deployed on **Base Mainnet**, verified on Basescan, and upgradeable via UUPS proxies controlled by multisig. ### InkdRegistry The core registry. Stores project metadata and version history on-chain. **Proxy:** [`0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d`](https://basescan.org/address/0xEd3067dDa601f19A5737babE7Dd3AbfD4a783e5d) #### Key functions | Function | Description | | ------------------------------------------------------------------------------------ | ----------------------------------- | | `createProject(name, description, license, isPublic, readmeHash, isAgent, endpoint)` | Register a new project | | `pushVersion(projectId, tag, contentHash, metadataHash)` | Add a version to a project | | `getProject(id)` | Read project metadata | | `getVersion(projectId, index)` | Read a specific version | | `getVersionCount(projectId)` | Number of versions | | `getOwnerProjects(address)` | All project IDs owned by an address | #### Events * `ProjectCreated(uint256 indexed projectId, address indexed owner, string name)` * `VersionPushed(uint256 indexed projectId, uint256 indexed versionIndex, string tag, string contentHash)` *** ### InkdTreasury Collects USDC payments and splits revenue between the buyback contract and the protocol treasury. **Proxy:** [`0x23012C3EF1E95aBC0792c03671B9be33C239D449`](https://basescan.org/address/0x23012C3EF1E95aBC0792c03671B9be33C239D449) #### Revenue split When `settle(amount, arweaveCost)` is called: ``` payment (USDC) ├── arweaveCost → arweaveWallet (actual Arweave upload cost) └── remainder ├── 50% → InkdBuyback └── 50% → Treasury Safe (multisig) ``` *** ### InkdBuyback Accumulates USDC from protocol revenue. Once the balance reaches the $50 threshold, it automatically buys $INKD via Uniswap V3 and sends it to the Buyback Safe. **Proxy:** [`0xcbbf310513228153D981967E96C8A097c3EEd357`](https://basescan.org/address/0xcbbf310513228153D981967E96C8A097c3EEd357) | Property | Value | | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | | Buyback threshold | $50 USDC | | DEX | Uniswap V3 | | $INKD token | [`0xa6f64A0D23e9d6eC918929af53df1C7b0D819B07`](https://basescan.org/address/0xa6f64A0D23e9d6eC918929af53df1C7b0D819B07) | *** ### Ownership All contracts are controlled by Safe multisigs (2-of-2): | Safe | Controls | | ----------------------------- | -------------------------------------- | | DEV\_SAFE `0x52d288c...` | InkdRegistry upgrades | | TREASURY\_SAFE `0x6f8D6ad...` | InkdTreasury upgrades + withdrawals | | BUYBACK\_SAFE `0x5882272...` | InkdBuyback upgrades + $INKD recipient | 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` ### What is x402? [x402](https://x402.org) is an HTTP payment protocol built on the `402 Payment Required` status code. It lets agents pay for API calls directly with on-chain USDC — no API keys, no OAuth, no rate limit tiers. The flow: 1. Client calls the API without any auth header 2. API responds with `402 Payment Required` and payment details (amount, recipient, network) 3. Client signs an EIP-3009 USDC transfer authorization and retries the request with `X-PAYMENT` header 4. API verifies the signature, executes the USDC transfer, and fulfills the request ### Why x402? **For AI agents**, x402 is ideal: * Agents hold wallets, not API credentials * Payments are programmable (agents can authorize specific amounts) * No manual API key rotation or rate limits to manage * Works with any USDC-holding wallet on Base **For developers**, it's simple: ```typescript // Wrap fetch once, pay automatically const fetchPay = wrapFetchWithPayment(fetch, client); // Calls that need payment: handled automatically await fetchPay("https://api.inkdprotocol.com/v1/projects", { method: "POST", body: JSON.stringify({ name: "my-project" }), }); ``` ### EIP-3009 x402 uses [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) `transferWithAuthorization` — a standard for signing USDC transfers off-chain. The signed authorization is sent in the `X-PAYMENT` header. The API server executes the on-chain transfer on the client's behalf. This means: * No ETH gas needed from the agent wallet (gas is paid by the API server) * Payment happens atomically with the API call * Signatures are single-use (nonce prevents replay) ### Pricing | Operation | Cost | | ---------------- | ------- | | Create project | $5 USDC | | Push version | $2 USDC | | Read (list, get) | Free | | Upload | Free | ### Revenue flow When you pay to create a project or push a version, the USDC flows through the protocol: 1. **USDC → Treasury** — collected via EIP-3009 2. **Treasury.settle()** — splits the payment: * 50% → InkdBuyback (accumulates until $50 threshold, then auto-buys $INKD) * 50% → Protocol treasury ### Libraries ```bash npm install @x402/fetch @x402/evm ``` * [`@x402/fetch`](https://npmjs.com/package/@x402/fetch) — wraps `fetch` with automatic payment handling * [`@x402/evm`](https://npmjs.com/package/@x402/evm) — EVM signer adapter (viem-compatible) ### 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. ### Base URL ``` https://api.inkdprotocol.com ``` ### Authentication Inkd uses **[x402](/concepts/x402)** for write operations. No API keys. No OAuth. Your wallet pays and signs. When you call a paid endpoint, the x402 client: 1. Receives a `402 Payment Required` response with payment details 2. Signs an EIP-3009 USDC transfer authorization 3. Retries the request with an `X-PAYMENT` header This happens automatically via `@x402/fetch`: ```typescript import { wrapFetchWithPayment, x402Client } from "@x402/fetch"; import { ExactEvmScheme } from "@x402/evm"; const signer = { address, signTypedData, readContract }; const client = new x402Client().register("eip155:8453", new ExactEvmScheme(signer)); const fetchPay = wrapFetchWithPayment(fetch, client); // Now use fetchPay exactly like fetch — payments handled automatically const res = await fetchPay("https://api.inkdprotocol.com/v1/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "my-project" }), }); ``` Or use the SDK's [`ProjectsClient`](/sdk/projects-client) which handles this for you. ### Read endpoints `GET` endpoints are free and require no authentication. ### Response format All responses are JSON. **Success:** ```json { "data": { ... } } ``` **Error:** ```json { "error": { "code": "BAD_REQUEST", "message": "name is required" } } ``` #### Error codes | Code | HTTP | Description | | --------------------- | ---- | ----------------------------- | | `BAD_REQUEST` | 400 | Missing or invalid parameters | | `UNAUTHORIZED` | 401 | Payment missing or invalid | | `NOT_FOUND` | 404 | Resource does not exist | | `RPC_ERROR` | 502 | On-chain transaction failed | | `SERVICE_UNAVAILABLE` | 503 | Server wallet not configured | ### Pricing | Endpoint | Cost | | -------------------------------- | ------- | | `POST /v1/projects` | $5 USDC | | `POST /v1/projects/:id/versions` | $2 USDC | | `POST /v1/upload` | Free | | All `GET` endpoints | Free | ### List projects ```http GET /v1/projects?offset=0&limit=20 ``` Free. Returns all public projects. **Query params:** | Param | Type | Default | Description | | -------- | ------ | ------- | --------------------- | | `offset` | number | 0 | Pagination offset | | `limit` | number | 20 | Max results (max 100) | **Response:** ```json { "data": [ { "id": "7", "name": "my-agent", "description": "An autonomous agent", "license": "MIT", "owner": "0x210bDf52ad7afE3Ea7C67323eDcCD699598983C0", "isPublic": true, "isAgent": true, "agentEndpoint": "https://my-agent.example.com", "createdAt": "1772803578", "versionCount": "1" } ] } ``` *** ### Get project ```http GET /v1/projects/:id ``` **Response:** Same shape as a single item from the list. *** ### Create project ```http POST /v1/projects ``` **Cost: $5 USDC** (paid via x402) **Request body:** ```json { "name": "my-agent", "description": "An autonomous agent", "license": "MIT", "isPublic": true, "isAgent": true, "agentEndpoint": "https://my-agent.example.com", "readmeHash": "" } ``` | Field | Type | Required | Description | | --------------- | ------- | -------- | ----------------------------- | | `name` | string | ✅ | Unique project name | | `description` | string | — | Short description | | `license` | string | — | SPDX license (default: `MIT`) | | `isPublic` | boolean | — | Default: `true` | | `isAgent` | boolean | — | Mark as AI agent project | | `agentEndpoint` | string | — | HTTP endpoint if agent | | `readmeHash` | string | — | Arweave hash of README | **Response:** ```json { "txHash": "0xabc...", "projectId": "8", "owner": "0xYourWallet", "signer": "0x210bDf52...", "status": "success", "blockNumber": "14234567" } ``` *** ### List versions ```http GET /v1/projects/:id/versions ``` Free. Returns all versions for a project. *** ### Push version ```http POST /v1/projects/:id/versions ``` **Cost: $2 USDC** (paid via x402) **Request body:** ```json { "tag": "v1.0.0", "contentHash": "ar://QmAbc123...", "metadataHash": "", "contentSize": 12345 } ``` | Field | Type | Required | Description | | -------------- | ------ | -------- | --------------------------- | | `tag` | string | ✅ | Version tag | | `contentHash` | string | ✅ | Arweave hash (`ar://...`) | | `metadataHash` | string | — | Optional metadata hash | | `contentSize` | number | — | Bytes (for pricing display) | **Response:** ```json { "txHash": "0xdef...", "projectId": "8", "tag": "v1.0.0", "contentHash": "ar://QmAbc123...", "pusher": "0xYourWallet", "status": "success", "blockNumber": "14234600" } ``` *** ### Estimate version cost ```http GET /v1/projects/estimate?bytes=102400 ``` Returns the USDC cost for uploading a given number of bytes (Arweave storage cost + 20% markup). **Response:** ```json { "bytes": 102400, "arweaveCost": "4600", "markup": "920", "total": "5520" } ``` All values are in USDC with 6 decimals (divide by `1e6` for USD). ### 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.