Guest User

Untitled

a guest
May 29th, 2026
148
0
Never
6
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 23.39 KB | Source Code | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. NYT Cooking recipe box exporter.
  4.  
  5. Fetches all saved recipes from a user's recipe box via the NYT Cooking
  6. internal search API and writes them to JSON and CSV.
  7.  
  8. The module is structured in four layers:
  9.  
  10.    1. ClientConfig   — immutable runtime configuration, sourced from the
  11.                        environment.
  12.    2. Domain model   — CookingTime and Recipe dataclasses that represent
  13.                        the canonical in-memory form of a recipe; all raw
  14.                        API coercion is confined to their `from_*` class
  15.                        methods.
  16.    3. NYTCookingClient — a thin, stateful HTTP client that owns the
  17.                          requests.Session and exposes a single generator
  18.                          interface over the paginated API.
  19.    4. ExportResult   — a value object that holds the collected recipes and
  20.                        knows how to serialise them to disk.
  21.  
  22. Usage:
  23.    NYT_S_COOKIE=<cookie> NYT_USER_ID=<uid> python nyt_recipe_box.py
  24.    NYT_S_COOKIE=<cookie> NYT_USER_ID=<uid> python nyt_recipe_box.py --out-dir ./exports
  25.  
  26. Environment variables:
  27.    NYT_USER_ID   Numeric NYT user ID (required).
  28.    NYT_S_COOKIE  Value of the NYT-S session cookie (required).
  29. """
  30.  
  31. from __future__ import annotations
  32.  
  33. import argparse
  34. import csv
  35. import json
  36. import logging
  37. import os
  38. import time
  39. from dataclasses import asdict, dataclass, field
  40. from pathlib import Path
  41. from typing import Iterator, Optional
  42. from urllib.parse import urljoin
  43.  
  44. from requests import Session
  45.  
  46. log: logging.Logger = logging.getLogger(__name__)
  47.  
  48.  
  49. # ---------------------------------------------------------------------------
  50. # Module-level constants
  51. # ---------------------------------------------------------------------------
  52.  
  53. # HTTP headers that mimic a standard browser session against the NYT Cooking
  54. # frontend.  The x-cooking-api header is required by the API gateway.
  55. _DEFAULT_HEADERS: dict[str, str] = {
  56.     "accept": "*/*",
  57.     "accept-language": "en-US,en;q=0.9",
  58.     "content-type": "application/json",
  59.     "user-agent": (
  60.         "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
  61.         "AppleWebKit/537.36 (KHTML, like Gecko) "
  62.         "Chrome/147.0.0.0 Safari/537.36"
  63.     ),
  64.     "x-cooking-api": "cooking-frontend",
  65. }
  66.  
  67. # Image crop variants requested from the API.  These are passed verbatim in
  68. # the `include_crops` query parameter and control which image URLs are
  69. # embedded in each recipe object.  They are not used by this exporter but are
  70. # included so the raw payload matches what the browser receives.
  71. _CROP_SIZES: tuple[str, ...] = (
  72.     "ipad_mediumThreeByTwo440",
  73.     "card",
  74.     "mediumThreeByTwo440",
  75.     "mediumThreeByTwo252",
  76. )
  77.  
  78. # Upper bound on the page number used in iter_saved_recipes to guard against
  79. # an infinite loop if the API never returns an empty page and the
  80. # collectables_count field is absent.
  81. _MAX_PAGES: int = 10_000
  82.  
  83.  
  84. # ---------------------------------------------------------------------------
  85. # Configuration
  86. # ---------------------------------------------------------------------------
  87.  
  88. @dataclass(frozen=True)
  89. class ClientConfig:
  90.     """
  91.    Immutable runtime configuration for NYTCookingClient.
  92.  
  93.    All fields that originate from external sources (environment variables,
  94.    CLI flags) are collected here so that the rest of the module can treat
  95.    them as constants.  Using a frozen dataclass rather than a plain dict or
  96.    module-level globals makes it trivial to pass different configurations in
  97.    tests without monkey-patching.
  98.  
  99.    Attributes:
  100.        user_id:        Numeric NYT user ID, as a string.
  101.        nyt_s_cookie:   Value of the NYT-S session cookie used for
  102.                        authentication.
  103.        per_page:       Number of recipes to request per API call.  The
  104.                        maximum accepted by the endpoint is 48.
  105.        request_delay:  Seconds to sleep between successive page requests to
  106.                        avoid hammering the API.
  107.        timeout:        HTTP request timeout in seconds.
  108.        base_url:       Root URL of the NYT Cooking site.  Exposed as a
  109.                        field rather than a hard-coded string so that tests
  110.                        can point the client at a local stub server.
  111.    """
  112.  
  113.     user_id: str
  114.     nyt_s_cookie: str
  115.     per_page: int = 48
  116.     request_delay: float = 0.5
  117.     timeout: int = 30
  118.     base_url: str = "https://cooking.nytimes.com"
  119.  
  120.     @classmethod
  121.     def from_env(cls) -> "ClientConfig":
  122.         """
  123.        Construct a ClientConfig from environment variables.
  124.  
  125.        Reads NYT_USER_ID and NYT_S_COOKIE from the process environment.
  126.        Both variables must be non-empty; if either is absent an
  127.        EnvironmentError is raised so that the caller can surface a clear
  128.        diagnostic rather than receiving a cryptic HTTP 401.
  129.  
  130.        Returns:
  131.            A fully populated ClientConfig instance.
  132.  
  133.        Raises:
  134.            EnvironmentError: If NYT_USER_ID or NYT_S_COOKIE is unset or
  135.                              blank.
  136.        """
  137.         user_id: str = os.environ.get("NYT_USER_ID", "").strip()
  138.         cookie: str = os.environ.get("NYT_S_COOKIE", "").strip()
  139.         if not user_id:
  140.             raise EnvironmentError("NYT_USER_ID is not set")
  141.         if not cookie:
  142.             raise EnvironmentError("NYT_S_COOKIE is not set")
  143.         return cls(user_id=user_id, nyt_s_cookie=cookie)
  144.  
  145.  
  146. # ---------------------------------------------------------------------------
  147. # Domain model
  148. # ---------------------------------------------------------------------------
  149.  
  150. @dataclass
  151. class CookingTime:
  152.     """
  153.    Human-readable and machine-readable cooking duration for a recipe.
  154.  
  155.    The NYT API returns cooking time as a nested object with both a
  156.    display string (e.g. "45 minutes") and an integer minute count.
  157.    Separating them here preserves both representations without lossy
  158.    conversion.
  159.  
  160.    Attributes:
  161.        display: Human-readable duration string as returned by the API
  162.                 (e.g. "1 hour 15 minutes").  Empty string if absent.
  163.        minutes: Total cooking time in minutes, or None if the API did
  164.                 not provide a numeric value.
  165.    """
  166.  
  167.     display: str = ""
  168.     minutes: Optional[int] = None
  169.  
  170.     @classmethod
  171.     def from_dict(cls, d: Optional[dict[str, object]]) -> "CookingTime":
  172.         """
  173.        Construct a CookingTime from the nested API dict.
  174.  
  175.        Accepts None (the field is sometimes absent from the API response)
  176.        and returns a zero-value instance in that case rather than
  177.        propagating None to callers.
  178.  
  179.        Args:
  180.            d: The raw dict at the `cooking_time` key of a recipe object,
  181.               or None if the key was missing.
  182.  
  183.        Returns:
  184.            A CookingTime populated from the dict, or a default instance
  185.            with empty/None fields if d is falsy.
  186.        """
  187.         if not d:
  188.             return cls()
  189.         return cls(
  190.             display=str(d.get("display", "")),
  191.             minutes=d.get("minutes"),  # type: ignore[arg-type]
  192.         )
  193.  
  194.  
  195. @dataclass
  196. class Recipe:
  197.     """
  198.    Canonical in-memory representation of a single NYT Cooking recipe.
  199.  
  200.    This is the domain type used throughout the rest of the module.  All
  201.    contact with raw API dicts is confined to `from_api`; downstream code
  202.    works exclusively with typed Recipe instances.
  203.  
  204.    Attributes:
  205.        id:              Unique integer recipe identifier assigned by NYT.
  206.        name:            Recipe title, whitespace-normalised.
  207.        byline:          Author credit line (e.g. "By Melissa Clark").
  208.        url:             Canonical URL of the recipe on cooking.nytimes.com.
  209.        yield_:          Serving size description (e.g. "4 servings").
  210.                         Trailing underscore avoids shadowing the built-in.
  211.        cooking_time:    Structured cooking duration.
  212.        kicker:          Editorial label attached to the recipe
  213.                         (e.g. "easy", "healthy").  Empty string if absent.
  214.        avg_rating:      Mean star rating across all user submissions, or
  215.                         None if no ratings exist yet.
  216.        num_ratings:     Total number of ratings submitted, or None if
  217.                         the field is absent.
  218.        has_video:       True if the recipe has an associated video.
  219.        published_at_ms: Unix timestamp in milliseconds of the original
  220.                         publication date, or None if absent.
  221.        image_credit:    Photographer or illustrator credit for the lead
  222.                         image.  Empty string if absent.
  223.    """
  224.  
  225.     id: int
  226.     name: str
  227.     byline: str
  228.     url: str
  229.     yield_: str
  230.     cooking_time: CookingTime
  231.     kicker: str
  232.     avg_rating: Optional[float]
  233.     num_ratings: Optional[int]
  234.     has_video: bool
  235.     published_at_ms: Optional[int]
  236.     image_credit: str
  237.  
  238.     @classmethod
  239.     def from_api(cls, r: dict[str, object]) -> "Recipe":
  240.         """
  241.        Construct a Recipe from a raw API response dict.
  242.  
  243.        This is the single point at which untyped API data is coerced into
  244.        the domain model.  Any field that the API may omit is handled here
  245.        with an explicit default so that callers never receive a KeyError or
  246.        a None where the type annotation promises a str.
  247.  
  248.        Args:
  249.            r: A single element from the `collectables` list in an API
  250.               response payload.
  251.  
  252.        Returns:
  253.            A fully populated Recipe instance.
  254.  
  255.        Raises:
  256.            KeyError: If the mandatory `id` field is absent from r, which
  257.                      would indicate a malformed API response.
  258.        """
  259.         return cls(
  260.             id=int(r["id"]),
  261.             name=(str(r.get("name") or "")).strip(),
  262.             byline=str(r.get("byline", "")),
  263.             url=str(r.get("url", "")),
  264.             yield_=str(r.get("yield", "")),
  265.             cooking_time=CookingTime.from_dict(
  266.                 r.get("cooking_time")  # type: ignore[arg-type]
  267.             ),
  268.             kicker=str((r.get("kicker") or {}).get("name", "")),  # type: ignore[union-attr]
  269.             avg_rating=float(r["avg_rating"]) if r.get("avg_rating") is not None else None,
  270.             num_ratings=int(r["num_ratings"]) if r.get("num_ratings") is not None else None,
  271.             has_video=bool(r.get("has_video", False)),
  272.             published_at_ms=int(r["published_at"]) if r.get("published_at") is not None else None,
  273.             image_credit=str((r.get("image") or {}).get("credit", "")),  # type: ignore[union-attr]
  274.         )
  275.  
  276.     def to_flat_dict(self) -> dict[str, object]:
  277.         """
  278.        Return a flat dict representation suitable for CSV serialisation.
  279.  
  280.        CSV rows must be flat key/value mappings; the nested CookingTime is
  281.        expanded into two prefixed columns.  Column names deliberately use
  282.        underscores rather than dots so they import cleanly into spreadsheet
  283.        tools and SQL without quoting.
  284.  
  285.        Returns:
  286.            An ordered dict whose keys form the CSV header row and whose
  287.            values are all JSON-serialisable scalars.
  288.        """
  289.         return {
  290.             "id": self.id,
  291.             "name": self.name,
  292.             "byline": self.byline,
  293.             "url": self.url,
  294.             "yield": self.yield_,
  295.             "cooking_time_display": self.cooking_time.display,
  296.             "cooking_time_minutes": self.cooking_time.minutes,
  297.             "kicker": self.kicker,
  298.             "avg_rating": self.avg_rating,
  299.             "num_ratings": self.num_ratings,
  300.             "has_video": self.has_video,
  301.             "published_at_ms": self.published_at_ms,
  302.             "image_credit": self.image_credit,
  303.         }
  304.  
  305.  
  306. # ---------------------------------------------------------------------------
  307. # API client
  308. # ---------------------------------------------------------------------------
  309.  
  310. class NYTCookingClient:
  311.     """
  312.    Thin HTTP client for the NYT Cooking recipe-box search endpoint.
  313.  
  314.    Owns a single requests.Session for connection pooling and cookie
  315.    persistence across pages.  The public interface is intentionally narrow:
  316.    callers should use `iter_saved_recipes` and treat the remaining methods
  317.    as implementation details.
  318.  
  319.    The client does not perform any domain-model construction; it yields raw
  320.    dicts so that the caller decides how to map them.
  321.    """
  322.  
  323.     def __init__(self, config: ClientConfig) -> None:
  324.         """
  325.        Initialise the client and build the underlying HTTP session.
  326.  
  327.        Args:
  328.            config: Fully populated ClientConfig instance.  All HTTP
  329.                    parameters are derived from this object; it is stored
  330.                    as an attribute so that `_fetch_page` can reference it
  331.                    without additional arguments.
  332.        """
  333.         self._config: ClientConfig = config
  334.         self._session: Session = self._build_session()
  335.         # Construct the endpoint URL once at initialisation time rather than
  336.         # on every request; config is frozen so this is safe.
  337.         self._endpoint: str = (
  338.             f"{config.base_url}/api/v2/users/{config.user_id}"
  339.             "/search/recipe_box_search"
  340.         )
  341.  
  342.     def _build_session(self) -> Session:
  343.         """
  344.        Construct and configure a requests.Session for this client.
  345.  
  346.        Sets browser-compatible headers on the session-level default headers
  347.        dict so that they are sent with every request automatically.  The
  348.        NYT-S authentication cookie is attached to the .nytimes.com domain so
  349.        that it is scoped correctly and not leaked to other hosts.
  350.  
  351.        Returns:
  352.            A configured requests.Session ready for use.
  353.        """
  354.         s: Session = Session()
  355.         s.headers.update(_DEFAULT_HEADERS)
  356.         # origin and referer must match the Cooking frontend or the API
  357.         # gateway returns 403.
  358.         s.headers["origin"] = self._config.base_url
  359.         s.headers["referer"] = urljoin(self._config.base_url, "/recipe-box")
  360.         s.cookies.set("NYT-S", self._config.nyt_s_cookie, domain=".nytimes.com")
  361.         return s
  362.  
  363.     def _fetch_page(self, page: int) -> dict[str, object]:
  364.         """
  365.        Request a single page of recipe-box results from the API.
  366.  
  367.        Args:
  368.            page: 1-based page number to request.
  369.  
  370.        Returns:
  371.            The decoded JSON payload as a dict.  The relevant keys are
  372.            `collectables` (list of recipe dicts) and `collectables_count`
  373.            (total recipe count across all pages).
  374.  
  375.        Raises:
  376.            requests.HTTPError: If the server returns a non-2xx status code.
  377.            requests.Timeout:   If the server does not respond within
  378.                                `config.timeout` seconds.
  379.        """
  380.         params: dict[str, object] = {
  381.             "q": "",
  382.             "per_page": self._config.per_page,
  383.             "page": page,
  384.             "include_crops": ",".join(_CROP_SIZES),
  385.         }
  386.         response = self._session.get(
  387.             self._endpoint,
  388.             params=params,
  389.             timeout=self._config.timeout,
  390.         )
  391.         response.raise_for_status()
  392.         return response.json()  # type: ignore[no-any-return]
  393.  
  394.     def iter_saved_recipes(self) -> Iterator[dict[str, object]]:
  395.         """
  396.        Yield raw recipe dicts from the API, paginating automatically.
  397.  
  398.        Fetches pages sequentially, sleeping `config.request_delay` seconds
  399.        between requests.  Iteration stops as soon as either of the following
  400.        conditions is met:
  401.  
  402.        * The API returns an empty `collectables` list (server-side sentinel).
  403.        * The number of yielded records reaches `collectables_count` as
  404.          declared by the first page response (client-side guard).
  405.  
  406.        A hard upper bound of _MAX_PAGES is applied to prevent an infinite
  407.        loop if the API behaves unexpectedly (e.g. collectables_count is
  408.        absent and the server never returns an empty page).
  409.  
  410.        Yields:
  411.            Raw dict objects as returned by the API for each recipe.  These
  412.            should be passed to Recipe.from_api() by the caller.
  413.        """
  414.         total: Optional[int] = None
  415.         yielded: int = 0
  416.  
  417.         for page in range(1, _MAX_PAGES + 1):
  418.             payload: dict[str, object] = self._fetch_page(page)
  419.             records: list[dict[str, object]] = payload.get("collectables") or []  # type: ignore[assignment]
  420.  
  421.             # collectables_count is only read from the first page; subsequent
  422.             # pages may return the same value but we do not need to re-check.
  423.             if total is None:
  424.                 total = payload.get("collectables_count")  # type: ignore[assignment]
  425.                 log.info("Recipe box contains %s recipes", total)
  426.  
  427.             log.debug("Page %d: received %d records", page, len(records))
  428.  
  429.             if not records:
  430.                 # The server has no more results.
  431.                 break
  432.  
  433.             yield from records
  434.             yielded += len(records)
  435.  
  436.             if total is not None and yielded >= total:
  437.                 # We have collected every record the server declared.
  438.                 break
  439.  
  440.             time.sleep(self._config.request_delay)
  441.  
  442.  
  443. # ---------------------------------------------------------------------------
  444. # Export
  445. # ---------------------------------------------------------------------------
  446.  
  447. @dataclass
  448. class ExportResult:
  449.     """
  450.    Value object that holds a completed collection of recipes and serialises
  451.    them to disk on demand.
  452.  
  453.    Keeping serialisation on this object rather than scattering write calls
  454.    through `export()` means the caller can inspect `recipes` before writing,
  455.    or write to a different location without re-fetching.
  456.  
  457.    Attributes:
  458.        recipes:   Ordered list of Recipe instances, in the same order they
  459.                   were returned by the API.
  460.        raw_pages: Optional list of raw API response payloads retained for
  461.                   debugging.  Not written to disk by `write()`; callers
  462.                   that need the raw data should serialise this field
  463.                   themselves.
  464.    """
  465.  
  466.     recipes: list[Recipe]
  467.     raw_pages: list[dict[str, object]] = field(default_factory=list)
  468.  
  469.     def write(self, out_dir: Path) -> None:
  470.         """
  471.        Serialise all recipes to JSON and CSV files inside `out_dir`.
  472.  
  473.        Creates `out_dir` and any missing parent directories.  Existing files
  474.        with the same names are silently overwritten.
  475.  
  476.        Args:
  477.            out_dir: Directory in which to write `recipe_box.json` and
  478.                     `recipe_box.csv`.
  479.        """
  480.         out_dir.mkdir(parents=True, exist_ok=True)
  481.         self._write_json(out_dir / "recipe_box.json")
  482.         self._write_csv(out_dir / "recipe_box.csv")
  483.         log.info("Wrote %d recipes to %s", len(self.recipes), out_dir)
  484.  
  485.     def _write_json(self, path: Path) -> None:
  486.         """
  487.        Write all recipes to a UTF-8 JSON file at `path`.
  488.  
  489.        Each recipe is serialised using dataclasses.asdict(), which recursively
  490.        converts nested dataclasses (e.g. CookingTime) to dicts.
  491.  
  492.        Args:
  493.            path: Destination file path.  Parent directory must already exist.
  494.        """
  495.         data: list[dict[str, object]] = [asdict(r) for r in self.recipes]
  496.         path.write_text(
  497.             json.dumps(data, indent=2, ensure_ascii=False),
  498.             encoding="utf-8",
  499.         )
  500.         log.debug("JSON → %s", path)
  501.  
  502.     def _write_csv(self, path: Path) -> None:
  503.         """
  504.        Write all recipes to a UTF-8 CSV file at `path`.
  505.  
  506.        Uses Recipe.to_flat_dict() to expand nested fields into top-level
  507.        columns.  The column order follows the key order of to_flat_dict(),
  508.        which is stable across Python 3.7+.  Does nothing if the recipe list
  509.        is empty, since a header-only CSV would be misleading.
  510.  
  511.        Args:
  512.            path: Destination file path.  Parent directory must already exist.
  513.        """
  514.         rows: list[dict[str, object]] = [r.to_flat_dict() for r in self.recipes]
  515.         if not rows:
  516.             log.warning("No recipes to write; skipping CSV output.")
  517.             return
  518.         with path.open("w", newline="", encoding="utf-8") as fh:
  519.             writer: csv.DictWriter[str] = csv.DictWriter(
  520.                 fh, fieldnames=list(rows[0].keys())
  521.             )
  522.             writer.writeheader()
  523.             writer.writerows(rows)
  524.         log.debug("CSV → %s", path)
  525.  
  526.  
  527. def export(config: ClientConfig, out_dir: Path) -> ExportResult:
  528.     """
  529.    Fetch all saved recipes and write them to `out_dir`.
  530.  
  531.    This is the primary entry point for programmatic use.  It composes
  532.    NYTCookingClient, Recipe.from_api(), and ExportResult.write() into a
  533.    single call so that callers do not need to assemble the pipeline
  534.    themselves.
  535.  
  536.    Args:
  537.        config:  Fully populated ClientConfig instance.
  538.        out_dir: Directory in which to write output files.
  539.  
  540.    Returns:
  541.        An ExportResult containing all fetched and parsed Recipe instances.
  542.        The files have already been written to out_dir by the time this
  543.        returns.
  544.    """
  545.     client: NYTCookingClient = NYTCookingClient(config)
  546.     recipes: list[Recipe] = [
  547.         Recipe.from_api(raw) for raw in client.iter_saved_recipes()
  548.     ]
  549.     result: ExportResult = ExportResult(recipes=recipes)
  550.     result.write(out_dir)
  551.     return result
  552.  
  553.  
  554. # ---------------------------------------------------------------------------
  555. # CLI entry point
  556. # ---------------------------------------------------------------------------
  557.  
  558. def _parse_args() -> Path:
  559.     """
  560.    Parse command-line arguments and return the output directory path.
  561.  
  562.    Defined as a separate function (rather than inline in main) so that it
  563.    can be called and tested independently of the rest of the startup
  564.    sequence.
  565.  
  566.    Returns:
  567.        A Path to the directory where output files should be written.
  568.        Defaults to the current working directory if --out-dir is not given.
  569.    """
  570.     parser: argparse.ArgumentParser = argparse.ArgumentParser(
  571.         description=__doc__,
  572.         formatter_class=argparse.RawDescriptionHelpFormatter,
  573.     )
  574.     parser.add_argument(
  575.         "--out-dir",
  576.         type=Path,
  577.         default=Path("."),
  578.         metavar="DIR",
  579.         help="Directory to write output files (default: current directory)",
  580.     )
  581.     return parser.parse_args().out_dir  # type: ignore[no-any-return]
  582.  
  583.  
  584. def main() -> None:
  585.     """
  586.    CLI entry point: configure logging, parse arguments, run the export.
  587.  
  588.    Exits with a non-zero status code and a human-readable message if
  589.    required environment variables are missing.  All other exceptions
  590.    (network errors, permission errors) propagate normally so that the
  591.    default Python traceback is shown; these represent unexpected conditions
  592.    that warrant inspection rather than a clean user-facing error.
  593.    """
  594.     logging.basicConfig(
  595.         level=logging.INFO,
  596.         format="%(asctime)s %(levelname)s %(name)s — %(message)s",
  597.         datefmt="%H:%M:%S",
  598.     )
  599.     try:
  600.         config: ClientConfig = ClientConfig.from_env()
  601.     except EnvironmentError as exc:
  602.         raise SystemExit(f"Configuration error: {exc}") from exc
  603.  
  604.     out_dir: Path = _parse_args()
  605.     result: ExportResult = export(config, out_dir)
  606.     print(f"Exported {len(result.recipes)} recipes → {out_dir.resolve()}")
  607.  
  608.  
  609. if __name__ == "__main__":
  610.     main()
Tags: python
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • Eftegeh
    13 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=toyfqlzkcg
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment