Skip to content

Recover Your Data ​

This guide walks you through reconstructing your full Stasho project and deployment history using only your wallet's private key. The recovery script does not depend on our backend: it talks directly to the public network your records are stored on. The stasho CLI plays no part here, because it talks to our API and recovery must work without it.

The script isn't public yet

Today you have two options: email hello@stasho.xyz and we'll send you a copy, or build your own tool from the storage schema, which is published in full and language-agnostic. Get one of them before you export any key.

Use only a copy that answers an email you sent. We never email it unasked and never ask for your key. Before you install it, read it: package.json has no install scripts and three runtime dependencies (eciesjs, ethers, and tsx, which runs the TypeScript entry point without a build step), and the source talks only to the API node you name (--aleph-api, by default https://api.aleph.im).

Network: records are Aleph messages on mainnet; API: https://api.aleph.im (the script's default; any public API node works, e.g. https://api2.aleph.im).

This key controls your wallet, not just your Stasho data. An agent that runs with the key in its environment can read it. For a wallet that holds funds, run step 2 yourself and give the agent only the output, using the second prompt.

Prompt for your agent

Copy this into Claude Code, Cursor or any agent that can run shell commands, and swap the placeholder. Use it only if you accept the warning above.

text
Read https://docs.stasho.xyz/guides/recover-your-data.md and rebuild my Stasho project and
deployment history from the public network, without the Stasho backend. The script is not
public: my copy is in <SCRIPT_FOLDER>, and it answers an email I sent. If I have no copy, stop and
tell me to ask hello@stasho.xyz for one before I export any key. Before installing, read
package.json and the source as the page says, and stop if they differ (the runtime dependencies must be exactly eciesjs, ethers and tsx). My wallet key is in the USER_PRIVATE_KEY environment variable
and my address is <WALLET_ADDRESS>: never ask me for the key in chat, never print it, never pass
it as a flag, and never write it anywhere. Run the recovery script, then tell me the counts from its output lines, report `complete` and,
when it is false, the INCOMPLETE line, and list every warning with its code. Do not print the output file's projects: they hold my decrypted data. If
the variable is not set because I signed in with email, Google, GitHub or Apple, read me the
page's Privy Home step and stop: you never drive that export.

When to use this ​

  • You no longer trust the platform and want to verify your data is portable
  • The platform has gone away (bankrupt, compromised, taken down)
  • You want to migrate your project history to a new tool
  • You want to audit what data exists about your projects on the public network

If Stasho itself may be compromised or gone, don't rely on an email from us: build your own tool from the storage schema, so none of our code touches your key.

What you need ​

  1. The recovery script. We send you a copy when you ask at hello@stasho.xyz. Check it as the box at the top says.

  2. Node.js 22 or later. Verify with node --version.

  3. Your wallet address. The 0x-prefixed Ethereum address you used to sign in.

  4. Your wallet's private key. Export it from the wallet app you sign in with; in MetaMask it is under the account's details. The script never transmits this key anywhere; it only uses it locally to decrypt your data. A hardware wallet cannot export its key, and the script takes a raw key only: don't type its seed into software to get one.

    If you signed in with email, Google, GitHub or Apple, your wallet is an embedded one held through Privy, and Stasho's dashboard has no export control. Export is available at Privy Home (https://home.privy.io), the page for wallet holders. It is not dashboard.privy.io: that is Privy's developer console, and its sign-in walks you through setting up an app, which is not what you want. At Privy Home, sign in with the email on your Stasho sign-in (the same code-by-email step), accept Privy's terms if asked, connect your Google, GitHub or Apple account from there if your wallet doesn't show, open the Ethereum wallet, choose Export private key, confirm, and copy the 64-character key it reveals. This step depends on Privy, not on Stasho, so do it now if independence matters to you. If your wallet doesn't show, or Privy is unavailable, this script can't run for your wallet: an email sign-in that never exported its key has no way to run this once Stasho and Privy are both gone. A key for a different wallet stops the script at exit code 12, which prints the address it derives to.

Sequence: you give the recovery script your wallet key, it fetches your project and deployment records from public network API nodes, decrypts them locally, and returns your projects and deployments. No Stasho backend is involved.Sequence: you give the recovery script your wallet key, it fetches your project and deployment records from public network API nodes, decrypts them locally, and returns your projects and deployments. No Stasho backend is involved.
Recovery talks to the public network and your key. Nothing else.

Steps ​

1. Install the script ​

In the folder we sent you (scripts/recovery/, if you have access to the repository), once you have read it:

bash
npm ci --ignore-scripts

2. Run it ​

The script reads your private key from the USER_PRIVATE_KEY environment variable. Never pass it as a CLI flag, and never type it after export on one line: either would leak it into your shell history. Read it from the terminal instead, which prints nothing as you paste. The parentheses run this in a subshell, so nothing started later in that terminal inherits the key:

bash
( printf 'Wallet key: '; read -rs USER_PRIVATE_KEY; echo; export USER_PRIVATE_KEY
  npm run recover -- --address 0xYOUR_WALLET_ADDRESS --output ./my-recovery.json )

Or pipe it from a password manager with --stdin, which reads the key from standard input instead of the environment. With 1Password's CLI, for example:

bash
op read "op://Private/My wallet/private key" \
  | npm run recover -- --stdin --address 0xYOUR_WALLET_ADDRESS --output ./my-recovery.json

The other options are --channel, --aleph-api and --verbose.

You should see output like:

[recover] Wrote 3 projects (1 deleted), 21 deployments, 0 warnings to ./my-recovery.json

When any record was lost, a second line follows:

[recover] INCOMPLETE: 0 decrypt failures, 1 malformed records, 0 untrusted messages, 0 truncated queries, 0 store fetch failures (exit 3)

The script exits 0 when it wrote a complete file. Otherwise:

Exit codeMeaning
2--address or --output is missing
3The file is written, but some records were lost: read the INCOMPLETE: line and the warnings
10No private key: USER_PRIVATE_KEY is not set, or --stdin read nothing
11The key is not 32 bytes of hex
12The key derives to a different address than --address
20The network API is unreachable
99Anything else; the message says what

The bundle command (see Recover an app bundle) has its own exits, besides 2 (a missing --from, --deployment or --output), 10, 11, 20 and 99 above:

Exit codeMeaning
30The fetched bytes do not match the deployment's recorded bundleHash
31The key does not derive the encryption key recorded in the recovery file
32The deployment is not in the file, has no bundleHash or cid, or the file is not a recovery file
33The bytes are not a stasho-bundle envelope, or they cannot be decrypted

3. Inspect the output ​

Open my-recovery.json. You'll see a JSON document with:

  • wallet: the address you recovered
  • recoveredAt: timestamp of the recovery
  • alephApiUrl and channel: where it read from
  • complete: true when no record was lost; false when any of the five failure codes in the table below came up
  • encryptionKey: the encryption key your wallet registered, or null when none was found
  • stats: counts of messages read, projects and deployments recovered (projectsRecovered, deploymentsRecovered, deletedProjects), and of decryption failures, malformed records, untrusted messages, truncated queries, store fetch failures and warnings
  • projects: your list of projects, each containing its own list of deployments
  • deletedProjects: projects you deleted, each with its deletedAt time and any deployments the script found for it
  • warnings: issues encountered during recovery, each with a code and a detail

Each project includes its name, source repo, branch, build command, output directory, rootDir, envVars, workflowFilename, customDomains, defaultSubdomain, currentDeploymentId and currentDeploymentSetAt (the rollback pin), and serving, plus its deployment history. serving names the deployment the project serves, in serving.deploymentId, with a reason: pinned, pin-superseded, newest-live, pin-missing (the pin names a deployment the script did not recover, so nothing serves) or none (no live deployment). Rebuild from serving.deploymentId, not from the newest entry: after a rollback, the newest build is the one the project moved away from.

Each deployment has its status, commit, error and the like, and for the artifact: cid, bundleHash (App VM bundles), artifactKind (ipfs or storage from the STORE, or null when the STORE is not resolved) and storeStatus (resolved, content-match, forgotten, removed, absent, unreachable, legacy-inline or no-artifact). content-match means the STORE was forgotten but another STORE still pins the same CID, so the cid is kept.

That is your decrypted data, and envVars are in the file with their values. The script writes the file readable by your user only (mode 600); keep it that way, and keep it out of any git commit.

Prompt for your agent

Copy this after you ran the script yourself. The agent needs no key for it.

text
Read https://docs.stasho.xyz/guides/recover-your-data.md and check ./my-recovery.json, which I
made with the Stasho recovery script. Report `complete`, stats, and explain every warning by its
code using the page's table, saying which ones mean an artifact is gone, which mean retry against
another node, and which five make `complete` false. For each project, say which deployment to
rebuild from: the one in `serving.deploymentId`, not the newest entry. Read only the fields you
need for that. Never ask me for my wallet key.

Schema v4 (May 2026 onwards): Each deployment POST references its artifact by storeRef (the STORE message's item_hash). The recovery script fetches each STORE to resolve storeRef → cid, and records the outcome per deployment as storeStatus.

If a project was deleted, its STORE has been FORGOTten: the script reports cid: null, storeStatus: "forgotten" and a STORE_FORGOTTEN warning, and the artifact CID is no longer recoverable. A deleted project is listed under deletedProjects with its deployments. When another STORE still pins the same CID, the script keeps the CID, reports storeStatus: "content-match" and a STORE_CONTENT_MATCH warning, and the bytes may still be retrievable.

If the network's own garbage collection reclaimed the STORE because the account paying for it ran out of balance or credits, the script reports storeStatus: "removed" with a STORE_REMOVED warning. The artifact is equally unrecoverable, but nobody deleted it, so the remedy is different: fund the account before re-uploading.

A STORE that simply isn't on the node you queried is reported differently, as storeStatus: "absent" with a STORE_ABSENT warning. This is not the same as "forgotten" or "removed". It means that node had no such message, which can also happen if the node is behind or you queried the wrong one. Try re-running against a different API node before concluding the artifact is gone.

4. Verify against the audit endpoint (optional) ​

If our backend is still running, you can compare your recovery output to what the backend claims:

bash
curl https://api.stasho.xyz/api/audit/0xYOUR_WALLET_ADDRESS | jq

backendView.cachedProjects and backendView.cachedDeployments should match your recovery output's stats.projectsRecovered and stats.deploymentsRecovered, except for two things: projects in deletedProjects, and deployments the service evicted (it keeps 50 per project, so a project with more has older deployments in your file that the service no longer lists). A difference that is neither of those means a record one side did not read: check complete and the warnings, re-run against another API node with --aleph-api <url>, and email us if it stays. The recovery script reads the network directly.

Recover an app bundle ​

If you deployed a project to an App VM rather than IPFS static hosting, your deployment history includes an extra artifact: the encrypted application bundle itself (the built server code your App VM runs). Recovering it uses the exact same cryptographic building blocks as the rest of this page, with one difference: the recovered bundle is a tar archive, not a JSON document, so the final step is not the same.

The bundle command rebuilds one deployment's bundle from a recovery file you already made. It needs the same key as step 2:

bash
( printf 'Wallet key: '; read -rs USER_PRIVATE_KEY; echo; export USER_PRIVATE_KEY
  npm run recover -- bundle --from ./my-recovery.json --deployment deploy_XXXXXXXXXXXX \
    --output ./bundle.tar.gz )

Take the deployment id from the file: an App VM deployment has a bundleHash and an artifactKind of storage. --aleph-api overrides the node recorded in the file, and --stdin and --verbose work as in step 2. The command checks, in this order:

  1. Key. The key must derive the encryption key the recovery file records (encryptionKey). A different key stops it with exit 31.
  2. Hash. It downloads the bundle's bytes from the node and compares sha256(bytes) with the deployment's bundleHash. A mismatch stops it with exit 30.
  3. Envelope. It decrypts the bundle envelope with deks.encryption. Bytes that are not a stasho-bundle envelope, or that do not decrypt, stop it with exit 33.

It writes the decrypted bytes to --output as they are and prints their size and sha256. The file is a gzipped tar archive. tar -xzf bundle.tar.gz gives you manifest.json and app.tar.gz; sha256(app.tar.gz) should equal manifest.artifactHash. Exit 32 means the deployment is not in the file, has no bundleHash or cid, or the file is not a recovery file; 20 means the node is unreachable. If you build your own tool, the steps are in the storage schema: the plaintext is raw bytes, so do not JSON.parse it.

Verify your implementation ​

Before trusting a hand-rolled version of this against real data, check it against the project's own round-trip test. It encrypts random bytes with freshly generated VM and user keypairs, decrypts through both recipients using the exact same calls above, and asserts the result matches the original byte-for-byte:

bash
cd apps/backend && npx vitest run tests/bundle-encrypt-script.test.ts

(This requires the project repository, which is private during the alpha.) The deployment's bundleHash, the STORE's item_type and the storage/raw endpoint it reads are described in the storage schema's instance section.

Troubleshooting ​

"private key derives to X, not Y". Your private key doesn't match the address you supplied. Double-check both.

"network API unreachable". Try a different API node with --aleph-api <url>.

Warnings about decryption failures. Some messages couldn't be decrypted. This usually means the encryption envelope is malformed or tampered with. The script skips these and continues, but the warnings tell you which messages had issues.

Empty output. Your wallet has no Stasho data on the current channel. Either you've never used the platform with this wallet, or you're looking at the wrong channel. The channel was renamed twice: ALEPH-CLOUDAPP → STASHO_CHANNEL_V0 on 2026-05-07 (Decision #98), then STASHO_CHANNEL_V0 → STASHO_CHANNEL_V1 on 2026-05-20 (Decision #117); alpha data on each prior channel was retired at the rename. The recovery script's default targets STASHO_CHANNEL_V1. To inspect an older channel, override with --channel STASHO_CHANNEL_V0 or --channel ALEPH-CLOUDAPP.

Warning codes ​

The recovery output includes a warnings array. The codes you might see:

CodeWhat it means
STORE_FORGOTTENA FORGET was broadcast for a deployment's STORE: a project delete, or an early drop claim's original upload. The artifact's cid is null and the bytes are gone from the network's index. Together with STORE_REMOVED, one of the two codes that mean the artifact is gone.
STORE_REMOVEDThe network's own garbage collection reclaimed the STORE because the account paying for it ran out of balance or credits. Nobody issued a delete. cid is null and the artifact is gone from the network's index; fund the account before re-uploading.
STORE_ABSENTThe node had no such STORE, or it wasn't usable (not a STORE message, carrying no CID, or rejected / still pending). Not the same as forgotten or removed: the node may be behind, or another node may still have it. Try re-running against a different node with --aleph-api <url>.
STORE_CONTENT_MATCHThe STORE was forgotten, but another processed STORE still pins the same CID, so the cid is kept (storeStatus: "content-match"). The bytes may still be retrievable.
LEGACY_SCHEMAA deployment uses the pre-May-2026 schema (inline cid, no storeRef). The script handles it gracefully by reading cid directly.
ORPHANED_DEPLOYMENTA deployment names a project the script never saw, so it is left out. This is not a deleted project: deleted projects are under deletedProjects. detail names both.
ENCRYPTION_KEY_MISSINGThe wallet has a security record, but none carries an encryptionKey, so encryptionKey is null. The bundle command then warns and cannot check the key against the file.
STORE_FETCH_FAILEDThe API node didn't respond when the script tried to resolve a storeRef. Try re-running.
DECRYPT_FAILEDA project or deployment record couldn't be decrypted, so it is left out. detail names it, and stats.decryptionFailures counts them.
MALFORMED_RECORDA message, project entry or deployment is missing a field the script needs or has the wrong shape, so that one entry is skipped. detail names it.
UNTRUSTED_MESSAGEA message was dropped: it has no inline content, its content hash or signature does not check, or its sender is not one your wallet authorized. detail gives the reason.
ALEPH_MESSAGES_TRUNCATEDThe node reported more messages than it served, so the read is incomplete. Re-run, or use another node with --aleph-api <url>.

Five codes make complete false and the exit code 3: DECRYPT_FAILED, MALFORMED_RECORD, UNTRUSTED_MESSAGE, ALEPH_MESSAGES_TRUNCATED and STORE_FETCH_FAILED. The other seven (STORE_FORGOTTEN, STORE_REMOVED, STORE_ABSENT, STORE_CONTENT_MATCH, LEGACY_SCHEMA, ORPHANED_DEPLOYMENT, ENCRYPTION_KEY_MISSING) are reported and leave complete true.

Working with an agent ​

An agent can install the script, run it if you accept the key warning near the top of this page, read the output and run the checks below. An email sign-in that never exported its key has no way to run this once Stasho and Privy are both gone. It cannot produce the key for you, it should never see it in chat, and it never drives an export, in your wallet app or at Privy Home. There is no API for any of this, by design, and nothing here needs an agent key.

What the agent hands you, in these words:

text
First get the script: ask hello@stasho.xyz for a copy if you don't have one. Use only a copy that
answers an email you sent: Stasho never emails it unasked and never asks for your key. Read it
before you install it: package.json has no install scripts and three runtime dependencies (eciesjs,
ethers and tsx, which runs the TypeScript entry point without a build step), and the source talks only to the API node you name. Then export the private key of the wallet you sign in to Stasho with, from your wallet app. In your own
terminal, in the script's folder, run the command from step 2 of the page with your
wallet address, so the key is not on screen, in your history or in that terminal afterwards. Do
not paste the key to me. Tell me
when ./my-recovery.json exists, and paste the lines that start with [recover].

If you signed in with email, Google, GitHub or Apple, your key is held through Privy, so export
it there first: open https://home.privy.io (the wallet holder's page, not dashboard.privy.io,
which is Privy's developer console and asks you to set up an app) and enter the email on your
Stasho sign-in, accept Privy's terms if asked, connect your Google, GitHub or Apple account if
your wallet doesn't show, open the Ethereum wallet, choose Export private key, confirm, and copy
the 64-character key it reveals. I don't touch that page and I never need to see the key. If
your wallet doesn't show, or Privy is unavailable, tell me: this script can't run for your wallet.
An email sign-in that never exported its key has no way to run this once Stasho and Privy are both
gone.

How the agent verifies it afterwards. The output lines and the public audit endpoint, which needs no credential. stats.projectsRecovered and stats.deploymentsRecovered should match the backend's counts, while the backend is still up:

bash
jq .stats ./my-recovery.json
curl -s https://api.stasho.xyz/api/audit/$ADDRESS | jq .backendView

Independence ​

The script depends only on:

  • The network's public HTTP API (any node)
  • Standard cryptographic primitives (AES-256-GCM, ECIES-secp256k1)
  • Node.js stdlib + three npm packages (eciesjs, ethers, and tsx, which runs the TypeScript entry point without a build step)

Once you hold the script, no part of this recovery flow depends on our infrastructure remaining online. During the alpha you get the script from us, or you build your own from the storage schema. What holds, and what does not:

  • Records and encrypted fields stay readable while the network keeps them. With your private key and access to any API node, you can read them.
  • Site bytes stay while the wallet's credits pay for them and are dropped when the credits run out (STORE_REMOVED). Download what you need while the wallet is funded.
  • An App VM's disk (/data) and a running VM are not on the network and are not recoverable. Only the deployed bundle is.
  • Terms, trial and case records are sealed to Stasho's keys and are not part of your records.
  • Solana wallets: Solana records can be verified, and nothing is recovered from them.
  • Email, Google, GitHub or Apple sign-in: the part that depends on someone else is getting the key out of Privy, which is why it is worth doing while you can. An email sign-in that never exported its key has no way to run this once Stasho and Privy are both gone.