# Avito

> Чтение объявлений, фильтров, категорий, локаций и отзывов продавцов на Avito через локальный CLI `avito`. Используй только если пользователь прямо упоминает Авито / Avito или даёт ссылку на avito.ru.

- Skill: `sherfold/avito` (Agent Skill)
- Install (CLI): `npx skillmds@latest add sherfold/avito`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sherfold/avito/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: SherfoLD (https://skillmd.com/u/sherfold)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/sherfold/avito

---


# Avito

CLI `avito` читает Avito через браузер пользователя. Десять команд, все read-only.
Команда печатает один JSON-объект в stdout. Отказ печатается в stderr и в exit code.

Вызывай только CLI. Не открывай страницы сам. Не собирай URL вручную: URL объявления
и `searchUrl` бери из ответа команды.

## Команды

| Команда | Вход | Ответ |
|---|---|---|
| `search <query>` | текст запроса | первую страницу выдачи и `searchUrl`. Гео задаётся здесь: `--location-id`, `--metro`, `--district`, `--coords`, `--radius` |
| `get-page <searchUrl> --page <n>` | `searchUrl` | другую страницу той же выдачи |
| `get-filters <searchUrl>` | `searchUrl` | все фильтры этой выдачи, их значения и что уже применено |
| `apply-filters <searchUrl> --set <k=v;k=v>` | `searchUrl` | суженную выдачу и новый `searchUrl` |
| `get-categories <searchUrl>` | `searchUrl` | дерево категорий и куда можно перейти |
| `move-category <searchUrl> --to <name>` | `searchUrl` | ту же выдачу в другой категории |
| `get-item <url>` | URL объявления | полный текст, `attributes`, прайс-лист, фотографии файлами |
| `get-seller-reviews <itemUrl>` | URL объявления | ленту отзывов продавца |
| `get-location <query>` | название города или региона | `locationId` для `search`, ID метро и районов |
| `get-coords <address>` | адрес | координаты для `--coords` |

`searchUrl` — единственный носитель состояния. Бери его из ответа предыдущей команды
и передавай в следующую.

## Флоу

Подразумевай, что пользователь ждёт не список выдачи, а несколько оптимальных
товаров, которые ты выбрал и проверил сам. Выдача — материал для отбора, а не ответ.

Шаги 1–3 обязательны и выполняются по порядку.

**1. Браузер.** Выполни `avito browser`.

- Endpoint доступен — иди дальше.
- Кандидаты перечислены, ни один не запомнен — покажи список пользователю, спроси,
  каким браузером читать Avito, выполни `avito browser use --profile <dir>`.
- Список пуст — попроси пользователя включить отладку на
  `chrome://inspect/#remote-debugging` в том браузере, которым он пользуется.
  Повтори `avito browser`.

Не запускай браузер сам. Не указывай на новый профиль: Avito отказывает профилю без
истории. Первая команда сессии просит у пользователя одобрение соединения — один раз
на сессию.

**2. `--help`.** Перед первым вызовом каждой команды в сессии выполни
`avito <command> --help`. Он печатает точный тип ответа и все аргументы. Не угадывай
имена полей и флагов.

**3. Каталог и файлы.** Создай свой временный каталог: `mkdir -p <tmp>`. Ниже
`<tmp>` — он.

Следующие ответы всегда пиши в отдельные файлы, а решения принимай по `jq` из них:

| Команды | Почему файл обязателен |
|---|---|
| `search`, `get-page`, `apply-filters`, `move-category` | до 50 карточек; тот же ответ читается несколькими срезами |
| `get-filters` | ключи, значения и зависимости фильтров нужны на следующих шагах |
| `get-categories` | дерево нужно сравнить с текущей категорией перед переходом |

Один вызов — один файл. Не повторяй вызов ради другого формата: снова прочитай файл.
Не применяй `head` к JSON. При отказе файл пустой, а сообщение уходит в stderr;
проверяй exit code. `get-item`, `get-seller-reviews`, `get-location` и `get-coords`
можно читать из stdout.

**4. Настройка выдачи.** Пройди этот цикл до отбора карточек.

**4.1. Гео и текст запроса.** Сначала задай гео, если оно нужно:

| Условие | Начало цепочки |
|---|---|
| город не назван | `search <query>` |
| назван город | `get-location <город>` → `search <query> --location-id <id>` |
| у метро или в районе | `get-location <город> --geo metro` → `search <query> --metro <ids>` |
| рядом с адресом | `get-coords <адрес>` → `search <query> --coords <lat,lon> --radius <км>` |

Для каждого ограничения пользователя реши, где продавец вероятнее его указал:

- конкретное название, модель или свойство, которое обычно пишут словами в заголовке
  или тексте карточки, оставь в `query`;
- диапазон или несколько допустимых значений одного свойства не перечисляй серией
  запросов: оставь в `query` общий предмет покупки, но не шире нужной категории, а
  варианты задай фильтром;
- если не уверен, сделай один разумный широкий запрос и сначала проверь фильтры и
  категории. Не запускай соседние варианты `search`, пока не прошёл пункты 4.2–4.4.

Первый ответ сохрани и сразу проверь минимальный конверт:

```sh
avito search "ddr4 32gb" --location-id 637640 > <tmp>/search.json
jq '{query, category, searchUrl, itemsCount, medianPrice}' <tmp>/search.json
```

**4.2. Фильтры обязательны.** После `search` и после каждого `move-category` всегда
вызывай `get-filters` на новом `searchUrl`, даже если ожидаешь отбирать только по
словам. Сохрани ответ и найди фильтры по названиям и подписям значений:

```sh
avito get-filters "<searchUrl>" > <tmp>/filters-1.json
jq --arg words 'Тип накопителя|Форм-фактор|Объ[её]м' '
  {searchUrl, filters: [.filters[]
    | select((.name + " " + ([.options[]] | join(" "))) | test($words; "i"))
    | {key, name, unit, valueSyntax, currentValue,
       changesFiltersOnSelect, options}]}' <tmp>/filters-1.json
```

В проекции обязательны `key`, `name`, `valueSyntax`, `currentValue`,
`changesFiltersOnSelect` и `options`: без них нельзя ни составить `--set`, ни понять,
что словарь ещё может перестроиться. `unit` нужен для диапазонов. Ключи и значения
копируй дословно; подписи из `options` нельзя передавать вместо их ключей.

- Нужные ключи и значения есть → запомни полный набор отборочных значений и иди к
  пункту 4.5.
- Нужного ключа или значения нет → проверь зависимость формы в пункте 4.3.

**4.3. Может ли нужный фильтр появиться?** Покажи все фильтры, которые перестраивают
форму:

```sh
jq '{filters: [.filters[] | select(.changesFiltersOnSelect)
  | {key, name, unit, valueSyntax, currentValue,
     changesFiltersOnSelect, options}]}' <tmp>/filters-1.json
```

Если один из них по смыслу является родителем недостающего свойства, примени его
точное значение, сохрани ответ, затем заново сохрани `get-filters` от нового
`searchUrl` и повтори проекцию из 4.2. Например, выбор «Типа накопителя» может
показать «Форм-фактор», причём с разными `options` для SSD и HDD. Не перебирай
несвязанные значения вслепую.

```sh
avito apply-filters "<searchUrl>" \
  --set 'params[родитель]=<value>' > <tmp>/after-parent.json
jq '{query, searchUrl, itemsCount, medianPrice}' <tmp>/after-parent.json
avito get-filters "<newSearchUrl>" > <tmp>/filters-2.json
```

Передавай в следующий `apply-filters` последний `searchUrl`: фильтры с другими
ключами, уже применённые к этому URL, сохранятся. Повторный `key=...` заменяет только
всё значение **того же ключа**, а не дополняет его. Поэтому несколько допустимых
значений одного фильтра передавай сразу через запятую (`key=a,b`), а для снятия
фильтра используй `key=`. Если ни один `changesFiltersOnSelect: true` не может по
смыслу открыть недостающее свойство, иди к пункту 4.4.

**4.4. Категория.** Если фильтров недостаточно или они не соответствуют предмету,
проверь, куда Avito поместил запрос и есть ли подходящая более узкая категория:

```sh
avito get-categories "<searchUrl>" > <tmp>/categories.json
jq '{query, searchUrl, categories: [.categories[] |
  {role, name, depth, parent, current, hasChildren, navigable,
   preservesQuery}]}' <tmp>/categories.json
```

- Есть правильная более узкая или соседняя категория с `navigable: true` и
  `preservesQuery` не `false` → перейди по её точному `name`, сохрани результат и
  проверь конверт. Затем вернись к 4.2: фильтры прежней категории недействительны.
- Текущая категория правильная и полезнее перейти некуда → отсутствие фильтра
  окончательно. Оставь это ограничение для проверки карточек и `get-item`, затем иди
  к 4.5.

```sh
avito move-category "<searchUrl>" --to '<name>' > <tmp>/moved.json
jq '{query, category, searchUrl, itemsCount, medianPrice}' <tmp>/moved.json
```

**4.5. Зафиксируй выдачу.** Примени все найденные ограничения одним вызовом без
сортировки и сохрани его ответ как `<defaultFile>`. Если применять нечего,
`<defaultFile>` — уже сохранённый последний ответ `search` или `move-category`.
Сохрани его `searchUrl`: от него строятся сортировки в пункте 5. Затем ещё раз
сохрани `get-filters` от этого окончательного URL как `filters-final.json`: это
актуальный снимок формы и применённых значений. Категория и фильтры должны быть
определены до страниц: `apply-filters` и `move-category` принимают только страницу 1
и отказывают URL с `p=<n>`.

```sh
avito apply-filters "<searchUrl>" \
  --set '<key=value;key=value>' > <tmp>/default.json
jq '{query, searchUrl, itemsCount, medianPrice}' <tmp>/default.json
avito get-filters "<defaultSearchUrl>" > <tmp>/filters-final.json
```

**5. Отбор.** Кандидатов выбираешь ты, а не пользователь из списка. Сначала найди
нужную подпись сортировки в последнем файле `get-filters` и возьми соответствующий
ключ значения из `options`; саму подпись в `--set` не передавай:

```sh
jq '.filters[] | select(.key == "sort") |
  {key, name, valueSyntax, currentValue, changesFiltersOnSelect, options}' \
  <tmp>/filters-final.json
```

- **Пользователь не задал порядок.** Сначала прочитай первые 25 карточек выдачи по
  умолчанию. Затем примени к её `searchUrl` только точное значение сортировки «по
  дате»: остальные фильтры уже несёт URL. Сохрани второй файл и прочитай его первые
  25.
- **Пользователь просил дешевле.** Примени точное значение сортировки по возрастанию
  цены и в том же вызове нижнюю границу цены: разумная минимальная цена товара × 0.8.
- **Пользователь задал другой порядок.** Возьми соответствующее значение из
  `sort.options`, примени к текущему `searchUrl` и начни с первых 25.

```sh
avito apply-filters "<defaultSearchUrl>" \
  --set 'sort=<значение по дате>' > <tmp>/by-date.json
jq '{query, searchUrl, itemsCount, medianPrice}' <tmp>/by-date.json

jq '.items[:25] | map({title, price, minPrice, hasPriceList, published,
  location, url, descriptionPreview})' <defaultFile>
jq '.items[:25] | map({title, price, minPrice, hasPriceList, published,
  location, url, descriptionPreview})' <tmp>/by-date.json
```

Выбери интересные или непонятные карточки, убери дубли по `url` между сортировками и
открой каждую через `get-item`. Если подходящих вариантов мало, только тогда прочитай
`items[25:50]` из каждого уже сохранённого файла тем же набором полей. Лишь если и
эти четыре среза не дали достаточно кандидатов, используй `get-page` на нужном
финальном `searchUrl`, сохрани ответ в новый файл, проверь
`{query, page, searchUrl, itemsCount, medianPrice}` и продолжи тот же отбор.

## Особенности CLI

- **Карточка — не объявление.** `descriptionPreview` обрезан самим Avito,
  `imageCount` — число фотографий, а не фотографии. Полный текст и таблица
  характеристик `attributes` есть только в `get-item`.
- **`query` и `searchUrl` говорят, что ты получил.** Avito канонизирует запрос в
  маршрут категории. `query: null` значит, что текст растворился в категории и
  совпадение по словам не гарантировано. `category` в ответе `search` — категория, в
  которую Avito отнёс запрос; две выдачи из разных категорий несравнимы.
- **`price` — то число, которое Avito напечатал на карточке**, вместе со скидкой или
  бонусом, если он их дал. `minPrice` вместо `price` значит «от …» — это нижняя
  граница, а не цена. `hasPriceList: true` значит таблицу цен; она приходит как
  `priceList` в `get-item`. Оба `null` — цена договорная. `price: 0` — бесплатно.
- **`published` — точный момент, ISO 8601, UTC.** `publishedText` в `get-item` — строка
  Avito без года и секунд.
- **Фотографии.** Ни одна команда не возвращает фотографию в JSON.
  `get-item <url> --images-dir <абсолютный существующий каталог>` создаёт
  `<dir>/<itemId>/` и пишет туда `01.jpg`, `02.jpg` … в порядке галереи, а пути
  кладёт в `images`. Сначала прочитай текст, потом скачивай фотографии одного-двух
  кандидатов. Если хотя бы один файл не скачался, падает весь вызов; повтори команду
  без `--images-dir`.
- **Фильтры.** Ключи и значения бери из `get-filters` дословно; `valueSyntax` говорит,
  что писать после `key=`. `;` разделяет фильтры, `,` — значения одного фильтра, `..` —
  диапазон, пустое значение очищает фильтр. Новый ключ добавляется к фильтрам во
  входном `searchUrl`; повтор того же ключа заменяет всё его значение. После
  `move-category` перечитай `get-filters` — ключи прежней категории недействительны.
- **`changesFiltersOnSelect: true`** — этот фильтр перестраивает форму: после его
  применения набор фильтров другой. Пример: на жёстких дисках «Форм-фактора» нет,
  пока не выбран «Тип накопителя», и словарь у него разный для SSD и HDD. Если
  применил такой фильтр и нужного ключа не хватало — вызови `get-filters` на новом
  `searchUrl` ещё раз, там он и появится. Список может и сократиться, так что
  прежние ключи тоже перечитывай.
- **Между запросами держится пауза.** Случайная, от 1 до 2,5 секунд, и она общая для
  всех команд: интервал считается от последнего запроса всей машины, а не текущего
  вызова. Поэтому команда может начаться с ожидания, а цепочка из нескольких команд
  идёт небыстро. Это не зависание — не прерывай команду и не запускай команды
  параллельно, чтобы ускориться: параллельные вызовы встают в ту же очередь.
- **`--remove-reserved`** убирает зарезервированные объявления и действует на один
  вызов. Передавай его каждой команде цепочки. Страница приходит короче, и что именно
  убрано, не сообщается.

## Особенности Avito

Регион задаёт `search --location-id`. Состав выдачи меняют два фильтра, оба
применяются через `apply-filters`: `localPriority` («Сначала ближайшие», значение `1`)
и `d` (Авито Доставка).

| `localPriority` | `d` | Что в выдаче |
|---|---|---|
| не задан | не задан | объявления региона запроса **и** объявления других регионов с Авито Доставкой |
| `1` | не задан | только объявления региона запроса |
| не задан | задан | только объявления с Авито Доставкой, из всех регионов; регион запроса не ограничивает |
| `1` | задан | только объявления региона запроса, у которых есть Авито Доставка |

Отсюда следует: по умолчанию выдача не ограничена одним городом. `locationName` в
конверте называет регион запроса, а не регион каждого объявления. Если пользователю
нужен самовывоз в своём городе, применяй `localPriority=1`.

## Правила покупки

- **Ходовой товар, висящий давно, — ред флаг.** Считай возраст по `published`.
  Ликвидный товар по рыночной цене уходит за дни. Возраст в недели значит, что
  объявление смотрели и не взяли: дефект, завышенная цена или продавец не отвечает.
  Скажи об этом пользователю.
- **Покупка нового товара — читай отзывы.** Прежде чем рекомендовать объявление,
  выполни `get-seller-reviews`. Смотри `score`, `stage` и текст, а не только среднее.
  Смотри много ли отзывов на похожие товары или они накручены продажой мелочи.
- **Частный продавец продает новую вещь как БУ.** Обычно люди не продают только что выпущенные
  товары без причин - ищи в полном тексте причину продажи
- **Цена сильно ниже прочих — причина открыть `get-item`, а не вывод.** Ищи в полном
  тексте «нюанс», «на запчасти», и т.д.
- **Сравнивай сравнимое.** Не сравнивай цены между категориями и не сравнивай
  объявления с `hasPriceList: true` по числу на карточке.

## Отказы

Отказ печатается в stderr как `CODE: message`, иногда со строкой `hint:`. Машиночитаемая
часть — exit code.

| Exit | Code | Что произошло | Что делать |
|---|---|---|---|
| 2 | `ARGUMENT` | аргумент не принят, запроса в сеть не было | исправь аргумент; сообщение называет ограничение |
| 66 | `EMPTY_RESULT` | запрос выполнен, возвращать нечего | это ответ; сообщение называет вид пустоты |
| 1 | `COMMAND_EXEC` | ответ пришёл, но ему нельзя верить: HTTP-отказ, изменившаяся форма ответа, невыполненное постусловие | остановись и сообщи. Не повторяй |
| 75 | `TIMEOUT` | ответа не дождались | сообщи |
| 77 | `ACCESS` | Avito ответил не данными: лимит запросов, страница проверки, страница без состояния | передай пользователю |

77 не чинится повтором. Не взаимодействуй с CAPTCHA. Не ищи обходной маршрут к тем же
данным. Попроси пользователя открыть www.avito.ru в том же браузере и посмотреть, что
тот требует.

`could not reach the browser: …` значит, что CLI не дошёл до сайта. Выполни
`avito session status` и передай результат пользователю: закрытый браузер, выключенная
отладка или неодобренное соединение — это его действие, а не повод повторять команду.

