Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 24 additions & 67 deletions docs/concepts/cid-computation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: 'How we use CIDs to bind payments to specific content'
---

<Tip>
**TL;DR**: we computes your file's CID (content hash) before you pay. This prevents uploading without payment or substituting different content after paying. Think of it as a cryptographic receipt - you're committing to pay for *this specific content*, not just "some files."
**TL;DR**: we pin your files to IPFS first, and the returned CID becomes the authoritative identifier. That CID goes into the on-chain deposit — binding your payment to the exact content that was pinned.
</Tip>

## What is a CID?
Expand All @@ -19,58 +19,45 @@ In traditional cloud storage, you pay for space regardless of what you store. Wi

### The Problem We're Solving

Without CID pre-computation, we run into a couple of issues:
- **Upload without paying**: Store files on IPFS first, then never actually pay
Without binding payments to a specific CID, we'd run into issues like:
- **Upload without paying**: Pin files to IPFS first, then never actually pay
- **Pay once, upload many**: Reuse a single payment for multiple different files
- **Content substitution**: Pay for one file, upload something completely different
- **No verification**: Server can't prove the payment matches the content stored
- **Content substitution**: Pay for one file, store something completely different

Solving this required implementing CID pre-computation, which is particularly complex for multi-file uploads.
### The Pin-First Approach

### How we hacked this

We compute the CID **before** creating the blockchain deposit, making it a **cryptographic commitment**:
We pin files to IPFS **before** creating the on-chain deposit, making the CID a **cryptographic commitment**:

```
User selects files
Server receives files
Server computes CID
Server pins files to IPFS
CID is included in blockchain deposit transaction
Server returns authoritative CID
User pays SOL (locked to this specific CID)
CID is included in the on-chain deposit
Files uploaded to Storacha
User pays (locked to this specific CID)
Server verifies uploaded CID matches committed CID
Server marks upload active
```

If the CID doesn't match, the upload is rejected.
The CID in the deposit is exactly what was pinned — no separate verification step needed.

## How It Works

### 1. CID Computation
### 1. CID from IPFS

When you upload files, the server:
When you upload files, we pin them to IPFS and receive the authoritative CID back:

```typescript
// Creates a map of filename → file bytes
const fileMap: Record<string, Uint8Array> = {
'file1.jpg': Uint8Array.from(file1Buffer),
'file2.pdf': Uint8Array.from(file2Buffer),
};

// Computes IPFS-compatible CID
const cid = await computeCID(fileMap);
// Returns: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
```
bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
```

This uses the same algorithm as IPFS:
IPFS uses:
- **SHA-256** hashing
- **dagPB** codec (IPFS directory structure)
- **CAR** (Content Addressable aRchive) format
- **UnixFS** for file and directory representation

### 2. CID in Blockchain Deposit

Expand All @@ -97,24 +84,9 @@ This means:
- Different CID = different deposit account
- Can't reuse deposits for different content

### 3. Verification on Upload

When files are actually stored on Storacha:

```typescript
// Upload file to IPFS
const uploadedCID = await client.uploadFile(files);

// Verify it matches
if (uploadedCID !== precomputedCID) {
throw new Error('CID mismatch! Upload rejected.');
}
```
### 3. Payment Confirmed

This double-check ensures:
- You uploaded the exact content you paid for
- No tampering occurred
- Payment is bound to verified content
Once the on-chain transaction is confirmed, we mark the upload active in our database. The CID is now permanently linked to your payment — accessible for the full duration you paid for.

## Benefits

Expand Down Expand Up @@ -166,16 +138,7 @@ const deposit2 = createDeposit({ cid: "bafyabc..." }); // ❌ Fails

### Prevents Content Substitution

```typescript
// User commits to uploading cat.jpg (CID: bafyabc...)
await createDeposit({ cid: "bafyabc..." });

// User tries to upload dog.jpg instead
await uploadToStoracha(dogFile);
// Server computes CID: bafyxyz...
// Verification fails: bafyxyz !== bafyabc
// Upload rejected ❌
```
Because we pin files to IPFS before building the deposit, the CID in the on-chain deposit is always exactly what was pinned. There's no window to swap content after paying — the pin and the payment reference the same CID.

### Enables Renewals

Expand Down Expand Up @@ -231,16 +194,10 @@ await renewStorageDuration({
- Prevents duplicate deposits
</Accordion>

<Accordion title="Why Server-Side Computation?">
You might wonder: why not compute CID in the browser?

**Reasons for server-side:**
1. **File Size**: Large files could exhaust browser memory
2. **Consistency**: Server ensures correct IPFS compatibility
3. **Performance**: Server has more resources for hashing
4. **Immediate Verification**: Server validates before blockchain transaction
<Accordion title="Why Pin First?">
Pinning before creating the deposit means the CID in the on-chain record is always the real IPFS CID — not a locally computed estimate that might differ from what the storage backend produces.

**Tradeoff**: This requires trusting the server's CID computation. Future versions could add browser-side verification for transparency.
Different IPFS implementations can produce different CIDs for the same content depending on chunk size, codec settings, and directory encoding. By pinning first and using the returned CID, we eliminate that mismatch entirely.
</Accordion>
</AccordionGroup>

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/renewal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ When you upload files with toju, you specify a storage duration (e.g., 30 days).

- Files expire and are marked for deletion
- You receive an email (only if you provide it) warning 7 days before expiration
- Expired files are automatically removed from Storacha
- Expired files are automatically unpinned from IPFS
- **Data is permanently lost** unless renewed

Renewal prevents data loss by extending the expiration date.
Expand Down
28 changes: 14 additions & 14 deletions docs/concepts/storage-payments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ We implement a pay-as-you-go model for decentralized storage using blockchain pa
For SOL: a deposit is created on the Solana blockchain via our smart contract. For USDFC: a token transfer is sent on Filecoin.
</Step>

<Step title="Storage Delegation">
The server uses UCAN delegation to store your files on Storacha. If you're curious about how UCANs work in practice, see [An up-close look at UCANs in the wild](https://www.meje.dev/blog/ucans-in-the-wild).
<Step title="Storage Provisioned">
We pin your files to IPFS. The CID is linked to your payment and stored in our database.
</Step>
</Steps>

Expand All @@ -46,7 +46,7 @@ Storage costs are calculated using this formula:
Cost (SOL) = (File Size in MB × Duration in Days × Base Rate) + Transaction Fee
```

The base rate is dynamically calculated to cover Storacha storage costs while keeping prices competitive.
The base rate is dynamically calculated to cover IPFS storage costs while keeping prices competitive.

<Info>
You can estimate costs before uploading using the `estimateStorageCost` SDK method.
Expand All @@ -59,29 +59,29 @@ sequenceDiagram
participant User
participant Wallet
participant Server
participant IPFS
participant Contract
participant Storacha

User->>Server: Request cost estimate
Server-->>User: Return cost (SOL or USDFC)
User->>Server: Upload files + request deposit instructions
Server->>IPFS: Pin files
IPFS-->>Server: Return CID
Server-->>User: Return CID + payment instructions
User->>Wallet: Sign transaction
Wallet->>Contract: Send payment (SOL deposit or USDFC transfer)
User->>Server: Submit files + transaction signature
User->>Server: Submit transaction signature
Server->>Contract: Verify payment onchain
Server->>Storacha: Upload files with UCAN
Storacha-->>Server: Return CID
Server-->>User: Confirm upload with CID
Server-->>User: Confirm upload with CID + URL
```

## Payment Model

When you pay for storage:

1. **Payment is processed** — SOL via our smart contract, or USDFC via direct token transfer
2. **Storage is provisioned** immediately on Storacha
1. **Files are pinned** to IPFS before the deposit is created
2. **Payment is processed** — SOL via our smart contract, or USDFC via direct token transfer
3. **Files are accessible** for the full duration you paid for

Your payment covers the cost of storing your data on Storacha through our infrastructure. The smart contract (SOL) provides on-chain accounting and deposit verification, while USDFC payments are direct token transfers.
Your payment covers the cost of storing your data on IPFS through our infrastructure. The smart contract (SOL) provides on-chain accounting and deposit verification, while USDFC payments are direct token transfers.

This model ensures:
- No recurring subscriptions or credit cards needed
Expand Down Expand Up @@ -114,7 +114,7 @@ Coming soon:
- **Additional chains**: Based on community demand

<Note>
All payment methods store data on the same Storacha network. You choose whichever chain you prefer &mdash; the storage experience is identical.
All payment methods store data on the same IPFS network. You choose whichever chain you prefer &mdash; the storage experience is identical.
</Note>

## Benefits
Expand Down
6 changes: 3 additions & 3 deletions docs/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ description: 'Decentralized storage payments with SOL and USDFC'

*tọjú* (pronounced "toh-joo") means "to keep" in [Yoruba](https://en.wikipedia.org/wiki/Yoruba_language), a language spoken by over 50 million people in West Africa.

We built toju to bridge the gap between crypto users and decentralized storage on Filecoin via [Storacha](https://storacha.network). No credit cards, no subscriptions &mdash; just pay for IPFS storage with SOL or USDFC.
We built toju to bridge the gap between crypto users and decentralized storage on IPFS. No credit cards, no subscriptions &mdash; just pay for IPFS storage with SOL or USDFC.

## What this is.

toju is a **payment bridge** that enables crypto-native users to pay for decentralized storage on IPFS via Storacha, without the traditional hassle of credit cards or subscriptions.
toju is a **payment bridge** that enables crypto-native users to pay for decentralized storage on IPFS, without the traditional hassle of credit cards or subscriptions.

We're not a storage layer &mdash; we're the onramp that lets you pay for Storacha storage using your crypto wallet. We currently support **Solana (SOL)** and **Filecoin (USDFC)**, with more chains coming soon.
We're not a storage layer &mdash; we're the onramp that lets you pay for IPFS storage using your crypto wallet. We currently support **Solana (SOL)** and **Filecoin (USDFC)**, with more chains coming soon.

## Key Features

Expand Down
32 changes: 14 additions & 18 deletions docs/sdk/deposit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ const result = await client.createDeposit({
```

<Note>
Multiple files are packaged as a directory with a single root CID. See [CID Computation](/concepts/cid-computation) for details.
Multiple files are packaged as a directory on IPFS. Pinata returns a single root CID for the directory. See [Content Identifiers](/concepts/cid-computation) for details.
</Note>

## Error Handling
Expand Down Expand Up @@ -312,36 +312,32 @@ Common errors and solutions:
What happens when you call `createDeposit`:

<Steps>
<Step title="Client-Side CID Computation">
Files are processed in the browser to compute the CID
<Step title="Pin to IPFS">
Server receives your files and pins them to IPFS. The server returns the authoritative CID for your content.
</Step>

<Step title="Upload to IPFS">
Files are uploaded to Storacha's IPFS nodes
</Step>

<Step title="Create Transaction">
A Solana transaction is built with payment details

<Step title="Build Transaction">
A Solana transaction is built using that CID along with the payment amount and duration
</Step>

<Step title="Sign Transaction">
Your `signTransaction` callback is invoked
</Step>

<Step title="Submit to Network">
Transaction is submitted to Solana blockchain
</Step>

<Step title="Wait for Confirmation">
SDK waits for transaction confirmation
</Step>
<Step title="Server Processing">
Server receives deposit event and records it in the database

<Step title="Confirm Upload">
Server marks the upload active in the database, linking the transaction signature to the pinned CID
</Step>

<Step title="Return Result">
CID, signature, and URL are returned to your app
CID, transaction signature, and gateway URL are returned to your app
</Step>
</Steps>

Expand Down
4 changes: 2 additions & 2 deletions docs/sdk/x402.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The flow under the hood:
Coinbase's public facilitator verifies and settles the USDC transfer on Base
</Step>
<Step title="File stored">
Server uploads to IPFS via Storacha and returns the CID
Server pins to IPFS and returns the CID
</Step>
</Steps>

Expand Down Expand Up @@ -184,7 +184,7 @@ const storeFileTool = tool(
},
{
name: "store_file_ipfs",
description: "Store a file on IPFS via decentralized storage (Storacha) and pay with USDC autonomously on Base.",
description: "Store a file on IPFS via decentralized storage and pay with USDC autonomously on Base.",
schema: z.object({
filePath: z.string().describe("Absolute path to the file to store"),
durationDays: z.number().describe("How many days to store the file"),
Expand Down
2 changes: 1 addition & 1 deletion ui/src/containers/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export const HomePage = () => {
}}
fontWeight="bold"
>
Storacha
Pinata
</Text>
</HStack>
<HStack cursor="pointer">
Expand Down
Loading