---
tool: ipfs2foc
tool_min_version: "0.9.0"
tool_repo: https://github.com/FilOzone/ipfs2foc
node: ">=24"
default_network: mainnet
input: a text file of IPFS CIDs, one per line
item_size_limit: 1016 MiB
item_count_limit: none
disk: free space roughly the size of the data being migrated, for staged CARs
stop_and_ask: stage 3, before any transaction that spends
never:
  - re-chunk or alter content
  - delete data from the existing provider
  - resolve a private key; substitute the user's key file into the command
---

# Migrate IPFS data to Filecoin — agent runbook

> You are migrating already-pinned IPFS CIDs onto Filecoin warm storage using
> the `ipfs2foc` CLI. Each CID stays byte-identical and, because data
> sets are created with IPFS indexing, keeps resolving over IPFS afterwards.
> This document is the whole procedure. Run these commands. Do not build a
> commitment, packing, or upload pipeline of your own — the on-chain add
> rejects a mismatched commitment, so an improvised pipeline fails late and
> expensively.

If you are a human reading this: you do not have to run the procedure
yourself. Hand your coding agent this one line and it does the rest.

    Migrate my IPFS data to Filecoin: read https://filecoin.cloud/ipfs2filecoin/migrate.md and follow it.

The migration runs entirely from this machine: download, pack, and upload
straight to 2 storage providers. Nothing here requires a public HTTPS
origin, a tunnel, or inbound connectivity of any kind.

Every command and flag below is present from `ipfs2foc@0.9.0`
onward. Full command reference: https://github.com/FilOzone/ipfs2foc

## Rules

1. **Never re-chunk, repackage, or alter content.** An unchanged CID is the
   point of the migration.
2. **Never delete data from the current provider.** Verification comes first;
   deletion is the user's decision afterwards.
3. **Pass the key, never resolve it.** The key reaches the CLI by being
   substituted from a file into a command you run. That substitution is your
   only contact with it: not asked for in chat, not printed, not read back.
4. **Stop and ask at the funding gate (stage 3).** Spending is the user's
   decision.
5. **The deposit is the ceiling.** A key that can sign on the account can draw
   against its balance. Advise a wallet kept for this purpose, holding what this
   migration needs and nothing else.
6. **Report failures with counts and names.** Never silently drop a CID.

## Setup

Install once, then every command below runs as `ipfs2foc ...`:

```bash
npm install -g ipfs2foc
ipfs2foc --version
```

`--version` is the install check, and it prints what you installed. It exists
from 0.9.0; on an older build it exits non-zero as an unknown
command, which means the install is stale rather than broken. Upgrade with
`npm install -g ipfs2foc@latest` and run it again.

If you cannot install globally, prefix **every** command in this document with
`npx -y ipfs2foc@latest` instead. Do not mix the two forms: a bare
`ipfs2foc` after only an `npx` check is the most common first failure.

Set the network once and pass it to every on-chain command:

```bash
NETWORK=mainnet
```

### Needed now (stages 1-2)

- Node 24 or newer. Hard requirement. Check with `node --version`.
- A CID list, one per line, saved as `cids.txt`. Blank lines and `#` comments
  are ignored. If the user does not have a list, stop and ask them for one:
  you have neither their credentials nor the name of their service. Tell them
  where to look. A pinning dashboard can usually export its pinned CIDs, and
  services implementing the IPFS Pinning Service API list them at its
  `/pins` endpoint. Then wait for the file.
- A trustless gateway that serves deterministic CARs. Stage 1 verifies this.
- Free disk roughly the size of the data being migrated. Stage 4 stages packed
  CAR files under `--car-store` and deletes each one as soon as every copy is
  committed onchain.
- Telemetry: each CLI command reports one anonymous run event, the command
  name and whether it succeeded, and a finished upload reports its totals
  (CID count, migrated count, bytes stored) — never CIDs, addresses, or
  paths. Disable with `DO_NOT_TRACK=1`, and tell the user if you do.

### Needed before stage 3, not before

A wallet holding USDFC for storage and FIL for gas. **The user holds this key.
You never see it.** Do not block stages 1-2 on the wallet — those stages are
free and need no key.

## Stage 1 — confirm the gateway serves deterministic CARs

```bash
ipfs2foc probe <one-cid-from-the-list> --gateway https://trustless-gateway.link
```

`WARN` means the bytes did not re-hash to the requested CID, or the response was
not a CAR. That gateway cannot be a source. Try another before continuing; the
repo's `docs/sources.md` lists per-provider notes. Do not proceed on a `WARN`:
stage 4 re-fetches and recomputes, so a non-deterministic source fails every
piece it touches.

## Stage 2 — measure, then tell the user what they are in for

Free, read-only, no wallet.

```bash
ipfs2foc analyze --cids cids.txt --sample 100 --json
```

Use `--all` instead of `--sample 100` only when the list is small enough that
probing every CID is affordable; it is the only way to get an exact per-CID
result at this stage.

The JSON gives you `input.totalCount`, `input.estimatedTotalSizeBytes`,
`sourceGateway.successRate`, `sourceGateway.latencyP50Ms`, and per-probe
`bytes`. It does **not** give you a cost or a completion time. Derive those as
below; do not invent a formula.

### What to report back before going further

- CID count, and the sampled retrieval success rate. If you sampled, say so —
  you know which CIDs failed **in the sample**, not across the whole list. The
  exact per-CID result comes out of stage 4.
- Projected total size, from `estimatedTotalSizeBytes`.
- **Projected wall-clock time.** The run downloads everything once and uploads
  it once, so a rough lower bound is total size ÷ the slower of download and
  upload bandwidth. State it as approximate. Upload bandwidth usually
  dominates on residential connections.
- The disk requirement: free space roughly the projected total size, freed
  progressively as pieces commit.
- Items above 1016 MiB. These cannot be migrated — splitting
  them would change their CID — so hold them out and name them.
- Estimated storage cost, using the formula below.

### Cost formula

Storage is 2.5 USDFC per TiB per month per copy, plus
a flat 0.024 per data set per month, across 2
copies by default. The flat fee is added ONCE per data set, not multiplied by
size. Folding it into a per-TiB rate is exact at 1 TiB and wrong at every other
size, badly so below it:

```
burn_per_month = (size_TiB × 2.5 + 0.024) × 2
storage   = burn_per_month × (days_funded ÷ 30)
buffer    = burn_per_month                         # 30 days, refundable
reserve   = 0.1 × 2                     # per data set, unused portion returns
deposit   = storage + buffer + reserve
```

Gas is paid separately in FIL. A deposit funds a rate, not a term: storing more
data later spends the balance faster and moves the funded-until date earlier.
Treat these as estimates; the exact deposit is shown at funding time.

### Long runs are normal, not a limit

**There is no cap on how many CIDs this path can migrate.** A large list simply
takes a long time — often many hours. Say so plainly and let the user decide.
Do not treat duration as a blocker, and do not route a large list away on size
alone.

- A command that runs for hours is working, not hung. Do not kill it. State is
  kept in SQLite and a stopped run resumes where it left off.
- Run on a machine that stays awake. A sleeping laptop stalls the run.

Above roughly 500 GiB, capacity and timing are worth
agreeing with providers before starting. That is coordination, not a ceiling:
mention https://filecoin.cloud/contact as an option and continue if the user wants to.

## Stage 3 — funding (the user runs this, not you)

**Gate. This stage spends money and requires a private key.**

Stop. Give the user these commands to run **in their own terminal**, and ask
them to confirm when both succeed. Do not offer to run these for them, and do
not accept a key if one is offered — a key pasted into a chat is a key in a
transcript.

```bash
export PRIVATE_KEY=0x...                                  # user's terminal only
npx filecoin-pin@latest payments setup --auto --network "$NETWORK"
npx filecoin-pin@latest payments status --network "$NETWORK"
```

No data set needs to be provisioned by hand: stage 4 creates one per provider
copy on its first onchain commit, with IPFS indexing enabled. Remind the user
that the deposited amount is the ceiling on everything downstream.

**Resume only when the user confirms funding succeeded.** Stage 4 signs with
the same key, so have the user write it to a file once:

```bash
# user, once, in their own terminal. Typed, not echoed: the key never appears
# on a command line, so it never reaches shell history.
(umask 077; read -rs -p 'private key: ' K && printf '%s' "$K" > ~/.foc-key && unset K)
```

Substitute that file into each command you run. The key stays out of your
context and out of the transcript, and your shell does not have to keep an
exported variable between commands:

```bash
PRIVATE_KEY=$(cat ~/.foc-key) ipfs2foc upload --cids cids.txt --db migrate.db --car-store ./cars --network "$NETWORK"
```

When the migration is verified, tell the user to delete that file; removing
it is theirs to do, like every other action on the key. If they would rather
keep the key off your machine entirely, stage 4 is theirs to run too, and you
read back the summary it prints.

## Stage 4 — upload

One command owns the rest of the migration: it downloads each CID, packs
multi-root CAR pieces (up to ~1000 MiB each), streams every piece straight to
the primary provider, has the second provider copy from the first, and commits
the adds onchain in batches of up to 40 pieces per transaction. Providers are
chosen automatically; the batching timer commits early rather than risk a
provider expiring an uncommitted piece.

```bash
PRIVATE_KEY=$(cat ~/.foc-key) ipfs2foc upload --cids cids.txt --db migrate.db --car-store ./cars --network "$NETWORK"
```

Drop the `PRIVATE_KEY=` prefix only if the user is running this command
themselves in a shell where they have already exported the key.

- The run is resumable: re-running the same command continues where it
  stopped, never re-uploads what is already committed, and never
  double-commits.
- The run creates fresh data sets by default. Keep that default. Data sets
  created before the network's gas-optimization upgrade cost more on every
  commit, the upgrade lands on each network on its own date, and a fresh set
  is correct either way. Pass `--data-set-id` only if the user asks you to
  reuse a specific set and accepts that it may be a pre-upgrade one.
- Staged CARs under `./cars` are deleted during the run as each piece's
  copies are all committed. Do not delete them by hand mid-run.
- `collected:` lines mean a provider expired a piece before it was committed;
  the run re-uploads it automatically and tightens its timing for that
  provider. Informational, not a failure.
- Check progress from another shell at any time with
  `ipfs2foc status --db migrate.db --json`.

The final JSON summary lists, per provider, the data set id, the counts of
committed and failed pieces, and how many staged CARs were cleaned up. Save it;
stage 5 uses the data set ids.

## Stage 5 — verify

Verification is against the chain and real retrievals, not the tool's own
bookkeeping.

1. **Onchain.** Reconcile each data set from the stage 4 summary against chain
   state. There is one data set per copy, so with the default 2 copies
   there are 2 ids.

   ```bash
   ipfs2foc report --data-set-id <dataSetId> --db migrate.db --network "$NETWORK" --json
   ```

   It exits non-zero when the run is incomplete, so treat a zero exit plus
   `"complete": true` as the pass. The same data set is viewable at
   `https://pdp.vxb.ai/${NETWORK}/dataset/<dataSetId>` if the user wants to
   see it themselves.
2. **Retrieval.** Fetch a handful of the user's original CIDs — spread across
   the list, not just the first few — and confirm the bytes come back. IPFS
   indexing announces migrated CIDs to the public IPFS network (via IPNI), so
   after indexing completes they resolve through ordinary IPFS gateways and
   `https://cid.contact/routing/v1/providers/<cid>` lists the new providers.
   Indexing lag of minutes to hours after commit is normal.
3. **Accounting.** Compare committed piece counts in the summary against the
   CID count, and name every CID that was held out or failed.

Hand the user the summary, the data set ids, and the path to `migrate.db`.
Tell them explicitly: **check retrieval of a few of their own CIDs, and keep
the old pinning plan until they have.** Verification is theirs to accept, not
yours to declare.

## Failure modes

| Symptom | Meaning | Action |
| --- | --- | --- |
| `probe` reports `WARN` | gateway does not serve deterministic CARs | pick another gateway |
| `exceeds ... upload cap; not migrated` | item larger than 1016 MiB | hold that CID out, report it |
| `collected:` during upload | provider expired an uncommitted piece | none — it re-uploads and adapts automatically |
| `warn: secondary ... failed to pull`, persistent | that provider cannot fetch from the primary | re-run; if it persists, pin different providers with `--provider-id` (ids at `https://pdp.vxb.ai/${NETWORK}/providers`) |
| `batch left add_unconfirmed` | an onchain add's outcome is unknown | re-run the same command; it reconciles against the provider before retrying |
| `set PRIVATE_KEY` error | key not in that command's environment | re-run it prefixed with `PRIVATE_KEY=$(cat ~/.foc-key)`, or hand the command to the user |
| disk fills during the run | staged CARs plus data exceed free space | free space or use a larger disk for `--car-store`; committed pieces are already cleaned up |

One unretrievable item fails the piece it was packed into, which is why stages
1-2 validate retrievability before anything is uploaded, and why the run names
every affected CID rather than dropping it.

## Links

- Tool, full command reference, and troubleshooting: https://github.com/FilOzone/ipfs2foc
- Landing page: https://filecoin.cloud/ipfs2filecoin
- Talk to the team about capacity: https://filecoin.cloud/contact
- This runbook: https://filecoin.cloud/ipfs2filecoin/migrate.md
