{"id":51794999,"url":"https://github.com/dfinity/immutable-object-storage-example","last_synced_at":"2026-07-21T02:06:24.888Z","repository":{"id":350420160,"uuid":"1176859928","full_name":"dfinity/immutable-object-storage-example","owner":"dfinity","description":"Example frontend and backend canisters for the immutable object storage on ICP","archived":false,"fork":false,"pushed_at":"2026-04-10T08:14:02.000Z","size":172,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-10T10:28:03.097Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dfinity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-09T13:01:50.000Z","updated_at":"2026-04-10T08:14:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dfinity/immutable-object-storage-example","commit_stats":null,"previous_names":["dfinity/immutable-object-storage-example"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dfinity/immutable-object-storage-example","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfinity%2Fimmutable-object-storage-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfinity%2Fimmutable-object-storage-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfinity%2Fimmutable-object-storage-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfinity%2Fimmutable-object-storage-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dfinity","download_url":"https://codeload.github.com/dfinity/immutable-object-storage-example/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfinity%2Fimmutable-object-storage-example/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35705781,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-21T02:00:06.728Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-21T02:06:20.456Z","updated_at":"2026-07-21T02:06:24.875Z","avatar_url":"https://github.com/dfinity.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Caffeine Object Storage — Example App\n\nThis example shows how to integrate [Caffeine Immutable Object Storage](https://blob.caffeine.ai)\ninto an Internet Computer app. It includes a Rust backend canister, a Motoko backend canister, and\na React frontend. Pick the backend language that suits your project.\n\n## What is Caffeine Object Storage?\n\nCaffeine Object Storage lets ICP canisters store large immutable files (images, videos, documents,\nmodels — up to 5 TB per file) off-chain while keeping cryptographic references on-chain. Your\ncanister stores only a 32-byte SHA-256 hash per file. The storage gateway handles the actual bytes,\nverifying every upload against that hash.\n\n**Key properties:**\n\n- Content-addressed: the hash *is* the address. If the bytes change, the hash changes.\n- Payment in ICP cycles via the Cycles Ledger. No separate accounts or tokens required.\n- No vendor lock-in on the data format: SHA-256 hashes are a universal standard.\n\n---\n\n## Architecture\n\nThere are four components in the system. As an integrator, you deploy and manage the\n**Backend Canister**. The other three are operated by Caffeine.\n\n```\n                  ┌───────────────────────────────────────────────┐\n                  │            Internet Computer                  │\n                  │                                               │\n                  │  ┌──────────────────┐  ┌──────────────────┐   │\n User/Browser ───►│  │  Your Backend    │  │ Cashier Canister  │  │\n       │          │  │  Canister        │  │ (billing / auth)  │  │\n       │          │  └──────────────────┘  └──────────────────┘   │\n       │          └───────────────────────────────────────────────┘\n       │                                          ▲\n       │ PUT blob-tree + chunks                   │ budget check\n       │ GET blob                                 │\n       ▼                                          │\n  ┌──────────────────────────────────┐            │\n  │  Storage Gateway                 │────────────┘\n  │  blob.caffeine.ai                │\n  │  (verifies tree, cert \u0026 budget)  │──────────┐\n  └──────────────────────────────────┘          │\n       ▲                                        │ stores data\n       │ periodic: BlobsAreLive checks,          ▼\n       │ deletion confirmation           Object Storage\n       │                                 (S3-compatible)\n  ┌──────────────────────────┐\n  │  Background Scrubber     │\n  │  (garbage collection)    │\n  └──────────────────────────┘\n```\n\n### Component roles\n\n| Component | Operated by | Purpose |\n|-----------|-------------|---------|\n| **Your Backend Canister** | You | Stores blob hashes on-chain. Issues upload certificates. Tracks which blobs are live vs. deleted. |\n| **Cashier Canister** | Caffeine | Manages payment accounts (cycles-based). Publishes the list of authorized gateway principals. Tracks budgets per data owner. |\n| **Storage Gateway** | Caffeine | Accepts file uploads (after verifying the upload certificate and budget). Serves file downloads. Endpoint: `https://blob.caffeine.ai` |\n| **Background Scrubber** | Caffeine | May periodically query your canister via `_immutableObjectStorageBlobsAreLive` to verify blobs are still needed. Calls `_immutableObjectStorageBlobsToDelete` and `_immutableObjectStorageConfirmBlobDeletion` to clean up deleted blobs. |\n\n---\n\n## Integration Checklist\n\nThis is the complete list of steps to integrate Caffeine Object Storage into your app.\n\n### 1. Implement the storage protocol on your canister\n\nYour backend canister must implement five `_immutableObjectStorage*` methods that the gateway\nand scrubber call automatically.\n\nSee [Canister API Reference](#canister-api-reference) for the full Candid interface and\n[the example backends](#repo-structure) for reference implementations in Rust and Motoko.\n\n### 2. Install the `icfs` CLI\n\n```bash\ncurl -L https://caffeinelabs.github.io/object-storage/artifacts/icfs/latest/icfs-linux-x86_64 -o icfs\nchmod +x ./icfs\n# Move to a directory in your PATH\n```\n\n### 3. Configure environment\n\n```bash\nexport CASHIER_CANISTER_ID=72ch2-fiaaa-aaaar-qbsvq-cai\nexport STORAGE_GATEWAY_URL=https://blob.caffeine.ai\nexport NETWORK_URL=https://icp-api.io\nexport PRIVATE_KEY_FILE=~/.config/dfx/identity/default/identity.pem\nexport WALLET_CANISTER_ID=$(dfx identity get-wallet --network ic)\n```\n\n### 4. Fund your payment account\n\n```bash\nicfs cashier payment-account top-up --amount 10T\nicfs cashier payment-account balance\n```\n\n### 5. Deploy your backend canister\n\n```bash\n# Rust\ncd rust-backend \u0026\u0026 dfx deploy --network ic\n\n# Motoko\ncd motoko-backend \u0026\u0026 mops install \u0026\u0026 dfx deploy --network ic\n```\n\nYou can optionally pass a custom Cashier canister ID at init time (see [Init Arguments](#init-arguments)).\n\n### 6. Fetch gateway principals\n\nThe gateway needs to be authorized on your canister so it can manage blob lifecycle\n(liveness checks, deletion confirmation). Call `_immutableObjectStorageUpdateGatewayPrincipals`\non your canister — it queries the Cashier for the current list of gateway principals and\nstores them locally:\n\n```bash\ndfx canister call example_backend _immutableObjectStorageUpdateGatewayPrincipals '()' --network ic\n```\n\n**Why:** The gateway calls `_immutableObjectStorageBlobsAreLive`, `_immutableObjectStorageBlobsToDelete`,\nand `_immutableObjectStorageConfirmBlobDeletion` on your canister. These methods check\n`caller_is_gateway()` — the gateway must be in your authorized list.\n\n**Note:** The list of gateways is dynamic and may change over time as new gateways are added\nor removed. Call `_immutableObjectStorageUpdateGatewayPrincipals` periodically (or after\nbeing notified of a gateway change) to stay up to date.\n\n### 7. Link your canister to your payment account\n\n```bash\nicfs cashier payment-account add-canister \\\n  --paid-canister $(dfx canister id example_backend --network ic) \\\n  --limit 5T\n```\n\n**Why:** The Cashier needs to know which payment account covers storage costs for your canister.\nThe `--limit` controls the maximum daily spend.\n\n### 8. Upload and download files\n\nSee the [Upload Protocol](#upload-protocol) section for the full TypeScript implementation,\nor use the CLI:\n\n```bash\n# Upload\nicfs blob upload --input-file ./my-photo.jpg \\\n  --owner $(dfx canister id example_backend --network ic)\n\n# Download\nicfs blob download \\\n  --owner $(dfx canister id example_backend --network ic) \\\n  --root-hash sha256:ba7816bf… \\\n  --output-file ./downloaded.jpg\n```\n\n---\n\n## Upload Protocol\n\nUploading a file involves four steps: chunking + hashing, getting a certificate, sending the\nblob tree, and sending each chunk. The frontend example implements this in full — see\n[`frontend/src/storage-client.ts`](frontend/src/storage-client.ts).\n\n### Step 1: Chunk the file and build a merkle tree\n\nFiles are split into 1 MiB (1,048,576 byte) chunks. Each chunk is hashed with the\ndomain separator `icfs-chunk/`:\n\n```\nchunk_hash = SHA-256(\"icfs-chunk/\" || chunk_bytes)\n```\n\nThe chunk hashes form the leaves of a binary merkle tree (type: **DSBMTWH** —\nDomain-Separated Binary Merkle Tree With Headers). Internal nodes are computed with\nthe domain separator `ynode/`:\n\n```\nnode_hash = SHA-256(\"ynode/\" || left_child_hash || right_child_hash)\n```\n\nIf a level has an odd number of nodes, the missing right sibling uses the sentinel\nvalue `\"UNBALANCED\"` (the literal UTF-8 bytes, not a hash).\n\nFile metadata headers (Content-Type, Content-Length) are hashed with the domain\nseparator `icfs-metadata/` and combined with the chunk tree root:\n\n```\nmetadata_hash = SHA-256(\"icfs-metadata/\" || sorted_header_lines)\nroot_hash = SHA-256(\"ynode/\" || chunks_root || metadata_hash)\n```\n\nThe resulting root hash is formatted as `sha256:\u003c64-hex-chars\u003e`.\n\n### Step 2: Get an upload certificate from your canister\n\nCall `_immutableObjectStorageCreateCertificate(root_hash)` on your backend canister as\nan **update call**. This does two things:\n\n1. Records the hash as a live blob on your canister.\n2. Returns `{ method: \"upload\", blob_hash: root_hash }`.\n\nThe important part is **not** the return value — it's the **IC response certificate**\nattached to the update call response. This certificate proves that the canister authorized\nthe upload. Extract it from the response body:\n\n```typescript\nconst result = await agent.call(canisterId, {\n  methodName: '_immutableObjectStorageCreateCertificate',\n  arg: IDL.encode([IDL.Text], [rootHash]),\n});\nif (isV4ResponseBody(result.response.body)) {\n  const certificateBytes = result.response.body.certificate;\n  // Use certificateBytes in the next step\n}\n```\n\n### Step 3: Send the blob tree to the gateway\n\n```\nPUT {gateway}/v1/blob-tree/\nContent-Type: application/json\n```\n\nRequest body:\n```json\n{\n  \"blob_tree\": {\n    \"tree_type\": \"DSBMTWH\",\n    \"chunk_hashes\": [\"sha256:...\", \"sha256:...\"],\n    \"tree\": { \"hash\": \"sha256:...\", \"left\": {...}, \"right\": {...} },\n    \"headers\": [\"Content-Length: 12345\", \"Content-Type: application/octet-stream\"]\n  },\n  \"bucket_name\": \"default-bucket\",\n  \"num_blob_bytes\": 12345,\n  \"owner\": \"\u003cyour-canister-id\u003e\",\n  \"project_id\": \"0000000-0000-0000-0000-00000000000\",\n  \"headers\": [\"Content-Length: 12345\", \"Content-Type: application/octet-stream\"],\n  \"auth\": {\n    \"OwnerEgressSignature\": [/* certificate bytes as number array */]\n  }\n}\n```\n\nThe gateway verifies that:\n- The certificate is a valid IC response certificate\n- The certified response contains `method: \"upload\"` and the matching `blob_hash`\n- The canister has sufficient budget (checked against the Cashier)\n\nResponse:\n```json\n{\n  \"status\": \"blob_tree_accepted\",\n  \"existing_chunks\": [\"sha256:aabb...\", \"sha256:ccdd...\"],\n  \"chunk_check_errors\": 0\n}\n```\n\nThe `existing_chunks` array lists chunk hashes that the gateway already has stored.\nSkip uploading any chunks whose hashes appear in this list. If `chunk_check_errors`\nis non-zero, some checks failed and those chunks should be re-uploaded (safe default).\nIf the response cannot be parsed (e.g. older gateway), fall back to uploading all chunks.\n\n### Step 4: Upload each chunk\n\n```\nPUT {gateway}/v1/chunk/?owner_id=...\u0026blob_hash=...\u0026chunk_hash=...\u0026chunk_index=...\u0026bucket_name=...\u0026project_id=...\nContent-Type: application/octet-stream\nBody: \u003craw chunk bytes\u003e\n```\n\nChunks can be uploaded in parallel (the example uses up to 10 concurrent uploads).\nIf a chunk already exists, the gateway returns `{ \"status\": \"chunk_already_exists\" }`\nwithout re-storing it.\n\n### Downloading a file\n\nConstruct the download URL:\n\n```\nGET {gateway}/v1/blob/?blob_hash=sha256:...\u0026owner_id=\u003ccanister-id\u003e\u0026project_id=\u003cproject-id\u003e\n```\n\nThe gateway serves verified data — no client-side merkle proof verification is needed.\n\n---\n\n## Canister API Reference\n\nBoth the Rust and Motoko backends expose the same interface. The methods are organized into\nthree groups based on who calls them.\n\n### Init Arguments\n\nThe canister optionally accepts a Cashier canister ID at init time. If not provided,\nit defaults to the production Cashier (`72ch2-fiaaa-aaaar-qbsvq-cai`):\n\n```candid\ntype InitArgs = record {\n    cashier_canister_id : opt principal;\n};\n\nservice : (opt InitArgs) -\u003e { ... };\n```\n\nPass a custom Cashier ID when deploying to a dev or test environment.\n\n### User-facing API (called by your frontend)\n\n| Method | Signature | Purpose |\n|--------|-----------|---------|\n| `_immutableObjectStorageCreateCertificate` | `(text) -\u003e (CreateCertificateResult)` | Call with the `sha256:...` root hash before uploading. Records the blob as live and returns a certificate (via the IC response) that the gateway requires. |\n| `set_blob_info` | `(text, text, nat64, text) -\u003e ()` | Attach display metadata (name, size, content type) to a blob after upload. |\n| `list_blobs` | `() -\u003e (vec BlobInfo) query` | List all live blobs with metadata. |\n| `delete_blob` | `(text) -\u003e ()` | Mark a blob for deletion. The scrubber will remove it from storage. |\n\n### Gateway / Scrubber API (called automatically — do not call from your frontend)\n\n| Method | Signature | Called by | Purpose |\n|--------|-----------|-----------|---------|\n| `_immutableObjectStorageUpdateGatewayPrincipals` | `() -\u003e ()` | Gateway / Admin | Queries the Cashier canister for the current list of gateway principals and stores them locally. Call after deployment and periodically to pick up gateway changes. |\n| `_immutableObjectStorageBlobsAreLive` | `(vec blob) -\u003e (vec bool) query` | Background Scrubber | May periodically check whether blobs (each identified by a 32-byte hash) are still needed. Returns a `vec bool` in the same order as the input — `true` if the blob is live and not marked for deletion. This  |\n| `_immutableObjectStorageBlobsToDelete` | `() -\u003e (vec text) query` | Background Scrubber | Returns hashes of blobs marked for deletion. Only responds to authorized gateway principals. |\n| `_immutableObjectStorageConfirmBlobDeletion` | `(vec blob) -\u003e ()` | Background Scrubber | Confirms blobs have been removed from storage. The canister then removes them from its state. |\n\n### Candid types\n\n```candid\ntype BlobInfo = record {\n    hash         : text;\n    name         : text;\n    size         : nat64;\n    content_type : text;\n    created_at   : nat64;\n};\n\ntype CreateCertificateResult = record { method : text; blob_hash : text };\n\ntype InitArgs = record {\n    cashier_canister_id : opt principal;\n};\n```\n\n---\n\n## Pricing\n\n\u003e **Disclaimer:** Pricing is set by Caffeine and may change at any time as upstream ICP resource\n\u003e costs (compute, storage, network) change. Always query the live price list before making\n\u003e cost-sensitive decisions.\n\nQuery the current price list at any time:\n\n```bash\ndfx canister call 72ch2-fiaaa-aaaar-qbsvq-cai pricelist_v1 '()' --network ic\n```\n\nCurrent charges include:\n\n| Resource         | Notes                                              |\n|------------------|----------------------------------------------------|\n| Storage          | Charged per GB per 30 days (prepaid)               |\n| Upload           | Charged per GB uploaded                            |\n| Download         | Charged per GB downloaded                          |\n| Requests         | Charged per 1 000 read/write requests              |\n\nStorage is prepaid for 30 days on upload. If your balance reaches zero, existing data is retained\nbut inaccessible until you top up. After 30 days of zero balance, data is deleted.\n\n---\n\n## Current Status\n\n**Single Caffeine-maintained gateway.** All uploads and downloads are served by a single gateway\noperated by Caffeine at `https://blob.caffeine.ai`. This provides a simple, reliable starting\npoint.\n\n**Near future — multiple gateways.** We plan to add support for multiple independent gateways so\nyou can choose between them based on latency, pricing, or trust. If you need multi-gateway support\nsooner, [submit a feature request](https://github.com/caffeinelabs/object-storage/issues/new?template=feature_request.md\u0026title=Multi-gateway+support).\n\n**Payment — Cycles Ledger.** The gateway currently accepts payment in ICP cycles via the\n[Cycles Ledger](https://internetcomputer.org/docs/current/developer-docs/defi/cycles/cycles-ledger).\nSupport for additional ledgers (e.g. ckUSDC, ckUSDT) is planned. If you need a specific ledger\nsooner, [submit a feature request](https://github.com/caffeinelabs/object-storage/issues/new?template=feature_request.md\u0026title=Payment+ledger+support).\n\n---\n\n## Getting Started\n\nInstall the prerequisites below, then run the [recommended test script](#run-tests-recommended) to\nbuild and verify the example. When you are ready to use it on the IC, follow the\n[Integration Checklist](#integration-checklist).\n\n### Prerequisites — install these first\n\n| Tool    | Version   | Install |\n|---------|-----------|---------|\n| **Rust** (cargo) | stable | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \\| sh` |\n| **dfx** | ≥ 0.28 | `sh -ci \"$(curl -fsSL https://internetcomputer.org/install.sh)\"` |\n| **mops** | latest | `curl -fsSL cli.mops.one/install.sh \\| sh` (or `npm i -g ic-mops`) — only for Motoko backend |\n| **Node** | ≥ 20 | [nodejs.org](https://nodejs.org) — for frontend and npm-based mops |\n| **Python** | 3.10+ | For the [recommended test script](#run-tests-recommended); PocketIC is downloaded by the script when needed. |\n\nVerify:\n\n```bash\ncargo --version\ndfx --version\n# If using Motoko: mops --version\n# If using frontend: node --version\n# For the test script: python3 --version  (3.10+)\n```\n\nFor **production deployment** you also need a **cycles wallet** with enough cycles to fund the\npayment account (at least 10 T cycles recommended, more is better). Obtain cycles by converting ICP via the\n[NNS dapp](https://nns.ic0.app).\n\n### Run tests (recommended)\n\nFrom the repo root:\n\n```bash\npython3 scripts/run_tests.py\n```\n\nThe script checks for `cargo`, `dfx`, and `mops`; downloads the PocketIC binary into `.tools/` if\nmissing; builds both backends; and runs Rust unit tests and PocketIC canister tests.\n\nManual alternative: see [docs/automation-options.md](docs/automation-options.md).\n\n### (Optional) Deploy the frontend\n\n```bash\ncd frontend\nnpm install\ndfx deploy --network ic\n```\n\nConfigure the frontend via `.env`:\n\n| Variable | Default | Purpose |\n|----------|---------|---------|\n| `VITE_CANISTER_ID` | — | Your deployed backend canister ID (required) |\n| `VITE_STORAGE_GATEWAY_URL` | `https://blob.caffeine.ai` | Storage gateway URL |\n| `VITE_IC_URL` | `https://icp-api.io` | IC network URL |\n\n---\n\n## Monitoring\n\n```bash\n# Current balance\nicfs cashier payment-account balance\n\n# Full audit log\nicfs cashier payment-account audit-log\n\n# Canister-specific spending\nicfs cashier payment-account audit-log | grep \u003cCANISTER_ID\u003e\n```\n\nPrometheus metrics exposed by the Cashier:\n\n| Metric                                                            | Alert when…           |\n|-------------------------------------------------------------------|-----------------------|\n| `ic_cashier_payment_account_balance{owner=\"\u003cprincipal\u003e\"}`         | `\u003c 5T`                |\n| `ic_cashier_auto_topup_skipped_insufficient_balance_total`        | increases             |\n\n---\n\n## Repo Structure\n\n```\nimmutable-object-storage-example/\n├── README.md               This file\n├── scripts/\n│   ├── run_tests.py        Build + run all tests (Python 3.10+)\n│   └── setup.sh            Automates deploy steps\n├── rust-backend/           Rust ic-cdk canister\n│   ├── Cargo.toml\n│   ├── dfx.json\n│   └── src/\n│       ├── lib.rs          Canister entry + app API\n│       └── storage.rs      Storage protocol implementation\n├── motoko-backend/         Motoko canister\n│   ├── dfx.json\n│   ├── mops.toml\n│   └── src/main.mo\n├── tests/                  PocketIC canister tests\n│   ├── Cargo.toml\n│   └── src/\n│       ├── lib.rs          Shared helpers and Candid types\n│       ├── rust_backend.rs\n│       └── motoko_backend.rs\n└── frontend/               React + Vite frontend\n    ├── package.json\n    ├── dfx.json\n    └── src/\n        ├── App.tsx          Upload/download UI\n        ├── canister.ts      IC agent wiring\n        └── storage-client.ts  Full upload protocol implementation\n```\n\n## Running the Automated Setup\n\nIf you prefer a single script over the manual steps:\n\n```bash\n./scripts/setup.sh\n```\n\nThe script installs `icfs`, validates environment variables, tops up your payment account,\ndeploys the canister, registers the gateway principal, and links it to your payment account.\n\n---\n\n## Links\n\n| Resource                  | URL                                                                             |\n|---------------------------|---------------------------------------------------------------------------------|\n| Storage Gateway (prod)    | https://blob.caffeine.ai                                                        |\n| Cashier canister (prod)   | https://dashboard.internetcomputer.org/canister/72ch2-fiaaa-aaaar-qbsvq-cai    |\n| Storage Gateway (dev)     | https://dev-blob.caffeine.ai                                                    |\n| Cashier canister (dev)    | https://dashboard.internetcomputer.org/canister/xc7sj-uyaaa-aaaaf-qbrja-cai   |\n| Price list                | `dfx canister call 72ch2-fiaaa-aaaar-qbsvq-cai pricelist_v1 '()' --network ic` |\n| Feature requests          | https://github.com/caffeinelabs/object-storage/issues                           |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdfinity%2Fimmutable-object-storage-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdfinity%2Fimmutable-object-storage-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdfinity%2Fimmutable-object-storage-example/lists"}