Bread Wallet by Miden — Privacy Policy
Last updated: 2026-10-19
Bread Wallet by Miden (“the App”) is a non-custodial cryptocurrency wallet for the Miden blockchain, published by Miden.
Who we are
The App is published by Miden Technologies (US) Corp., a Delaware corporation (file number 10598340), 850 New Burton Road, Suite 201, Dover, Delaware 19904-5786, United States (“Miden”, “we”). Miden is the controller of the personal data described in this policy. For anything about your data, write to privacy@miden.team or call +1 707 813 2275.
The Miden network itself is operated by the Miden Foundation, a foundation organised under the laws of the Republic of Panama (registration number 25068980), Via Ricardo J. Alfaro, Edificio PH The Century Tower, Oficina 317, Suite 742, Corregimiento Betania, Distrito Panamá, Provincia Panamá, Republic of Panama (the “Foundation”). The Foundation is a separate party from us: it operates the network’s sequencer, one of its validators and the USDCx contract, itself or through its contractor Gateway.fm (GATEWAY.FM AS). The Miden swap solver is operated by 0D Labs Limited, a Seychelles company (LEI 254900SN1Y7P2AYYLK22), also a separate party from us. Where this policy describes what the network, a validator or the solver does with your data, it describes those parties’ processing for transparency; we do not control it.
This policy covers two different kinds of data. The first is telemetry: crash reports and optional usage data the App sends to us. The sections from “Data we collect” to “What scrubbing can and cannot promise” describe it, and the short version is that it carries no identifier for you. The second is data that leaves the App to make it work: what the Miden network, your Guardian Operator, and the providers behind swaps, bridges, Earn and fiat on-ramp and off-ramp receive. The section “Data that leaves the App for reasons other than telemetry” describes that. The statements about telemetry are not statements about the second kind.
Data we collect
The App sends crash reports so we can fix what breaks. They are described under “Crash reports” below: the error and where it happened in the code, scrubbed of sensitive data before sending, with no identifier for you or your device. Everything else is collected only if you turn on Share usage data. If you never turn it on, the App sends no product events.
You are asked once, after you create or import a wallet, and you can say no. Whatever you choose then, you can change it at any time in Settings → General → Help improve Wallet. Turning it off stops sending usage data: the setting is checked before every single send, so nothing that was waiting gets sent later. Crash reports are sent whether this setting is on or off.
On Firefox, Firefox’s own switch also applies. Firefox asks whether an extension may collect technical and interaction data, and where it has said no, the App sends neither crash reports nor usage data. Other browsers and the mobile and desktop apps have no equivalent switch.
Usage data, below, is sent only while that setting is on. Crash reports are sent either way.
Usage data
When the setting is on, the App reports the start and the end of a small set of activities. Each report is a single message with no free-form text in it — there is no field an address, an amount, or an error message could occupy. A message contains only these:
| What | Values |
|---|---|
| Which activity | One of: opening the app, unlocking, creating a wallet, importing a wallet, recovering a wallet, returning to the app, funding, sharing your receive address, sending, swapping, earning, connecting a dApp, approving a dApp request, changing your guardian, handling an incoming note, viewing activity |
| Which kind of work, for work the App did on your behalf | One of: sending, receiving, swapping, earning, bridging, a guardian change, a dApp’s transaction, another transaction, proving, or one of three services being unreachable. A category of work, never a recipient, an amount, an asset, a dApp, or an endpoint. See “Work the App does on your behalf” below |
| Whether it started or ended | started or ended, or settled for work the App did on your behalf |
| How it ended | completed, cancelled, or errored |
| Broad error category, if it failed | One of: network, rpc, proving, validation, storage, auth, timeout, unknown |
| How long it took | A duration in milliseconds |
| How far you got | For an activity, the name of the furthest screen it reached, from a fixed list — for example select_amount or review. The screen’s name only, never anything you typed on it. For work the App did on your behalf, the stage it reached instead — for example proving or submitting |
| App version | For example, 1.15.21 |
| Platform | extension, ios, or android |
| Network | mainnet, testnet, devnet, or localnet |
| A short random number for that one activity | Explained under “Identifiers” below |
| A short random number for that run of the App | Explained under “Identifiers” below |
That is the complete list. There is no other field, and the error category is chosen from the eight names above — the underlying error message is read to pick a category and is never sent.
Work the App does on your behalf
The App also reports whether the work it does on your behalf actually succeeded. When you send, swap, claim, earn, bridge, or change your guardian, the App has to prove and submit a transaction after you have finished asking for it, and that part can fail for reasons that have nothing to do with you — a prover or a node being down or unreachable. Without this, a failure affecting everyone would be invisible to us, because by then the activity above has already been reported as finished.
Those reports use the same fields as the table above and add nothing that describes you or your money. Nothing about the transaction itself travels with them: not who you sent to, not how much, not which asset, not which dApp, not which endpoint.
They also carry no activity identifier, which means we cannot tell which of your activities a piece of work belonged to. That is deliberate rather than an oversight: linking the two would mean writing one of those random values onto the transaction record the App keeps on disk, and a value written to disk outlives the run it was made for — which is exactly the durable identifier this design refuses to hold.
Three more things travel with each message as part of the technical envelope our analytics provider defines: the time the message was sent, a flag saying whether the App was a development build or a released one, and a fixed name and version number for the piece of our own code that sent it (miden-wallet-aptabase@1.0.0 — the same string for everyone). None of them varies with who you are or what device you use.
We deliberately leave three of the provider’s fields empty. Aptabase’s format has slots for your operating system version, your language or locale, and your device model, and their own libraries fill all three in. We send none of them. Each one is a fingerprinting signal — a combination of them narrows a crowd down to a person — and none is needed to find out where the wallet breaks. Automated tests fail the build if any of the three ever appears in an outgoing message, which is what stops someone filling them in later because the field was there.
Crash reports
- Only
raw_bridge_balance_failedonfieldcarriesbridgeBalanceBranch(fast_usdc,circle_usdc,native,registered_erc20,unrecorded) andbridgeBalanceStage(configuration,input_validation,request_build,request,response_decode,rpc_rejected,value_decode,unrecorded). These describe the owned balance read, without addresses, chain identifiers, token contracts, balances, amounts, URLs or error text. Unowned display fallbacks carry no producer evidence. - UI feature diagnostics are scoped to
bridgeFeatureUnavailableBodyonnotice:featureUnavailableFeature(earnDeposit,fastBridgeOut,fastBridgeIn,bridgeIn,bridgeOut,usdcxDeposit,usdcxWithdrawal,unknown) andfeatureUnavailableReason(off,not-configured,not-deployed,service-down,unknown). Composite features stay unknown; pending and restricted decisions remain unreported. - Only
submissionUnansweredonstatus_screencarriessubmissionFeedbackBranch(prover_at_submit,guardian_unanswered,grpc_unknown,no_answer), naming the existing selected feedback branch rather than its cause. pendingRetryReason(row_missing,not_retryable,guardian_hold,landing_pending,verdict_busy,unsafe_retry,unexpected) is carried byraw_pending_activity_retry_failedonnotice,raw_history_retry_failedoninlineornotice, andraw_transaction_retry_failedoninline. The latter two also carryretryNoticeOrigin(history_primary,history_earn_receive,history_payback,status_screen), restricted to the corresponding display. A refusal does not disclose transaction or account data.- Only
invalidMidenAccountIdoninlinecarriesrecipientValidationStage(prefix_rejected,decoder_throw,validator_exception). A caught decoder exception does not establish malformed input. No address, network prefix, input or error text is sent. - Only
earnPositionsLoadErroronnoticecarriesearnPositionsLoadStage(configuration,owner_lookup,request,response_decode,position_decode,unrecorded) andearnPositionsLoadOutcome(complete_failure,partial_failure). Partial means a current owner query succeeded, not that old positions were retained. No owner, amount, response or error text is sent. - Only
usdcxBurnFeeUnavailableonfieldcarriesburnFeeFailure(configuration_failed,native_asset_failed,request_build_failed,dry_run_failed,result_decode_failed,probe_mismatch,busy_exhausted,request_failed,estimate_failed). It identifies the fixed measurement or retry branch. No error text, account, amount, balance, recipient or network address is included. - Only
mainnetAccessCheckFailedoninlinecarriesmainnetAccessCheckFailure(endpoint_resolve,sdk_ready,rpc_client,invitation_check,unrecorded). It names the local access-check stage that failed, never the access code, account or node address. - Only
mainnetAccessCodeRejectedoninlinecarriesmainnetAccessCodeRejectionOrigin(eligibility_check,registration_redemption). It distinguishes a refusal during the initial eligibility check from a refusal when account setup redeems the accepted code; no code or request identifier is sent. - Only inbound bridge failures at
wallet_approve,wallet_depositorepoch_solvecarry wallet request client (native_reown,wagmi), naming the selected request implementation. No provider text, address, amount or URL is sent. - Only
noteClaimFailedRetryonnoticecarriesnoteClaimFailureOrigin(queue_single,queue_batch,transaction_row). It distinguishes enqueue failures from a specific watched transaction outcome, without a note or transaction identifier. - These access-check and note-claim notices may carry
uiFailureKind(network,rpc,proving,validation,storage,access_denied,timeout,unknown,unavailable). The category is calculated locally from the attributable error. Raw text and values are never sent; missing attributable evidence says unavailable. - The transaction execution envelope uses
executionObservation(not_reached,observed,read_failed),executionArguments(present,absent,unavailable),executionLayout(absent,fee,guardian,other,missing_preimage,unavailable) andaccountCodeFamily(current_guardian,unrecognized,unavailable). These fixed categories describe local execution checks; they contain no authentication material, code, argument values or identifiers. - Only
encryptedWalletFileWrongNetworkonfieldcarriesbackupNetworkFailureStage(declared_network,genesis_header). These identify the existing declared-network and genesis-header guards. No file contents, network, genesis, password, account, identifier or error text is sent.
Handled transaction failures are also reported, including failures after submission and transactions whose network outcome remains unknown. These reports contain only fixed transaction, stage and execution-route categories, bundled software versions, the network the App was on, known public protocol error identities, the fixed failure reasons, refusal codes and claim origins listed below, and authentication-code compatibility, preparation, layout and commitment-match categories calculated locally. They never include the transaction request, an account or transaction identifier, addresses, amounts, signatures, salts, commitments or dApp URLs. Unknown assertion numbers and raw failure text are not sent. Historical rows imported from a backup do not generate new failure reports.
A failure report can also say why the transaction failed, using only these fixed values:
- The App’s own reason, when the App itself ended the transaction:
user_cancelled(you cancelled it),interrupted_on_startup(the App closed while it was running),stuck_reaped(it ran too long and the App stopped waiting for it),queued_expired(it waited in the queue too long),rotation_key_pending(the account’s everyday key was not active yet, so the App did not claim the incoming transfer),rotation_funding_note_unavailable(the transfer the App was claiming to activate the account’s everyday key was no longer available to claim),rotation_funding_not_native(the App could not confirm that the transfer it was claiming to activate the account’s everyday key was a standard payment holding only the network’s own token),rotation_gate_refusal(the App refused to claim a transfer while the account’s everyday key was not active yet, for a reason other than the three before it),consume_not_landed(the network still showed the incoming transfer as unclaimed),consume_landed_external(the network showed the incoming transfer as claimed, but not by a transaction the App could prove was its own),note_invalid(the incoming transfer can never be claimed), ornever_committed(the network showed that a transaction the App submitted was never included, so nothing moved). Any other value is sent asother. Withnever_committed, the report can also say which check showed it:expired(its time to be included ran out),superseded(the account moved on with another transaction),spent_elsewhere(a transfer it claimed was claimed by another transaction), ormixed(the App had tried more than once, and different checks showed it for different tries). Any other check is sent asother. - The age of a queued row that expired:
30m_to_1h,1h_to_6h,6h_to_24horover_24h;unavailablewhen its start time is missing or invalid. The exact start timestamp and age are never sent. - The queue sighting of a queued row that expired:
never(no run of the App recorded looking at this wait),not_reached(a pass ran while it was queued and stopped at an earlier row),in_progress_gate(a pass found another transaction already running),row_ineligible(a pass reached it and it was not ready to run yet) orselected(a pass chose it). The strongest is kept for that wait, including after the App restarts. Retry starts a new wait. No identifier, timestamp, queue position or cause text is sent. - The preflight sync of a queued row that expired:
not_entered(this wait has no record that the network-state update before the transaction started),entered(that update started and this wait has no record of it finishing),returned(it finished) orthrew(it failed). Only this fixed label is sent, never the error text or a time. - Hidden time while a queued row waited:
not_measuredon desktop and extension, where background time is not discounted; on mobile,unobservedwhen this run is not recording background time,nonewhen it recorded no background time overlapping the wait (a run that started later can saynonefor time it did not see),under_halfwhen that recorded time is more than none and less than half the wait, orhalf_or_morewhen it is half the wait or more.unavailablewhen the wait’s start is missing or invalid. The exact duration is never sent. - The attempt’s submit crossing, for the matching current attempt:
crossedwhen its fixed submit-stage record or evidence shows the App reached the submit call,not_crossedwhen that attempt was explicitly marked as ending before the call, andunavailablewhen its evidence cannot decide or no matching attempt is recorded. This says only that the App reached its submit call, not that the network accepted it. The attempt identifier and recorded evidence are never sent. - Proof-wait availability (
current_attempt_missing,current_entry_missing,execution_time_invalid,submit_time_invalid,time_order_invalid,reference_block_missing,expiration_block_missing,reference_block_invalid,expiration_block_invalid,expiry_order_invalid,derived_wait_invalid,derived_window_invalid,available):proofWaitAvailabilitydescribes only the existing calculation for the selected current attempt after an expired never-committed node verdict. It does not explain expiry or describe every historical attempt. Only this first failed prerequisite or availability is sent, never identifiers, timestamps, block values, durations or row data. - Startup eligibility stage (
endpoint_overrides,config_initialization,eligibility_lookup,config_refresh,eligibility_retry), startup eligibility reason (override_load_failed,config_unavailable,config_initialization_failed,endpoint_missing,endpoint_invalid,request_failed,response_invalid,network_mismatch,version_mismatch,unexpected) and startup eligibility retry (not_attempted,config_not_advanced,refresh_failed,retry_failed): only beside the unavailable startup message, these name the App’s failed check and whether its existing policy-document retry ran. They carry no location, document version, address, identifier, URL, response body or error text. - the node’s consume verdict (one of
landed-local,landed-external,invalid,processing,not-landedorunknown): after the wallet reads an input note following a killed consume, this names the closed result.landed-localmeans this wallet’s consume landed;landed-externalmeans the note was consumed by another transaction;invalidmeans it cannot be consumed;processingmeans a consume is underway;not-landedmeans the note remains unconsumed; andunknownmeans the node did not establish a state. The result carries no note, account or transaction identifier. - The prior failure detail on an expired
never_committedverdict: the report may include the earlier failure’s fixed identity and the proof-wait bucket described below for an expired submit refusal. When the earlier unanswered submit is bound to the sole recorded attempt, the report may also carry whether the mobile app was hidden during that submit (yes,noorunavailable) and its duration bucket (under_9s,9_to_11s,over_11sorunavailable). The raw prior error, exact duration and block counts are never sent. - A guardian refusal:
guardian_http_followed by the HTTP status the guardian answered with, as its number, such as409, when it is a whole number from 100 to 599, and asotherwhen it is not. It also carries the guardian’s error code when it is one the guardian’s published client defines:account_already_exists,account_data_unavailable,account_not_found,account_paused,account_released,authentication_failed,authentication_replay,authorization_failed,candidate_landed,commitment_mismatch,configuration_error,conflict_pending_delta,conflict_pending_proposal,data_unavailable,delta_not_found,insufficient_operator_permission,insufficient_signatures,invalid_account_id,invalid_commitment,invalid_cursor,invalid_delta,invalid_evm_proposal,invalid_input,invalid_limit,invalid_network_config,invalid_proposal_signature,invalid_status_filter,invalid_timestamp,network_error,pending_proposals_limit,proposal_already_signed,proposal_not_found,rate_limit_exceeded,rpc_unavailable,rpc_validation_failed,session_expired,session_revoked,signature_scheme_not_allowed,signer_not_authorized,signing_error,state_not_found,storage_error,unsupported_evm_chain,unsupported_for_networkorwallet_signature_required. Any other code is sent asother, and a refusal with no code asnone. - A device key refusal:
hot_key, with the code the App’s secure-hardware signing key was refused with, when it is one ofAUTH_FAILED,AUTH_UNAVAILABLE,BAD_CIPHERTEXT,BIOMETRIC_BUSY,DEVICE_LOCKED,HARDWARE_UNAVAILABLE,INVALID_INPUT,KEY_INVALIDATED,KEY_NOT_FOUND,LEGACY_CIPHER_INIT_FAILED,UNWRAP_FAILEDorUSER_CANCELLED. Any other code is sent asother, and a refusal with no code asnone. - A submit with no definite answer:
submit_outcome_unknown, when the network neither accepted nor refused a transaction the App submitted. It identifies the request endpoint, one ofsubmit_proven_transaction,submit_proven_batch,get_transaction_encryption_keyorotherwhen a request’s endpoint is outside that list. A submit that names no gRPC request reports the endpoint and the answer asunavailable. It carries the kind of answer the App got instead, one ofunavailable,internal,cancelled,aborted,data_loss,deadline_exceededorunknown(any other is sent asother), and whether the App was in the background while it waited:yes,no, orunavailablewhen the App cannot tell; and how long the submit took:under_9s(less than 9 seconds),9_to_11s(9 to 11 seconds, both included),over_11s(more than 11 seconds), orunavailablewhen the App has no start to measure from or cannot measure a valid duration. Only closed values and the duration bucket are sent, never the raw error or the duration in milliseconds. - A submit the network refused:
submit_rejected, when the network gave a definite refusal while the App submitted a transaction. It names the request that was refused,submit_proven_transaction,submit_proven_batch,get_transaction_encryption_key(the key the App seals a transaction with before it submits) orother; the kind of answer, one ofunavailable,internal,cancelled,aborted,data_loss,deadline_exceeded,unknown,not_found,invalid_argument,permission_denied,already_exists,resource_exhausted,failed_precondition,out_of_range,unimplementedorunauthenticated(any other is sent asother); and the network’s own reason when its fixed wording names one,internal,expired,state_conflict,capacity_exceeded,missing_fee,consumes_inflight_fee_notes,invalid_fee_asset,batch_id_mismatch,authentication_failedorunknown_code, elsenonewhen it gave no reason andotherfor any other; and for an expired proof, how long the transaction waited between being run and being submitted against the number of blocks its expiration allowed:under_half_window(less than half of them),over_half_window(half of them or more, up to all of them),past_window(more than all of them), orunavailablewhen the App has no record of either. The wait is converted to blocks at an estimated block time, and only the bucket is sent, never a duration or a block count. The network’s own message is never sent. - Work the App stopped: when the App stopped a transaction’s work from outside because it ran too long or what was running it went away. On the extension,
offscreen_aborted_deadline(the background document running it passed its time limit),offscreen_aborted_deadline_no_kill(one read passed its time limit while another transaction was running),offscreen_aborted_doc_closed(that document closed before it answered),offscreen_aborted_transport(the message to that document could not be delivered) oroffscreen_aborted_other. Each of these also says whether the App had been told the work started running (yes,no, orunavailablewhen the App cannot tell);nodoes not prove it never started, since that notice can be lost. The two time-limit values also say whose time limit it was (own, this work’s own, orcollateral, another piece of work whose time limit closed the document under it), which time limit it was (dispatch, counted from when the work was sent;backstop, the upper bound for a transaction that never reported starting;started, counted from when it started running; orresumed, counted again after a signature), and whether the work whose time limit it was is a transaction (write) or a read (read), each sent asunavailablewhen the App cannot tell. Only these fixed values are sent, never an identifier or a duration. On any platform,wasm_lock_evicted_watchdog(an operation held the transaction engine past its time limit),wasm_lock_evicted_realm_error(the transaction engine crashed while in use) orwasm_lock_evicted_other. - What the transaction software said went wrong, when it could not build or run a transaction:
transaction_execution_failedwith thecauseits own fixed wording names, one ofrequest_note_consumed(a transfer being claimed was already claimed),request_note_processing(another transaction was already claiming it),request_note_unauthenticated(a transfer could not be proven to exist on the network),request_duplicate_note(the same transfer was included twice),request_empty(the transaction would change nothing),request_fee_conversion(the network fee could not be arranged for the account),request_other(any other problem with the request),recency_check(the App’s copy of the network was too old),account_locked(the account’s state may be out of date),account_data_not_found(the account’s data was missing on the device),output_mismatch(the transaction did not produce the transfers it was asked to),data_store,storeorrpc(reading the device’s records or the network failed),fetch_inputs,partial_blockchain_build(the transaction software reported failure building its partial chain representation),block_after_reference,account_not_in_store,block_not_in_storeorfetch_asset_witness(gathering what the transaction needed failed),foreign_account_not_anchoredornote_past_reference_block(something the transaction used was newer than the network state it was built on),kernel_program(the transaction’s program failed),summary_block_unknown,summary_expiration_mismatchorsummary_commitment_mismatch(what the signers approve did not match the transaction),signature_failed(a signature could not be made),executor_other(any other failure while running it) orother. A check in the transaction’s program that fails is named by the public name the transaction software the App ships gives it, read from the sources of the protocol, its standard components, its core library and its AggLayer bridge: the name of the error the check raises or, for a check whose message is written where it is made, the component and the words of that message. A check that fails with no message is sent askernel_assertion_without_message, or askernel_u32_assertion_without_messagewhen it is a range check, and one whose number none of those sources publishes askernel_assertion_outside_shipped_sources, or askernel_u32_assertion_outside_shipped_sourceswhen it is a range check. - The signing step, when the transaction failed while it was being signed:
creating_proposal(preparing it for the guardian),signing_proposal(signing what was prepared) orsigning_locally(signing on this device alone). - The preparation step, when the transaction failed while it was being prepared for the guardian or handed on to be sent: the preparation step that failed (
service_select,service_account_read,service_signer_read,service_load,chain_sync,reclaim_build,reclaim_persist,purchase_build,purchase_persist,swap_request_build,swap_request_persist,rebase_build,proposal_create,rebased_request_persist,signing_stage_write,sign_lock,proposal_sign,custom_advice,custom_request_build,proposal_request,submit_serialize,submit_pinorclient_build), in the order the App takes them: finding the account, reading it and its signing keys on this device, loading it from the guardian, bringing the network state up to date, building and saving the return of a swap order, building and saving a Game Coins purchase, building and saving a new swap order, rebuilding the request at the current network height, asking the guardian to prepare the transaction, saving the rebuilt request it was prepared from, recording that it did, entering the signing lock, signing the proposal, preparing custom advice, building the custom request, building the standard request, packing the signed transaction to be sent, recording that it may be sent, and starting the transaction software that sends it. When one of the App’s own checks stopped it first, the report also says which check refused it (account_not_guardian, the account is not a guardian account,hot_key_missing, its everyday key is missing, orguardian_out_of_sync, the account’s guardian is being identified again and cannot be used yet). When the failed step is the guardian being asked to prepare the change, the report also says which security change failed (switch-guardian,replace-hot-keyorupdate-procedure-threshold): a move to another guardian, a replacement of this device’s everyday key, or a change to how many signatures a procedure requires. - The actual execution request: the execution route (
ordinary_new_transaction, the ordinary request execution call, orguardian_signed_request, the guardian-signed pipeline); the authentication observation (not_reached, the request read was not reached,observed, its fields were read, orread_failed, a local read failed); and auth argument presence (present,absentorunavailable). These name the selected code path and local observation only. They do not identify an account type, the request’s author, whether the transaction was submitted, or whether the SDK later built default authentication. Layout, commitment match and account code use the categories below. Account code stays unavailable when no account object is already available. Existing observed preparation provenance remains separate. No extra account or network read is made for these facts. - The signing key, when the guardian refused to accept this device’s signature on a request: whether the key this device signed with is one of the account’s signers on chain (
yes,noorunknown), which of the account’s keys signed it (hot, its everyday key, orcold, its recovery key) and whether the account was still waiting for its everyday key to be replaced (yesorno). It never carries a key, a signature or the account’s identifier. - The confirming step, when the transaction failed after it was submitted:
confirming(waiting for the network to include it),delivering(handing a private transfer over for its recipient),registering_guardian(registering the account with the guardian it moved to),guardian_syncing(bringing the account’s guardian up to date, or finishing the transaction after that update failed) orguardian_synced(finishing the transaction after the guardian was brought up to date). - The finishing step, when a claim of an incoming transfer failed after it was sent, while the App was recording its result: the finishing step that failed (
result_read,row_read,result_serializeorstatus_write), in the order the App takes them: reading what the transaction did, reading its own record on this device, packing the result to keep, and saving it. When one of the App’s own checks stopped it first, the report also says which finishing check refused it (no_input_notes, the result names no incoming transfer, orno_fungible_assets, the transfer holds no token amount). Only the fixed step is sent, never the result, an identifier or an amount. - The last deferral of a failed claim, and of any other failed transaction the App had put back in its queue:
wallet_locked(the App was locked),preflight_sync_failed(the App could not bring its network state up to date before claiming),guardian_backpressure(the guardian needed the App to wait before sending more work),delegated_prover_failed(the proving service could not finish),guardian_rate_limited(the guardian asked the App to slow down),guardian_unreachable(the guardian could not be reached), orguardian_unauthorized(the guardian did not authorize the request). For a claim,nonemeans no deferral was recorded, including claims queued before the App recorded this; any other transaction sends nothing when none was recorded. Any unlisted value is sent asother. Only the fixed reason is sent, never an identifier, amount, time or the failure’s text. - The claim fee check’s admission of a failed claim:
native_id_unknown(the network’s own token could not be identified),fee_unknown(the fee could not be determined),balance_unknown(the account’s balance could not be read),ahead_assets_unknown(the assets of claims ahead in the queue could not be determined),ahead_native(a claim ahead in the queue held the network’s own token),note_assets_unknown(the assets or native-token amount of this incoming transfer could not be determined), orknown_sufficient(the known funds covered the fee).unrecordedmeans no admission reason was recorded, including claims queued before the App recorded this; any unlisted value is sent asother. Only the fixed reason is sent, never an identifier, fee, balance, amount or time. - The stuck-transaction check, on a transaction it cancelled:
missing_start(the transaction had no recorded start) orpast_limit(it passed the App’s waiting limit); andstarted_here(this app session started an attempt for it) ornot_started_here(this app session had not started an attempt for it). Either value is sent asunavailablewhen it was not recorded or is outside its list. These are fixed categories, never an identifier, timestamp or elapsed time, and they carry no amount or other transaction value. An attempt started here is not proof it is still running. - The startup check, on a transaction it stopped because the App last ran it before this start: which rule picked it,
missing_start(it had no recorded start),before_session(it started before this start of the App) orfuture_stamp(its recorded start was further ahead than the device clock can explain); how long before this start it entered the step it was in,under_1m,under_10m,under_1h,under_1dorover_1d; andsubmit_recorded(it had recorded that it may have been sent) orno_submit_recorded(it had not). The stage age is sent asunavailablewhen the step’s start was not recorded or lies after this start, and any value is sent asunavailablewhen the report carries none or it is outside its list. These are fixed categories, never an identifier, timestamp or exact time, and they carry no amount or other transaction value. - Two failures the transaction engine names itself:
advice_stack_read_failed(the transaction’s program asked for data it was not given) andclient_terminated(the transaction engine was shut down while the transaction was using it). - A broad category, when nothing above names the failure: one of
network,rpc,proving,validation,storage,auth,timeoutorunknown, chosen on the device by reading the failure’s text, which is never sent. Withunknown, the report also says what kind of value was thrown:wasm_runtime_error(the transaction engine’s WebAssembly code stopped with a runtime error),type_error(a value was used in a way its type does not allow),range_error(a value was outside its allowed range),reference_error(the code referred to something not defined),syntax_error(the code could not be parsed),error(any other error),string(text was thrown instead of an error),non_error(another kind of value was thrown), ornone(no value was supplied). When the transaction was being submitted, it also carries the same background answer and submit-duration bucket listed under “A submit with no definite answer” above. Only these fixed values are sent, never the thrown value, its name or text, or the duration in milliseconds.
None of these carries the text of the refusal, the guardian’s response, or the address of the server that sent it.
A failed claim of an incoming transfer also says what queued the claim, using only these fixed values: auto_consume (the App claimed the transfer on its own), claim (you accepted one transfer), claim_all (you accepted several transfers at once), dapp (a connected site asked to claim it), bridge_reclaim (you took back the funds of a failed bridge transfer), swap_settlement (the App settled one of your swaps), rotation_funding (the App claimed it to activate the account’s everyday key), or arcade_gc_purchase (you bought Game Coins in Arcade). A claim queued before the App recorded this is sent as unrecorded; any unlisted value is sent as other. It never names the transfer, the site or the account.
A failed inbound bridge, a deposit from another network into Miden, also carries these fixed values:
- An inbound bridge that failed: the route the deposit took,
epoch(Fast),agglayerorusdcx, andotherfor any other; the step at which it failed, one ofprovider_unavailable(no wallet connection to sign with, or an amount that is not valid),precheck_route(the route was not set up to take the deposit),precheck_balance(the balance did not cover the deposit, or could not be read),precheck_session_chain(the connected wallet had not approved the network, or did not switch to it),precheck_allowance(the amount the bridge may spend could not be read),precheck_domain(Miden’s registration with the bridge could not be confirmed),precheck_quote(no fee quote for the transfer),wallet_approve(the wallet’s approval of the spend),approve_receipt(that approval on its network),wallet_deposit(the wallet’s deposit),deposit_receipt(that deposit on its network),epoch_solve(the Fast deposit),epoch_execute(the rest of the Fast deposit’s preparation),reconciler_receipt(a later check of the deposit on its network),submission_interrupted(the App closed before the deposit was recorded),epoch_intent_failed(the Fast route reported the transfer failed),usdcx_mismatch_domain,usdcx_mismatch_token,usdcx_mismatch_recipientorusdcx_mismatch_amount(Circle attested the deposit to another network, token, account or amount), ordelivery_timed_out(the transfer did not arrive in time), andotherwhen the App names none; and what the other network’s wallet or its software answered, read from its fixed wording or error type:user_rejected(you or your wallet declined),session_deleted(the wallet connection was closed),no_session(no wallet was connected),request_expired(the wallet did not answer in time),insufficient_funds(the account could not pay for the transaction),reverted(the network refused the transaction),receipt_timeout(the wait for a transaction’s receipt ran out of time),timeout(a request to the network ran out of time),chain_mismatch(the wallet was on another network than the request),chain_unconfigured(the request named no network, or one that is not set up),provider_unauthorized(the wallet did not authorize the request),method_unsupported(the wallet or the network does not support the request),provider_disconnected(the wallet was disconnected),chain_disconnected(the wallet was disconnected from the network),chain_switch_failed(the wallet could not switch networks),request_invalid(the network refused the request as malformed),rpc_internal(the network reported an internal error),rpc_unavailable(the network could not find or serve what was asked),transaction_rejected(the network rejected the transaction),rpc_limited(the network refused the request over a limit),http_followed by the HTTP status a server answered with, as its number, when it is a whole number from 100 to 599, andhttp_otherwhen it is not,invalid_permissions(the exact wallet refusal Invalid permissions for call.),unknown_rpc_error(the wallet answered with an error it gave no code for),otherfor any other answer, andnonewhen there was no answer at all; native origin (argument_validation,unconfigured,request_construction_error,request_call_error,publish_error,wallet_error,request_expired,session_deleted,other); wallet response code (user_rejected,invalid_params,internal_error,provider_unauthorized,method_unsupported,provider_disconnected,chain_disconnected,chain_switch_failed,request_invalid,rpc_unavailable,transaction_rejected,rpc_limited,other); request age (under_30s,30_to_120s,120_to_300s,over_300s,unknown); publication observation (confirmed,unobserved); background observation (yes,no); balance check, for a deposit the balance check stopped: whether the balance was too low (too_low) or could not be read (read_failed); orphan check, for a deposit the App closed before it was recorded: whether the App knew no deposit screen still owned it from the lock every App window shares (origin_lock) or from its own window’s count (realm_idle); USDCx source route, for that deposit: the route it recorded,xreserveorcctp_executor, andunrecordedwhen it recorded neither. When a native session precheck refuses the bridge network and the failure can hold that record, the report also names that failed condition,state_unread(the session state could not be read),session_missing(there was no connected session), orchain_unapproved(the connected session had not approved the network), andotherwhen the condition is outside this list; and what the add-and-switch attempt did,not_attempted(no request was sent),unsupported(the network could not be proposed to the wallet),rejected(the request was refused and the session state was read again),rejected_unread(the request was refused and the session state could not be read again),unconfirmed(the requests returned and the session still had not approved the network), orunconfirmed_unread(the requests returned and the session state could not be read), andotherwhen the attempt is outside this list. The answer’s own words, the wallet’s message, addresses, amounts and server addresses are never sent.
The native observations are sent only for an instrumented Android or iOS Reown request failure on an inbound bridge. On iOS, only a failure attached directly to its request can report an origin: unconfigured, argument_validation, request_construction_error or request_call_error. These mean the local configuration or arguments were unavailable, the request could not be constructed, or the awaited request call threw; they do not prove what the other wallet saw. iOS reports unknown request age and unobserved publication, with no wallet response or background observation. When the connected wallet refuses to add or switch to the bridge’s network, the failure that follows carries the observations of that refused request. Native origin names the rejection site: argument validation, missing configuration, publishing, a wallet error response, SDK request expiry, session deletion or another site. Wallet response code is present only for a wallet error response. It identifies a user rejection, invalid parameters, internal error, lack of authorization, unsupported method, disconnected provider or chain, failed chain switch, invalid request, unavailable or rate-limited RPC, or transaction rejection. An absent or unlisted code is reported as other. Request age and publication observation are required for a native group: the age is a bucket of monotonic elapsed time, with unknown when no start was recorded, and publication is confirmed only if that request’s publication callback ran; unobserved does not prove that publication failed. Background observation says whether Bread went to the background while the request was pending; it is omitted when observation was unavailable or continuity broke. Pre-start failures have unknown age, unobserved publication and no background answer. Older builds, other platforms and errors without a valid native group omit the observations. Publication and backgrounding prove neither that a wallet prompt appeared nor that the user abandoned it. These five observations are searchable tags and extra diagnostic fields and preserve grouping; only the exact permissions EVM class intentionally starts a different report title. Numeric response codes, raw age, timestamps, identifiers, payloads, message text and endpoints are not sent.
When a native session precheck refuses the bridge network and the failure can hold that record, the report names the failed condition and the add-and-switch attempt with only the fixed values in that list. A plain value is rethrown unchanged, and the report names neither the condition nor the attempt. When the report names them, those words are part of the report title, so each pair starts its own crash-report issue. A chain id, chain name, topic, account, address, amount, URL, or the text of the error is not included.
A crash sends the error type, the error’s message, and the stack trace (the list of code locations the error passed through), plus the app version. Where one error was caused by another, each error in that chain is included. Text is scrubbed before it leaves your device: addresses, amounts, long hex values, credentials in URLs, and values under names like password or seed are replaced with [redacted]. A message that scrubs down to nothing useful is dropped rather than sent as a row of markers, and if anything in the report looks like a recovery phrase, the whole report is thrown away rather than sent.
Every crash report also names the network the App was on: mainnet, testnet, devnet, or localnet.
Every crash report also says how the error reached the reporter: reported by the App itself (generic, or react when a screen failed to show), or never caught and picked up by the App’s global error handlers (onerror or onunhandledrejection). It names the platform (extension, ios or android) and the build the App came from: which build it was (extension, mobile, desktop or background, the extension’s background part), the commit of the App’s source code it was built from, which is the same for everyone running that build, and whether that source had changes not in the commit (true or false). A crash in one of three of the App’s own operations names it: its messaging between its windows and its background (intercom_request), a dApp approval window reading its request (confirm_payload), or starting the built-in WebAssembly module (sdk_initialize). When a dApp approval window cannot read its request, the report says whether the window’s address had no approval id at all or an empty one (missing or empty).
An existing crash report can also include a storage owner (wallet_transactions, wallet_limits or wallet_database) and a storage operation (get, query, count, cursor, mutate, transaction or open). The first two owners identify the wallet’s own transaction and spending-limit tables using fixed labels, never a name copied from storage. wallet_database identifies a failure of the wallet database itself, such as opening, upgrading or a transaction involving more than one table, when no single table is known. Storage fields are omitted when no validated annotation is available. They include no database names, keys, primary keys, rows, record fields, raw error text, ids, addresses or amounts.
Every crash report also carries page visibility (visible, hidden or unknown), read once when the report is captured in the window or background context sending it. unknown means that context has no document, its visibility cannot be read, or the value is unrecognized. A visible page does not prove the native app resumed. Storage values are extra diagnostic fields and per-report tags; page visibility is an extra diagnostic field. These values do not change report titles or issue grouping, and they do not trigger new reports.
If the App’s built-in WebAssembly module fails while starting, the report also says which startup step failed (wasm_ready, client_create, mock_import or mock_create), when client creation failed whether it was the primary client or the realm reader (primary or realm_reader), and whether that client ran in a worker (enabled or disabled). When the failure is a WebAssembly trap, it adds whether a fresh one-page block of shared memory could still grow (grew, refused or unavailable) and how many of the App’s own pages were open, counting its hidden background page (1, 2-3, 4+ or unavailable). None of these is an identifier.
A crash in the App’s own messaging between its windows and its background also names the kind of request that failed, from the App’s fixed list of request names, and, in the browser extension, whether the window had reconnected to the background before it failed (true or false). When a dApp approval window’s request finds nothing in the background to answer it, the report also names the state the background held for that approval (open, answered, window_closed, expired, payload_failed or unknown), the shape of the id the request carried (string, array_with_string, array_without_string, missing or other), what the background still remembered of recent approvals (empty, holding or pruned, meaning it had dropped older ones), how long the background had been running (under_10s, 10_to_60s, 60_to_300s, over_300s or unknown) and how the approval window was last loaded (navigate, reload, back_forward, prerender or unknown). None of these includes the approval’s id, the dApp, or anything the request carried.
A crash that is the App’s connection to the WalletConnect relay failing is sent as a warning, at most once for each range of such failures in a window (the first, the second to fifth, and the sixth onward), so at most three times per window. Every such failure is counted, but none is sent while the device is offline: a range is sent with the first failure in it that happens while the device is online. Its message is WalletConnect’s fixed wording with only the relay’s host, none of the errors that caused it is sent, and it also carries these fixed values:
- A WalletConnect relay connection that failed: the label that says so (
walletconnect_relay); whether the device’s network connection had changed in the 30 seconds before (just_changedorsteady); whether the App’s window was on screen (visibleorhidden); which App window it was (popup,fullpage,sidepanelorconfirm); whether the connect-wallet screen was open in that window when the connection failed (connect_modalorno_connect_modal); whether the relay had connected in that window before (trueorfalse); and how many relay failures that window had seen (1,2-5or6+). It never carries your connected wallet, its address, a session or pairing identifier, or the project key.
When the App shows you an error message, it also reports that it did: which message it was (activityHiddenNotesError, agglayerTokenUnsupported, amountExceedsTokenDecimals, amountMustBeLessThanBalance, arcadeGcPurchaseAboveInventory, arcadeGcPurchaseBelowMinimum, biometricFailed, biometricHardwareUnavailable, biometricLockedOut, biometricNotEnrolled, biometricNotEnrolledWork, biometricNotStrong, biometricPasscodeNotSet, biometricSecurityUpdate, biometricStrongNotEnrolled, biometricStrongNotEnrolledWork, biometricUnavailable, biometricUnavailableIOS, bridgeDepositBalanceCapExceeded, bridgeDepositBalanceCapUnpriced, bridgeDepositBalanceReadFailed, bridgeDepositBalanceTooLow, bridgeFeatureUnavailableBody, cameraPermissionDenied, cannotSendToSelf, confirmError, connectivityGuardianBody, connectivityNetworkBody, connectivityNodeBody, contactAlreadySaved, contactIsYourAccount, couldNotCheckUnlockMethod, couldNotDecodeTransaction, couldNotVerifyBySimulation, customGuardianUnreachable, dappLoadFailed, deadletteredNotesBody, deadletteredNotesBodyOne, devEndpointSaveFailed, earnCollateralUnavailable, earnFailedToOpenPosition, earnGaslessWithdrawalFailed, earnNoEvmAddress, earnPositionsLoadError, earnUnavailableOnNetwork, earnVaultLoadError, earnWithdrawAboveAvailable, earnWithdrawNotOwned, earnWithdrawPositive, earnWithdrawPrecision, earnWithdrawQuoteUnavailable, earnWithdrawRefresh, encryptedWalletFileExportFailedAccount, encryptedWalletFileExportFailedDesc, encryptedWalletFileJsonOnly, encryptedWalletFileMalformed, encryptedWalletFileReadFailed, encryptedWalletFileRestoreFailed, encryptedWalletFileSelectOne, encryptedWalletFileTooLarge, encryptedWalletFileUnsupportedVersion, encryptedWalletFileWrongNetwork, evmChainNotApproved, externalPageLoadFailed, failedToParseSigningPayload, faucetOverCap, faucetRateLimited, faucetRateLimitedWait, feeReserveBlocksSend, guardianAccountNotFound, guardianAuthenticationUnavailable, guardianEndpointUnchanged, guardianHistoryFailed, guardianHistoryPartial, guardianSwitchAlreadyInProgress, guardianUrlMismatch, guardianUrlNoOnChainGuardian, guardianUrlStaleRetry, guardianUrlUnreachable, hiddenTokensError, hiddenTokensUnreadable, historyDetailsLoadError, hotKeyHardwareErrorPromptBody, hotKeyRotationFailedGeneric, hotKeyRotationFundingClaimFailed, hotKeyRotationFundingClaimUnconfirmed, hotKeyRotationPromptBody, hotKeyRotationUnconfirmedBody, importHotKeyError, importHotKeyInvalid, importHotKeyNoAccount, importSeedPhraseError, importSeedPhraseTooLong, incorrectPasscode, incorrectPassword, insufficientFeeAsset, insufficientFeeAssetShort, invalidAddress, invalidAmount, invalidEthereumAddress, invalidMidenAccountId, invalidMidenAddress, invalidPayload, invalidQrImage, invalidRecoverySeed, invalidUrl, justValidPreGeneratedMnemonic, mainnetAccessCheckFailed, mainnetAccessCodeRejected, mainnetAccessPasteFailed, meetGuardianNoneReachable, midenAddressWrongNetwork, noCameraFound, noQrCodeFound, noteClaimFailedRetry, nothingToPaste, oneClickBalanceUnavailable, oneClickBroadcastTrackingPending, oneClickQuoteUnavailable, oneClickSubmitFailed, passcodesDoNotMatch, passwordsDoNotMatch, raw_account_export_failed, raw_arcade_gc_purchase_failed, raw_bridge_balance_failed, raw_bridge_claim_failed, raw_bridge_connect_failed, raw_bridge_deposit_failed, raw_bridge_deposit_submit_failed, raw_bridge_reclaim_failed, raw_choose_guardian_submit_failed, raw_confirm_approval_failed, raw_confirm_payload_failed, raw_contact_delete_failed, raw_contact_save_failed, raw_contact_scan_failed, raw_dapp_approval_failed, raw_dapp_permission_denied, raw_earn_deposit_submit_failed, raw_earn_withdraw_failed, raw_earn_withdraw_submit_failed, raw_encrypted_file_unlock_failed, raw_faucet_download_failed, raw_faucet_id_save_failed, raw_faucet_share_failed, raw_guardian_replace_hot_key_failed, raw_guardian_switch_failed, raw_guardian_url_save_failed, raw_history_cancel_failed, raw_history_details_load_failed, raw_history_retry_failed, raw_history_swap_cancel_failed, raw_home_faucet_failed, raw_hot_key_rotation_failed, raw_hot_key_rotation_funding_claim_failed, raw_import_hot_key_scan_failed, raw_import_recovery_lookup_failed, raw_onboarding_recovery_failed, raw_options_reload_failed, raw_pending_activity_retry_failed, raw_recovery_seed_submit_failed, raw_secret_reveal_failed, raw_seed_remove_failed, raw_seed_reveal_failed, raw_seed_verification_reveal_failed, raw_send_submit_failed, raw_startup_init_failed, raw_startup_root_missing, raw_swap_submit_failed, raw_transaction_failed, raw_transaction_retry_failed, raw_usdcx_execute_failed, recallTimeInPast, required, resetDidNotFinish, runtimeUnsupportedBody, seedBackupSaveFailed, seedRemovalFailed, smthWentWrong, spendingLimitInvalidAmount, spendingLimitLoadFailed, spendingLimitSaveFailed, submissionBlockedByIssuer, submissionInterrupted, submissionRejectedByGuardian, submissionRejectedByNetwork, submissionUnanswered, swapInvalidAmounts, swapPairNotListed, swapPriceMoved, swapPriceUnavailable, swapSameToken, swapSlippageRange, swapVerdictNoMarket, swapVerdictNoPrice, tokenActivityLoadError, tokenDecimalsInvalid, tokenMetadataSaveError, tokenNameInvalid, tokenSymbolInvalid, transactionErrorDescription, transactionFailedNotificationBody, transactionNotConfirmedNotificationBody, unknownTokenScale, unlockPasswordErrorDelay, updateNotificationActionFailed, usdcxBelowMinimumBurn, usdcxBurnFeeUnavailable, usdcxDepositFeeAboveBalance, usdcxDomainNotRegistered, usdcxFaucetUnavailable, usdcxForwardQuoteUnavailable, usdcxInvalidAmount, usdcxSendMoreThanPayoutFee, usdcxUnsupportedFaucet, verifyStepWrong, walletAccessUnavailableBody, walletSetupDidNotComplete) and which kind of display showed it (inline, field, passcode, notice, review, dialog, status_screen, prompt, startup, browser_alert, os_notification or native_toast). It never sends the text you saw or typed. These reports use the same crash-report permission, with at most one report a minute for one message on one kind of display and 30 reports in ten minutes in each running App window or worker.
The App also sends a short diagnostic history of each swap, so that a failed swap can be traced to the step it failed at. These records go to the same crash-reporting service on the same terms as crash reports: they are sent whether or not you share usage data, never where Firefox’s own switch says no, and nothing waiting is sent once crash reporting stops. Each record is one of two fixed events (operation_step or operation_end) and carries only these fixed values: the operation it is about (swap); for a step, the step it describes (service_prepare, request_build, request_persist, proposal_create or proposal_execute) and how that step ended (ok, failed, timeout, aborted or locked); for the end of a swap, how the operation ended (completed, failed, timeout, cancelled, interrupted or unconfirmed); and how long it took, as a range (under_1s, 1_to_5s, 5_to_30s, 30s_to_2m, 2_to_10m or over_10m). Each record also carries the time it was made, a severity level, the app version, a number that orders records made in the same instant, and a random number created in memory when crash reporting starts, never written to disk, which the crash reports sent in that run of the App also carry, so a failure can be read beside the records before it. A record never contains an error message, an address, an amount, an identifier of an account, a transaction or a note, or a web address. The App sends at most 10 records for one operation in any 10 seconds and 60 in any 10 minutes, and drops the rest.
Crash reports do not include the page you were on, your browser’s user-agent string, your language, your timezone, or a recording of your screen or clicks. Alongside the error, each report carries the time it happened, a severity level, its own random report id generated for that one report, and the name and version of the reporting library — the standard technical envelope, with nothing about you in it.
Please read “What scrubbing can and cannot promise” below, which is the honest limit of this.
What we never collect
Not when the setting is on, not when it is off, not ever:
- Your private keys, recovery phrase, or password
- Your account addresses, balances, or transaction amounts
- The contents of your transactions, your notes, or your note IDs
- Your name, email address, or contacts
- Your location — the App asks for no location permission and reads no location sensor
- Your operating system version, your language or locale, your timezone, or your device model — see the note under “Usage data”
- Any advertising identifier, and anything that would let us or anyone else follow you across other apps or websites. The App does not show Apple’s App Tracking Transparency prompt, because it has nothing to ask permission for.
And nothing collected is ever sold, or shared with data brokers or ad networks.
We also do not call this data anonymous. Sending anything over the internet means the receiving server sees the IP address the request came from, exactly as it does for any other network request — and an IP address indicates a rough region. Our processors are configured not to store it. A processor may still derive something coarse from it before discarding it, such as which country a request came from, and keep that alongside the message; we are not going to describe data as anonymous when a server saw where it came from.
Identifiers
The App holds no persistent identifier for you — no user id, no device id, no install id, no cookie, no advertising identifier. Nothing in the usage data or the crash reports says “this is the same person as last week”, or even “as yesterday”.
Two short random numbers are involved, and both live only in memory:
- One per activity. Created when an activity starts, used only to match that activity’s “started” message to its “ended” message, never written to disk, never reused. Once the activity is over, it is gone.
- One per run of the App. Created when the App starts and shared by the activities you perform in that run, so that a sequence like “opened the wallet, went to swap, changed your mind, sent instead” can be read as one visit rather than as unconnected fragments. It is never written to disk, so closing the App or reloading the extension ends it permanently and the next run gets an unrelated value. It also renews itself after 30 minutes with no activity, so a window left open does not accumulate a long trail.
What that second number does and does not allow, stated plainly. Within one run of the App, the activities you performed can be seen as belonging together. Across runs — and across devices, and across people — nothing can. This is a deliberate trade: the earlier design used the per-activity number for both purposes, and the result was data in which every session held exactly one activity and lasted zero seconds, which could not answer the only question the feature exists to answer, namely where people get stuck.
One thing worth spelling out, because our provider’s naming suggests otherwise. Aptabase calls the field this number travels in a “session id”, and their own libraries store it on your device and reuse it on a four-hour timeout — which makes it survive restarts. Ours does not exist anywhere but memory. That is also the only option available to us: the App has no way to store such a value at all, and a test asserts it stays that way.
What that means for deleting your data
It means we cannot delete your data on request, and we would rather say so than promise otherwise.
There is no identifier in this data by which we could find your records. If you asked us to delete your usage data or your crash reports, we would have no way to tell yours apart from everyone else’s — the records simply do not say who they came from.
This is a deliberate trade-off, not a way of avoiding the request. The alternative is to attach a durable identifier to every message so that we could find your data later, which would mean building the exact tracking capability we chose not to have. We decided that not being able to identify you is worth more than being able to service a deletion request, and this paragraph is the cost of that decision stated plainly.
Two things do follow from it, in your favour:
- Turning the setting off is complete. There is no profile left behind to keep, because there was never a profile.
- Everything expires on its own. All usage data and all crash reports are deleted 90 days after they are received. Nothing is kept longer, and nothing needs a request to make that happen.
Everything the App stores about your wallet — your keys, accounts, balances, and history — is on your device, and you can delete it yourself with “Delete wallet” in Settings, or by uninstalling the App. That is unaffected by any of the above.
What scrubbing can and cannot promise
The scrubbing described under “Crash reports” is thorough but it is a filter, not a guarantee, and you should know its limits: crash reports are sent whether or not you share usage data.
It works by recognising what secrets look like. That is reliable for the things that have a recognisable shape: a Miden address, a long hex value, a run of digits, a credential in a URL, or any value stored under a name like password. Three kinds of thing can get past it:
- A recovery phrase broken into small pieces. The filter spots runs of consecutive words from the recovery-phrase wordlist, and a real phrase is 12 or 24 words. A phrase scattered two or three words at a time across many separate fields of one report could fall below that threshold and survive.
- A recovery phrase split by the App’s own fixed words. A transaction failure report is built only from fixed values the App defines, and each of them ends a run of wordlist words, so that the report’s own labels are never read as a phrase. Some of those values, such as
fee,localandother, are wordlist words themselves, so a phrase that such values split every three words would not read as one run. No transaction failure report carries free text, so nothing can place a phrase there today. - A password with nothing around it. If a password were ever the entire text of an error message, with no field name and no URL to identify it, the filter has nothing to go on: by shape alone it is indistinguishable from a harmless request id.
None of them is a known leak. No part of the App puts a recovery phrase or a password into an error, and automated tests run on every change assert that. They are the residual risk if some future code did, and we would rather name them than imply the filter is perfect.
Who processes this data
Crash reports are sent either way; usage data only if the setting is on:
| Data | Processor | Where |
|---|---|---|
| Usage data | Aptabase | European Union (Germany) |
| Crash reports | Sentry | European Union |
| Feature eligibility check | Cloudflare and MaxMind | Connection address for the lookup; short-lived service records as described below |
Usage data goes straight from the App to Aptabase’s European endpoint, one message per request, with no server of ours in between and nothing batched up in the meantime. We use neither company’s own tracking library: the App builds each message itself, out of the fixed list of fields above, and sends it directly.
Aptabase is configured to delete data after 90 days, and Sentry deletes it after the periods under “How long we keep data”. Both are configured to store no IP addresses and to forward nothing onward. Neither is permitted to use the data for its own purposes, and no third party receives it for advertising or resale. The feature eligibility service runs on Cloudflare and uses MaxMind to check the connection’s IP address. The App receives only whether wallet access, Earn and Swap are allowed, plus the network and policy version. The service stores no raw IP address or geographic answer; it stores daily rotating keyed hashes of connection addresses, request counters and feature decisions. Feature decisions expire after five minutes, and expired caller records are removed in batches.
How the App uses your device
For transparency about how the App functions on your device (this is not data we receive or have access to):
- Any Recovery Phrase and signing keys stored by the App are kept locally and encrypted at rest using your device’s secure enclave (Android Keystore / iOS Secure Enclave) when biometric protection is enabled. The App does not send them to Miden or your Guardian Operator. You can copy a Recovery Phrase, download it and remove it from the device; what you do with that copy is up to you.
- The App also derives an Ethereum address and key for you from your Recovery Phrase, used for Earn and for fiat on-ramp and off-ramp. That key is stored encrypted on your device, signs on your device, and never leaves it; restoring from your Recovery Phrase restores it.
- Your account state (your accounts’ data, balances and history) is held on your device. If you use a Guardian, an encrypted copy is also held by your Guardian Operator; see “Your Guardian Operator” below. Apart from that copy, your account state leaves your device only if you export it: Settings → Security → Encrypted Wallet File saves a password-encrypted snapshot of the account state the file can restore. The file includes your Recovery Phrase if one is stored in the wallet and the signing secrets needed to restore eligible imported public accounts; it does not include a Guardian account’s Everyday Key. The App saves the file through the destination you select and does not send it to Miden or your Guardian Operator. Anyone with the file and its password can read its account state and any phrase or imported-account secret it contains.
Biometric authentication (Face ID, Touch ID, Fingerprint)
The App offers optional biometric unlock using your device’s built-in authentication — Face ID or Touch ID on iOS, fingerprint or face authentication on Android.
The App does not collect, access, receive, store, transmit, or share any face data, fingerprint data, or other biometric data.
Biometric matching is performed entirely by your device’s operating system inside secure hardware (Apple’s Secure Enclave / Android’s hardware-backed keystore). Your biometric templates are created, stored, and managed solely by iOS/Android and are never made available to the App. When you unlock with biometrics, the App receives only a yes/no result indicating whether authentication succeeded — it never receives any biometric data itself.
- What we collect: No face or fingerprint data. None is ever transmitted off your device; we operate no servers that could receive it.
- How it is used: A successful biometric check causes your device’s operating system to release a wallet-unlock key held in secure hardware, allowing the App to decrypt your local wallet without you typing your password. This happens entirely on-device.
- Sharing / third parties: None. No biometric data exists in the App to share.
- Storage: No biometric data is stored by the App. Your biometric templates remain solely under your operating system’s control in secure hardware.
- Retention / deletion: The App retains no biometric data. You manage your enrolled biometrics in your device’s system settings. Disabling biometric unlock in the App, or uninstalling the App, removes the App’s on-device unlock key and has no effect on and no access to your OS-level biometric enrollment.
Data that leaves the App for reasons other than telemetry
Everything in this section is data the App must send for a feature to work. None of it is telemetry, none of it is optional once you use the feature, and most of it is received by someone other than us.
The Miden network and its operator
To transact, the App sends transaction proofs and related data to the Miden network through its RPC endpoint and the note transport service. Miden is designed to support private transaction execution. During the network’s current operating period, however, the transaction data you submit, including data associated with private accounts and private notes, is stored by the network’s validators in encrypted form, and any two validators acting together can decrypt it. There are three validators, operated by the Foundation, Gateway.fm and LambdaClass. We operate no validator and cannot decrypt your data. The Foundation cannot decrypt it on its own either: decryption requires two validators to cooperate, so at least one party independent of the Foundation must agree, and it takes place only for network-integrity, compliance and legal purposes, including in response to a lawful request. This is a property of the network in its current phase, not of the App; it applies whatever wallet you use.
We retain compliance-related records, including block, unblock and legal-process records, for at least five years, and may disclose information where required by law or lawful process. Data written to the blockchain itself is permanent and cannot be deleted by anyone, including us.
Your Guardian Operator
If you enable a Guardian, your Guardian Operator receives and stores an encrypted copy of your account state, and nothing else about you. It does not receive your Recovery Phrase, your Everyday Key, or any identity or contact data. At launch the Guardian Operators are LambdaClass (FOLTREK SA), KODA (Korea Digital Asset Co., Ltd.), Para (Capsule Labs Inc.) and Gateway.fm (GATEWAY.FM AS), all separate parties from us. When you create a wallet the App measures each operator’s response time and assigns you the fastest one; the App shows you which operator you have, and you can change it at any time. Each operator handles this data under its own privacy notice.
Fiat on-ramp and off-ramp (Transak)
Buying crypto with fiat and selling crypto for fiat in the App is provided by Transak through a widget. When you use it you open an account with Transak and deal with Transak directly. Transak verifies your identity before you can transact and collects identity documents, bank details and other personal data for that purpose, under Transak’s privacy policy. We do not receive your identity documents or bank details. Transak pays crypto to, and receives crypto from, the Ethereum address the App derives for you (see “How the App uses your device”), so Transak can link your verified identity to that address, and transfers between that address and your Miden accounts are visible on the networks involved.
Swaps and bridges
A Miden swap is filled by the solver operated by 0D Labs Limited, a cross-chain swap by a third-party solver, and a bridge transfer is processed by a third-party bridge operator and the other network. Each of them sees the transaction it processes: the assets, the amounts and the addresses involved. Before that, the App requests quotes: a swap quote sends the two tokens and their amounts to the swap-quote service, which for a Miden swap is run by 0D Labs Limited, never your address, and tracking a bridge transfer sends its destination address to the bridge’s status service (both named under “Network traffic”).
Earn
Earn is provided by Epoch (SYNC TECH LLC, St. Vincent and the Grenadines). When you use it, Epoch receives your Miden account, the Ethereum address the App derives for you, and the transactions you sign for the lending market on the Ethereum network that holds your position. The position is recorded at your Ethereum address in a third-party lending market, which sees that address and the amounts. The note that funds a deposit is public on Miden, so the deposit amount and your Miden account are visible on the network. Epoch handles this data under its own privacy notice.
Geographic restrictions
The App asks for no location permission and reads no location sensor. The App and the Miden network are, however, not available from comprehensively sanctioned jurisdictions, and some features are not offered in every place. At startup, in the background, the App asks a service running on Cloudflare whether wallet access, Earn and Swap are allowed for its connection. It keeps those decisions in memory for the page session and does not repeat the check while the wallet is open. Where a global or enabled feature restriction needs a place check, the service sends the connection’s IP address to MaxMind and reads the country and region. The App receives no location and stores no IP address. The service retains only the short-lived records described above. The same address is also used for the ordinary operation of the connection, and the network’s sequencer endpoint checks the country of a request.
Wallet access, including withdrawals, is unavailable in Cuba, Iran, North Korea, Russia, Belarus, Crimea, Sevastopol and the whole Donetsk, Luhansk, Kherson and Zaporizhzhia oblasts. New Earn deposits are additionally unavailable in New York and the UK. Without a successful startup eligibility decision, the wallet opens with Earn and Swap unavailable.
Network traffic
The App connects to the public Miden blockchain RPC endpoint (rpc.testnet.miden.io) and the Miden note transport service (transport.miden.io) to send and receive on-chain transactions. These requests contain only data needed to interact with the blockchain (transaction payloads, public account state) - never your private keys or recovery phrase.
It also downloads public data. Miden’s verified-token list and update notices come from GitHub (raw.githubusercontent.com) and carry nothing about you. The logos of listed tokens come from the same GitHub list, so GitHub sees which listed tokens’ logos the App draws (the listed tokens you hold or open), never your account, address or balance. Explore’s catalog of apps and their icons come from GitHub too, so GitHub sees which network’s catalog and which of its icons the App requests, never your account, address or balance. Token prices and charts come from Binance (data-api.binance.vision) and, for tokens without a supported Binance market, Kraken (api.kraken.com). These providers see the requested markets from the verified token list or bridge configuration and the charts the App opens, never your address, keys or balances.
Three features send the service behind them only what that feature needs. Requesting test tokens sends your account address and the amount to the Miden faucet (faucet-api.testnet.miden.io). Tracking a bridge transfer sends its destination address to the bridge’s status service (miden-testnet-bridge.dev.eu-north-3.gateway.fm). A swap quote sends the two tokens and their amounts to the swap-quote service (35-175-40-181.sslip.io), never your address.
The App reaches Sentry to send crash reports. If Share usage data is on, it also reaches Aptabase; if it is off, it does not contact Aptabase at all.
Apart from those token logos and Explore icons, and the site icons the dApp browser on mobile fetches from Google’s favicon service, the App loads no fonts, scripts, or images from third-party servers. Everything else it needs to draw itself ships inside the App.
Permissions
The App requests the following Android permissions:
- INTERNET - to reach the Miden RPC and transport endpoints and the services named under Network traffic
- VIBRATE — for haptic feedback on UI interactions
- USE_BIOMETRIC (Android 6+) — to unlock the wallet via fingerprint / face authentication
Children
The App is not directed at anyone under 18, and our Terms of Service require users to be at least 18. We do not knowingly collect any information from children.
Why we process data, and on what legal basis
| Purpose | Data | Legal basis (GDPR Article 6) |
|---|---|---|
| Running the App and submitting your transactions | Transaction data sent to the network, Guardian data | Performance of our contract with you (the Terms of Service) |
| Fixing crashes | Crash reports | Our legitimate interest in keeping the App working |
| Understanding where people get stuck | Usage data | Your consent, which you give by turning on “Share usage data” and can withdraw at any time in the same place |
| Sanctions compliance, geographic restrictions, record-keeping, responding to lawful requests | IP country and region check, block and unblock records, legal-process records | Compliance with legal obligations, and our legitimate interest in operating a lawful network |
| Restricting new Earn deposits by region | IP country or region check as part of the wallet eligibility request | Our legitimate interest in not offering new Earn deposits where we have chosen not to, and compliance with the laws of those places |
How long we keep data
- Usage data and crash reports: 90 days at most, then deleted automatically (see “Identifiers”).
- Diagnostic records sent with crash reports: 30 days, then deleted automatically by the crash-reporting service; the crash reports themselves are kept for 90 days.
- Compliance records (block, unblock and legal-process records): at least five years. These are separate from the transient eligibility-service records below.
- Eligibility-service records: feature decisions expire after five minutes; caller records expire after five minutes without a request and are removed in batches. Caller identifiers are keyed hashes that rotate daily. The App keeps its eligibility decision in memory for at most 60 seconds and writes no geographic answer to device storage.
- Data held by your Guardian Operator: as set out in its privacy notice.
- Data written to the blockchain: permanent.
Where data goes
Crash reports and usage data are processed in the European Union (see “Who processes this data”). Your Guardian Operator and Transak may be located outside the European Union and the United Kingdom.
Your rights
If you are in the European Union, the European Economic Area or the United Kingdom, you have the right to access the personal data we hold about you, to have it corrected or erased, to restrict or object to its processing, to receive it in a portable format, and to withdraw consent where processing rests on consent. You also have the right to complain to a data protection supervisory authority, in particular in the country where you live.
Two limits follow from how the App is built, and we would rather state them than imply otherwise:
- We cannot find, and therefore cannot access, correct or erase, your usage data or crash reports, because they carry no identifier that links them to you (see “What that means for deleting your data”). They are deleted automatically after 90 days.
- Data written to the blockchain cannot be erased by anyone. Erasure requests can only apply to records we hold off-chain.
To exercise a right, write to privacy@miden.team.
Changes
Material changes to this policy will be reflected on this page with an updated “Last updated” date.
Contact
Open an issue at github.com/0xMiden/wallet/issues or email privacy@miden.team.
Closed terminal transaction observations
These independent observations add no raw error text, identifiers, row contents, timestamps, request bytes or account/authentication data and are not diagnoses or funds outcomes. They use existing producer boundaries without extra SDK, database or network work.
proposalCreateApiis onlyconsume_notes; its mandatoryproposalCreateFamilyis one ofconsume_note_missing,consume_note_not_verified,consume_metadata_oversize,multisig_required_args_missing,bound_block_not_declared,summary_layout_unsupported,sdk_input_note_read,sdk_account_read,sdk_latest_block_header_read,sdk_protocol_config_read,sdk_request_build,unmatchedorunreadable. This original-rejection observation applies only while the effective boundary remains proposal creation. It does not establish an internal phase or whether a remote proposal was accepted.guardianProvingSubstepisprove_delegate,local_prover_build,prove_local,fallback_prover_build,prove_fallback,proof_evidence,submitting_stage_writeorunobserved. It names only an escaping inline Guardian boundary, never an offscreen inference or swallowed failure.- On committed queued expiry,
queueAdmissionObservationisin_progress_gate,row_ineligible,selectedorunavailable. Only directly examined same-realm membership is retained, for at most 60 seconds and 256 entries; no queue cause, continuous blockage, key or exact age is sent. Separately,queueSightingisnever,not_reached,in_progress_gate,row_ineligibleorselected: the strongest look stored on that wait, including after a restart.preflightSyncisnot_entered,entered,returnedorthrew.queueHiddenisnot_measured,unobserved,none,under_half,half_or_moreorunavailable. Those three are fixed labels only. The binding numbers that keep them on the same wait stay on the row and are not sent. - Only an own started-write deadline whose captured timer was
startedmay carry the mandatory pairoffscreenDeadlineMethodandoffscreenDeadlineStage. Methods aresendTransaction,swapTransaction,consumeNoteId,newTransaction,guardianPipelineorunobserved. Stage is the last accepted canonical transaction stage (syncing,sending,creating-proposal,signing-proposal,signing-locally,executing,proving,submitting,confirming,registering-guardian,delivering,guardian-syncing,guardian-synced,complete) ornot_observed. It is an observation at rejection construction, not the exact timer-fire instant or the hung SDK operation. Collateral victims receive none of this pair. - Only a committed stuck reap may carry
reapStartRelation:before_realm_cutoff,at_or_after_realm_cutofforunavailable. It compares the selected row’s recorded start with the existing realm cutoff, bound to unchanged committed identity and start. No raw timestamp or lifecycle inference is sent.