{
  "id": "of1-sell-metamask",
  "title": "The Fiat Off-Ramp",
  "context": {
    "wallet": "MetaMask extension",
    "platform": "Chrome extension",
    "network": "mainnet",
    "settings": "defaults",
    "version": "web/5.157.3",
    "observed": "2026-09-01",
    "captures": [
      {
        "run": "20260901-140427-sell-metamask",
        "note": "PARTIAL live capture. Reached the off-ramp FRONT surface live: geolocation resolved the exit to region us-co, and MetaMask pulled the region-gated provider/payments catalog (~25KB). Key finding the code map missed: MetaMask's Buy/Sell is NOT in the extension — it opens a hosted web app in a NEW TAB (app.metamask.io/buy), carrying a persistent metametricsId in the URL, and loads third-party AD + analytics trackers (LinkedIn ad pixel, HubSpot CRM, Intercom, LaunchDarkly, Sentry) before you do anything. The Sell-specific legs (provider quote, MoonPay KYC + bank, the signed public exit tx, AML payout) were NOT reached — they sit behind a MetaMetrics/marketing-cookie consent wall and then KYC, which this capture-only run deliberately does not cross. Nothing signed, no KYC, no bank entered."
      }
    ]
  },
  "_comment": "The EXIT — turning crypto back into fiat, and the mirror of the on-ramp (ON). PARTIALLY live-captured Sep 1 2026 (run 20260901-140427): the geolocation + region gate + tracker surface are observed; the KYC/bank/sign-exit/AML legs remain code/inferred because they sit behind a consent wall and KYC that the capture-only run does not cross. Two things make the off-ramp WORSE than the on-ramp. First, you must hand over a BANK ACCOUNT to RECEIVE the wire — full legal identity plus a payout destination. Second, and unique to the exit: you SIGN and broadcast an on-chain transaction sending your crypto to the provider, which permanently and PUBLICLY links that address to your KYC'd identity — anyone reading the chain can tie the two together forever. And because the crypto leaves your custody BEFORE the fiat arrives, the provider can freeze the payout (an AML flag, a sanctions hit, a bad region) after you have already given up the coins. The capture ALSO surfaced something the code trace missed entirely: the ramp is a hosted, marketing-instrumented web app (app.metamask.io) opened in a new tab with a persistent metametricsId in the URL, loading a LinkedIn ad pixel + HubSpot + Intercom + LaunchDarkly + Sentry the moment it opens. The zero-option question is even starker than the on-ramp: there is essentially no private fiat exit through these rails — the only 'zero option' is to spend crypto directly or sell peer-to-peer for cash, both thin.",
  "steps": [
    {
      "id": "sell-geolocation",
      "phase": "discover",
      "name": "MetaMask sell · geolocation",
      "host": "on-ramp.api.cx.metamask.io/geolocation",
      "actor": "consensys-apis",
      "purpose": "Geolocate you the moment Sell opens, to decide your region",
      "need": "T",
      "carries": [
        "ip"
      ],
      "births": [
        "kyc"
      ],
      "returns": "your region — which gates which providers can pay you out",
      "can_block": true,
      "on_failure": "blocks",
      "failure_note": "no region → no off-ramp providers offered",
      "worst_lie": {
        "outcome": "false_belief",
        "note": "the region it assigns decides who is allowed to buy your crypto and wire you fiat"
      },
      "removable_by": "none",
      "provenance": {
        "status": "observed",
        "ref": "20260901-140427-sell-metamask — geolocation.api.cx.metamask.io/v2/geolocation fired live on the ramp surface and resolved the exit to region us-co, which gates the provider list. Same geolocation+region endpoint serves both Buy and Sell."
      },
      "notes": "Observed live: the moment the ramp opens, MetaMask geolocates you and resolves a region (us-co here) before you type anything. New finding vs the code map: the ramp is not in the extension — it opens a HOSTED web app (app.metamask.io) in a new tab, with a persistent metametricsId in the URL.",
      "fixed_by": [
        "indie-frontends"
      ]
    },
    {
      "id": "ramp-trackers",
      "phase": "discover",
      "name": "app.metamask.io · ad + analytics trackers load",
      "host": "px.ads.linkedin.com",
      "actor": "linkedin-ads",
      "via": [
        "hubspot",
        "intercom",
        "launchdarkly",
        "sentry"
      ],
      "purpose": "The hosted ramp web app loads third-party ad, CRM, and telemetry trackers the instant it opens",
      "need": "T",
      "carries": [
        "ip"
      ],
      "returns": "nothing you need — this is pure instrumentation of your visit to the off-ramp",
      "can_block": false,
      "on_failure": "silent",
      "worst_lie": {
        "outcome": "none"
      },
      "removable_by": "none",
      "provenance": {
        "status": "observed",
        "ref": "20260901-140427-sell-metamask — on opening app.metamask.io the page loaded a LinkedIn ad pixel (px.ads.linkedin.com, www.linkedin.com), HubSpot marketing/CRM (js.hubspot.com, track.hubspot.com, api.hubapi.com, hs-analytics, hsforms), Intercom (widget.intercom.io, api-iam, nexus-websocket), LaunchDarkly (app+events.launchdarkly.com), and Sentry (o1377931.ingest.sentry.io)."
      },
      "notes": "The finding the code map missed: MetaMask's off-ramp entry is a marketing-instrumented web app. Before any Sell action, it fires an advertising conversion pixel (LinkedIn — the primary actor) plus HubSpot CRM, Intercom support-chat, LaunchDarkly flags, and Sentry (all carried as via-recipients, so each counts in the aggregates) — and gates entry behind a MetaMetrics + Error-Tracing + marketing-cookie consent modal.",
      "fixed_by": [
        "indie-frontends"
      ]
    },
    {
      "id": "sell-region-quote",
      "phase": "discover",
      "name": "MetaMask sell · region-gated providers + quote",
      "host": "on-ramp-cache.api.cx.metamask.io/v2/regions/{region}/providers",
      "actor": "consensys-apis",
      "purpose": "Return the sell providers, payout methods, and a quote allowed in your jurisdiction",
      "need": "T",
      "carries": [
        "ip",
        "holdings"
      ],
      "returns": "the list of providers permitted to pay you out, and how much fiat you'd get",
      "can_block": true,
      "on_failure": "blocks",
      "failure_note": "empty list in an unsupported region → you cannot cash out at all",
      "worst_lie": {
        "outcome": "false_belief",
        "note": "your jurisdiction decides who can pay you; the quote also frames what you accept"
      },
      "removable_by": "none",
      "repeat": "on every Sell open",
      "provenance": {
        "status": "observed",
        "ref": "20260901-140427-sell-metamask — on-ramp-cache.api.cx.metamask.io/v2/regions/us-co/providers returned a ~25KB region-gated provider catalog, plus /payments and /topTokens for the region. Live-confirmed the jurisdiction gate; the sell-direction quote (amount + selected provider) sits one screen deeper, behind the consent wall, and was not captured."
      },
      "notes": "Live-confirmed: the provider list is jurisdiction-gated by your detected region (us-co) exactly like the on-ramp. The specific sell quote (the amount you're cashing out) is entered on the next screen, which this capture-only run stops short of.",
      "fixed_by": [
        "indie-frontends"
      ]
    },
    {
      "id": "provider-kyc-bank",
      "phase": "confirm",
      "name": "MoonPay · KYC + bank for payout",
      "host": "api.moonpay.com",
      "actor": "moonpay",
      "purpose": "The chosen provider runs full KYC and takes a BANK ACCOUNT to wire the fiat to",
      "need": "T",
      "carries": [
        "ip",
        "selected_address",
        "kyc"
      ],
      "returns": "clearance to sell — once it has your legal identity AND a payout bank account",
      "can_block": true,
      "on_failure": "blocks",
      "failure_note": "fail KYC, or an unsupported bank/region → no payout",
      "worst_lie": {
        "outcome": "false_belief",
        "note": "the provider holds your identity, your bank link, and the on/off switch on your exit from crypto"
      },
      "removable_by": "none",
      "provenance": {
        "status": "inferred",
        "ref": "api.moonpay.com (real host, seen on the Buy side); the sell KYC + bank collection sits behind the provider and was NOT entered"
      },
      "notes": "The identity + bank SINK. Worse than the on-ramp: the on-ramp took a card to CHARGE you; the off-ramp takes a bank account to PAY you — a durable financial link, plus tax reporting.",
      "fixed_by": [
        "indie-frontends"
      ]
    },
    {
      "id": "provider-deposit-address",
      "phase": "confirm",
      "name": "MoonPay · deposit address",
      "host": "api.moonpay.com",
      "actor": "moonpay",
      "purpose": "The provider returns the address you must send your crypto to",
      "need": "T",
      "carries": [
        "selected_address"
      ],
      "returns": "the provider's deposit address — now tied to your KYC record",
      "can_block": true,
      "on_failure": "blocks",
      "worst_lie": {
        "outcome": "funds_lost",
        "note": "a swapped deposit address sends your crypto to an attacker; you have no recourse"
      },
      "removable_by": "none",
      "provenance": {
        "status": "inferred",
        "ref": "standard sell flow: the provider issues a per-order deposit address; not captured"
      },
      "notes": "The moment your address is bound to your legal identity in the provider's records — before anything is even sent.",
      "fixed_by": [
        "clear-signing"
      ]
    },
    {
      "id": "sign-send-crypto",
      "phase": "broadcast",
      "name": "Send crypto to the provider · RPC broadcast",
      "host": "mainnet.infura.io",
      "actor": "infura",
      "purpose": "You sign and broadcast an on-chain tx moving your crypto to the provider's address",
      "need": "R",
      "carries": [
        "signed_tx",
        "recipient",
        "amount",
        "selected_address"
      ],
      "returns": "a public, permanent on-chain record linking your address to the provider (and your KYC)",
      "can_block": true,
      "on_failure": "blocks",
      "failure_note": "the RPC can withhold your broadcast",
      "worst_lie": {
        "outcome": "funds_stuck",
        "note": "the broadcast node can drop or delay the tx; your crypto is committed to the provider once it lands"
      },
      "removable_by": "custom_rpc",
      "provenance": {
        "status": "code",
        "ref": "standard eth_sendRawTransaction via the default MetaMask RPC (Infura); identical to the send leg on every other map"
      },
      "notes": "UNIQUE to the off-ramp: unlike the on-ramp (no signing, crypto arrives to you), here you SIGN a public tx. That transaction ties this address to your KYC'd identity forever, readable by anyone.",
      "fixed_by": [
        "private-rpc",
        "verifiable-rpc"
      ]
    },
    {
      "id": "offramp-inclusion",
      "phase": "broadcast",
      "name": "Block builder · inclusion",
      "host": "(builder / relay)",
      "actor": "builders",
      "purpose": "A builder includes your send-to-provider tx in a block",
      "need": "R",
      "carries": [
        "signed_tx"
      ],
      "returns": "your exit tx, public in a block",
      "can_block": true,
      "on_failure": "degrade",
      "worst_lie": {
        "outcome": "none"
      },
      "removable_by": "none",
      "provenance": {
        "status": "inferred",
        "ref": "every mainnet tx passes a builder/relay; inclusion inferred from protocol, not a wallet call"
      },
      "fixed_by": [
        "cr-broadcast"
      ]
    },
    {
      "id": "aml-payout",
      "phase": "confirm",
      "name": "MoonPay · AML screen + bank wire",
      "host": "api.moonpay.com",
      "actor": "moonpay",
      "purpose": "The provider screens the payout, then wires fiat to your bank — after it already holds your crypto",
      "need": "T",
      "carries": [
        "kyc",
        "amount"
      ],
      "returns": "fiat in your bank — or a frozen payout",
      "can_block": true,
      "on_failure": "blocks",
      "failure_note": "an AML/sanctions flag can freeze the payout after you've sent the crypto",
      "worst_lie": {
        "outcome": "funds_stuck",
        "note": "the crypto left your custody BEFORE the fiat arrives; the provider can hold or claw the payout"
      },
      "removable_by": "none",
      "provenance": {
        "status": "inferred",
        "ref": "AML screening + bank wire + tax reporting sit behind the provider and the banking rails; not observable from the client"
      },
      "notes": "The asymmetry that defines the off-ramp: you give up custody first, the fiat comes second, and a screen sits in between. Tax authorities are reported to here too.",
      "fixed_by": [
        "indie-frontends"
      ]
    }
  ],
  "own_node": {
    "node": "192.168.68.54",
    "runs": [
      "20260924-214902-sell-metamask-localnode"
    ],
    "steps": {
      "sell-geolocation": {
        "state": "out",
        "ref": "on-ramp.api.cx.metamask.io · 4 req after switch · /geolocation"
      },
      "ramp-trackers": {
        "state": "out",
        "ref": "px.ads.linkedin.com · 4 req after switch"
      },
      "sell-region-quote": {
        "state": "out",
        "ref": "on-ramp-cache.api.cx.metamask.io · 14 req after switch · /providers//v2/regions"
      },
      "provider-kyc-bank": {
        "state": "unseen",
        "ref": "not in these runs"
      },
      "provider-deposit-address": {
        "state": "unseen",
        "ref": "not in these runs"
      },
      "sign-send-crypto": {
        "state": "unseen",
        "ref": "not reached (capture-only run). The 2 mainnet.infura.io calls after the switch were eth_chainId under a different Infura key from an origin-null page, not MetaMask's send"
      },
      "offramp-inclusion": {
        "state": "unseen",
        "ref": "not in these runs"
      },
      "aml-payout": {
        "state": "unseen",
        "ref": "not in these runs"
      }
    },
    "unmapped": [
      "tron-mainnet.infura.io",
      "app.metamask.io",
      "app.launchdarkly.com",
      "api.segment.io",
      "gas.api.cx.metamask.io",
      "bitcoin-mainnet.infura.io",
      "sentry.io",
      "solana-mainnet.infura.io",
      "user-storage.api.cx.metamask.io",
      "price.api.cx.metamask.io",
      "accounts.api.cx.metamask.io",
      "authentication.api.cx.metamask.io",
      "cdn.segment.com",
      "events.launchdarkly.com",
      "monad-mainnet.infura.io",
      "subscription.api.cx.metamask.io",
      "cdn.contentful.com",
      "www.googletagmanager.com",
      "o1377931.ingest.sentry.io",
      "carrot.megaeth.com",
      "testnet-rpc.monad.xyz",
      "sepolia.infura.io",
      "linea-sepolia.infura.io",
      "api.merkl.xyz",
      "bsc-mainnet.infura.io",
      "base-mainnet.infura.io",
      "linea-mainnet.infura.io",
      "polygon-mainnet.infura.io",
      "optimism-mainnet.infura.io",
      "arbitrum-mainnet.infura.io",
      "notification.api.cx.metamask.io",
      "static.cx.metamask.io",
      "www.google-analytics.com",
      "js.hs-banner.com",
      "snap.licdn.com",
      "track.hubspot.com",
      "geolocation.api.cx.metamask.io",
      "client-config.api.cx.metamask.io",
      "tokens.api.cx.metamask.io",
      "metamask.github.io",
      "gateway.api.cx.metamask.io",
      "token.api.cx.metamask.io",
      "ad.doubleclick.net",
      "js.hs-scripts.com",
      "js.hsadspixel.net",
      "js.hs-analytics.net",
      "js.hubspot.com",
      "fonts.googleapis.com",
      "api.hubapi.com",
      "cta-service-cms2.hubspot.com",
      "staking.api.cx.metamask.io",
      "www.linkedin.com",
      "content-autofill.googleapis.com",
      "perf-na1.hsforms.com",
      "oidc.api.cx.metamask.io",
      "token.safebrowsing.apple"
    ],
    "note": "After the switch, 2 eth_chainId calls still hit mainnet.infura.io with a different Infura key than MetaMask's, from an origin-null page (likely the sell provider's embed). A wallet RPC setting can't reach that (run 20260924-214902)."
  }
}
