OmniSkin Wiki
RU EN
Войти Trade-Up Рынок Топ Инвентарь Folio Таблица арбитража →
Декодирование inspect-ссылок CS2 локально, без API

Декодирование inspect-ссылок CS2 локально, без API

С марта 2026 inspect-ссылка CS2 несёт предмет внутри себя. Разбор payload, карта полей protobuf, CRC и XOR-маска, побайтовый пример и рабочий декодер на Python из одной стандартной библиотеки.

Опубликовано 27.08.2026 · Обновлено 08.09.2026 · English

Короткий ответ: чтобы прочитать float и paint seed современной inspect-ссылки CS2, API больше не нужен. С марта 2026 ссылка несёт данные предмета внутри себя — шестнадцатеричный хвост после csgo_econ_action_preview это сериализованное protobuf-сообщение CEconItemPreviewDataBlock. Декодирование целиком локальное: развернуть конверт, проверить CRC (или снять XOR-маску), разобрать protobuf и переинтерпретировать одно поле uint32 как IEEE-754 float. Ни входа в Steam, ни бот-аккаунта, ни лимитов, ни единого сетевого запроса. api.csfloat.com как путь резолва больше не существует — домен даже не резолвится, а репозиторий csfloat/inspect заархивирован 25 марта 2026 с пометкой, отправляющей пользователей к библиотеке-сериализатору.

Оговорка, и она серьёзная: классические ссылки S/A/D по-прежнему не декодируются офлайн. Это указатели, а не payload, и для их резолва всё так же нужно подключение к game coordinator. Где именно проходит эта граница — ниже.


Что именно изменилось в марте 2026

До изменения inspect-ссылка выглядела как набор ключей к базе:

steam://rungame/730/76561202255233023/+csgo_econ_action_preview S76561198084749846A698323590D7935523998312483177

S — SteamID владельца (либо M, идентификатор лота на площадке), A — asset id, D — детерминированное «d-значение», доказательство. Ни один из этих байтов не говорит о float. Чтобы его узнать, нужно было залогинить Steam-бота в game coordinator CS2, отправить preview-запрос, дождаться ответа и надеяться, что GC жив. Ровно поэтому float-API вообще существовали: это были пулы бот-аккаунтов, каждый с потолком примерно в один запрос в секунду.

Новый формат переворачивает модель. Valve теперь отдаёт ссылки, хвост которых — чистый шестнадцатеричный код, и этот хвост и есть предмет:

steam://rungame/730/76561202255233023/+csgo_econ_action_preview 0010DAB7CD80A4011807209A02...

Всё, что нужно для превью — paint index, paint seed, износ, стикеры, чармы — едет в самом URL. Game coordinator из цепочки для таких ссылок исключён полностью.

Отличить одно от другого элементарно. Возьмите хвост после csgo_econ_action_preview. Если он целиком состоит из [0-9A-Fa-f] и имеет чётную длину — ссылка самокодирующаяся, декодируется локально. Если внутри есть буквы S, M, A или D как маркеры параметров — это классическая ссылка, и ей нужен GC. Эта единственная проверка и есть точка ветвления любого современного резолвера.


Анатомия самокодирующейся ссылки

Hex разворачивается в буфер байтов: небольшой конверт вокруг protobuf-сообщения.

ЧастьБайтыЧто это
Ключевой байтbuf[0]0x00 = payload без маски. Любое другое значение = маска, и это её XOR-ключ
Payloadbuf[1:-4]Сериализованное protobuf-сообщение CEconItemPreviewDataBlock
Контрольная суммаbuf[-4:]Big-endian uint32, производная от CRC32, проверяется на payload без маски

В природе встречаются два варианта:

  • Без маски — первый байт 0x00. Четыре хвостовых байта это контрольная сумма, которую стоит проверить. Ссылки, сгенерированные сторонними инструментами, обычно именно такие.
  • С маской — первый байт ненулевой. Каждый байт буфера, включая первый, проксорен с этим первым байтом. Проксорьте весь буфер значением buf[0] — и получите форму без маски (ключевой байт, проксоренный сам с собой, становится 0x00; это и есть инвариант, по которому вы подтверждаете, что сделали всё верно). Ссылки, которые генерирует сама игра, обычно приходят с маской.

Контрольная сумма — не обычный CRC32, а CRC32 со свёрткой по длине сверху. Референсная реализация в @csfloat/cs-inspect-serializer выглядит так:

export const getChecksum = (payload: Uint8Array): number => {
	const bufferPayload = Buffer.concat([Uint8Array.from([0]), payload]);
	const crc = CRC32.buf(bufferPayload);
	const x_crc = (crc & 0xffff) ^ (payload.byteLength * crc);
	return (x_crc & 0xffffffff) >>> 0;
};

Обратите внимание на две детали, которые легко пропустить: CRC считается по payload с предпосланным 0x00, а результат сворачивается как (crc & 0xffff) ^ (length * crc) и только потом обрезается до 32 бит.

Карта полей protobuf

Payload — это CEconItemPreviewDataBlock, описанный в cstrike15_gcmessages.proto и публично отслеживаемый репозиторием GameTracking-CS2 от SteamDB. Значение имеют номера полей: сообщение можно разобрать минимальным ридером, ни разу не тронув компилятор .proto.

#ПолеТипПримечания
1accountiduint32Account id владельца. В самокодирующихся ссылках часто отсутствует
2itemiduint64Asset id
3defindexuint32Тип оружия (7 = AK-47)
4paintindexuint32Paint kit / id скина (282 = Redline)
5rarityuint32Уровень редкости
6qualityuint32Уровень качества
7paintwearuint32Float в виде сырых битов IEEE-754 — см. ниже
8paintseeduint32Индекс паттерна
9killeaterscoretypeuint32Наличие означает счётчик StatTrak / Souvenir
10killeatervalueuint32Число убийств
11customnamestringName tag
12stickersrepeated StickerНаклеенные стикеры
13inventoryuint32Слот / корзина инвентаря
14originuint32Каким путём предмет получен
15questiduint32
16dropreasonuint32
17musicindexuint32Music kit
18entindexint32
19petindexuint32
20keychainsrepeated StickerЧармы — та же форма вложенного сообщения, что у стикеров
21styleuint32
22variationsrepeated Sticker
23upgrade_leveluint32

Вложенное сообщение Sticker, переиспользуемое для стикеров, чармов и вариаций:

#ПолеТип
1slotuint32
2sticker_iduint32
3wearfloat
4scalefloat
5rotationfloat
6tint_iduint32
7-9offset_x / offset_y / offset_zfloat
10patternuint32
11highlight_reeluint32
12wrapped_stickeruint32

paintwear — главная ловушка. Поле объявлено как uint32 и едет по проводу варинтом, но как число это целое бессмысленно: его 32 бита и есть float32. Биты нужно переинтерпретировать, а не приводить. В Python: struct.unpack("<f", struct.pack("<I", value))[0]. Приведение вместо переинтерпретации даёт значения в миллиардах и баг, который выглядит как повреждённые данные.


Разбор на практике: ссылка по байтам

Ниже полная самокодирующаяся ссылка. Каждое число получено реальным прогоном декодера, а не переписано из документации.

steam://rungame/730/76561202255233023/+csgo_econ_action_preview%200010DAB7CD80A4011807209A022805300438899F9BF3034095056205080010A82768838080800C7004E71B40F1
  1. Снять обёртку. URL-декодировать (%20 это пробел) и взять хвост после csgo_econ_action_preview. В нём 90 hex-символов, все [0-9A-F], длина чётная — самокодирование подтверждено.
  2. Hex в байты. 90 символов превращаются в 45 байт.
  3. Прочитать ключевой байт. buf[0] равен 0x00, значит payload без маски. XOR не нужен.
  4. Разделить конверт. Payload = байты с 1 по 40 (40 байт). Хвост = последние 4 байта, E7 1B 40 F1.
  5. Проверить контрольную сумму. CRC32 по 0x00 плюс payload, свёрнутый по формуле выше, даёт 0xE71B40F1 — ровно совпадает с хвостом. Payload цел.
  6. Разобрать protobuf. Идти парами тег/значение до конца буфера.

40 байт payload разбираются ровно в десять полей:

Сырые байтыПолеWireЗначение
10 DA B7 CD 80 A4 012 itemidvarint44024683482
18 073 defindexvarint7 (AK-47)
20 9A 024 paintindexvarint282 (Redline)
28 055 rarityvarint5
30 046 qualityvarint4
38 89 9F 9B F3 037 paintwearvarint1046925193 -> 0.22540106
40 95 058 paintseedvarint661
62 05 08 00 10 A8 2712 stickerslength-delimitedslot 0, sticker_id 5032
68 83 80 80 80 0C13 inventoryvarint3221225475
70 0414 originvarint4

Прочитайте один тег руками, чтобы увидеть механику. Байт 0x20 это 32 в десятичной; protobuf-тег упаковывает номер поля в старшие биты, а wire type — в три младших: 32 >> 3 даёт 4 (поле 4, paintindex), а 32 & 7 даёт 0 (varint). Следующий варинт 9A 02 — это little-endian по основанию 128: (0x9A & 0x7F) | (0x02 << 7) = 26 | 256 = 282. Redline.

Самое интересное — поле 7. Варинт 89 9F 9B F3 03 даёт целое 1046925193. Как число это бессмыслица. Переинтерпретированное как биты float32, оно равно 0.22540105879306793 — AK-47 | Redline в Field-Tested. Это и есть float, полученный без единого сетевого запроса.

Тот же предмет, замаскированный ключом 0x9E, — это тот же самый буфер, побайтово проксоренный:

csgo_econ_action_preview 9E8E4429531E3A9F8699BE049CB69BAE9AA61701056D9DDE0B9BFC9B969E8E36B9F61D1E1E1E92EE9A7985DE6F

Проксорьте каждый байт значением 0x9E, первый байт станет 0x00, и дальше декодирование идёт точно так же — тот же float, тот же сид.

🔧Проверить флоат и паттернбесплатно, без регистрации

Рабочий декодер примерно в 60 строк

Только стандартная библиотека — ни компилятора protobuf, ни зависимостей. Обрабатывает оба варианта payload, с маской и без.

import re, struct, zlib, urllib.parse

LINK = re.compile(r"csgo_econ_action_preview[\s+]*([0-9A-Fa-f]+)\s*$")

def read_varint(b, i):
    shift = val = 0
    while True:
        byte = b[i]; i += 1
        val |= (byte & 0x7F) << shift
        if not byte & 0x80:
            return val, i
        shift += 7
        if shift > 70:
            raise ValueError("varint too long")

def parse_pb(b):
    out, i, n = {}, 0, len(b)
    while i < n:
        key, i = read_varint(b, i)
        field, wt = key >> 3, key & 7
        if wt == 0:
            v, i = read_varint(b, i)
        elif wt == 5:
            v, i = b[i:i+4], i + 4
        elif wt == 2:
            ln, i = read_varint(b, i)
            v, i = b[i:i+ln], i + ln
            if len(v) < ln:
                raise ValueError("truncated field")
        else:
            raise ValueError(f"unsupported wire type {wt}")
        out.setdefault(field, []).append(v)
    return out

def checksum(payload):
    crc = zlib.crc32(b"\x00" + payload) & 0xFFFFFFFF
    return ((crc & 0xFFFF) ^ (len(payload) * crc)) & 0xFFFFFFFF

def unwrap(buf):
    if len(buf) < 5:
        raise ValueError("payload too short")
    if buf[0] != 0:                                  # с маской: XOR каждого байта ключевым байтом
        buf = bytes(x ^ buf[0] for x in buf)
    else:                                            # без маски: проверяем хвостовую контрольную сумму
        if checksum(buf[1:-4]) != struct.unpack(">I", buf[-4:])[0]:
            raise ValueError("checksum mismatch")
    return buf[1:-4]

def sticker(raw):
    pb = parse_pb(raw)
    f = lambda k: struct.unpack("<f", pb[k][-1])[0] if k in pb else None
    return {"slot": pb.get(1, [None])[-1], "sticker_id": pb.get(2, [None])[-1],
            "wear": f(3), "rotation": f(5), "pattern": pb.get(10, [None])[-1]}

def decode(link):
    m = LINK.search(urllib.parse.unquote(link.strip()))
    if not m:
        raise ValueError("not a self-encoding link (classic S/A/D links need the GC)")
    pb = parse_pb(unwrap(bytes.fromhex(m.group(1))))
    g = lambda k: pb[k][-1] if k in pb else None
    wear = g(7)
    return {
        "itemid":      g(2),
        "defindex":    g(3),
        "paint_index": g(4),
        "rarity":      g(5),
        "quality":     g(6),
        "float":       struct.unpack("<f", struct.pack("<I", wear))[0] if wear is not None else None,
        "paint_seed":  g(8),
        "killeater_type": g(9),      # присутствует => счётчик StatTrak / Souvenir
        "kill_count":  g(10),
        "custom_name": g(11).decode("utf-8") if g(11) else None,
        "stickers":    [sticker(x) for x in pb.get(12, [])],
        "keychains":   [sticker(x) for x in pb.get(20, [])],
    }

if __name__ == "__main__":
    link = ("steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20"
            "0010DAB7CD80A4011807209A022805300438899F9BF303409505"
            "6205080010A82768838080800C7004E71B40F1")
    for k, v in decode(link).items():
        print(f"{k:15} {v}")

Вывод:

itemid          44024683482
defindex        7
paint_index     282
rarity          5
quality         4
float           0.22540105879306793
paint_seed      661
killeater_type  None
kill_count      None
custom_name     None
stickers        [{'slot': 0, 'sticker_id': 5032, ...}]
keychains       []

Валидируйте, прежде чем доверять. Произвольный hex может разобраться как синтаксически корректный protobuf и вернуть вам мусор. Требуйте хотя бы одно опознавательное поле (itemid, defindex, paintindex, paintseed или непустой массив стикеров/чармов), прежде чем отдавать результат. Для payload с маской референсная реализация ещё строже: она требует, чтобы присутствовали itemid, defindex, paintindex, inventory и origin одновременно. Всё, что эти проверки не проходит, должно бросать исключение, а не возвращать пустые значения.


Чего локально всё равно не получить

Честность насчёт границы важнее самого декодирования, потому что продукт, построенный на неверном допущении в этом месте, обходится дорого.

Классические ссылки S/A/D и M/A/D. В них нет вообще никаких данных предмета. Для резолва нужен Steam-аккаунт, залогиненный в game coordinator CS2. Если на вход к вам попадают исторические ссылки, лоты Steam Community Market или что угодно, собранное из старых источников, вам всё ещё нужна GC-инфраструктура — либо готовность признать, что такие входные данные не резолвятся. Все офлайн-библиотеки говорят одно и то же; в документации Helyux/cs2inspect прямо сказано, что легаси-указатели лежат за пределами возможностей офлайн-библиотеки.

Актуальное владение и торгуемость. В payload есть поле accountid, но в самокодирующихся ссылках оно часто отсутствует, а если присутствует — это снимок на момент генерации ссылки, а не текущий факт. Нынешнего владельца, срок трейд-локa, статус лота и цену ссылка не содержит и содержать не будет.

Читаемые названия. Ссылка даёт defindex 7 и paintindex 282, а не «AK-47 | Redline». Превращение id в названия требует схемы предметов игры (items_game.txt или зеркала дампа схемы). То же и со стикером id 5032 — это поиск по справочнику, а не декодирование.

Названия износа. «Field-Tested» в payload нет. Оно выводится из float по границам износа конкретного скина, а эти min/max тоже живут в схеме, а не в ссылке.

Всё, что выводится из сида. Процент fade, тиры blue gem, фазы доплера, паттерны Case Hardened — ссылка даёт только сырое целое paintseed. Превращение сида в «97.4% fade» или «blue gem первого тира» это отдельный расчёт или таблица соответствий на вашей стороне.

Самокодирующаяся ссылка — это снимок, а не живая запись. Ничто не мешает человеку собрать валидную ссылку, описывающую предмет, которого не существует или которым он не владеет. CRC защищает от повреждения, а не от подделки: валидную контрольную сумму может посчитать кто угодно. Если ваш пайплайн принимает финансовые решения, считайте декодированную ссылку непроверенным утверждением о предмете, пока не подтвердите актив независимо.

🔧Паттерны: blue gem, fade%бесплатно, без регистрации

Типичные ошибки

  • Приведение paintwear вместо переинтерпретации. Баг номер один. float(1046925193) это не 0.2254. Нужен битовый шаблон.
  • Допущение, что все ссылки самокодирующиеся. Ветвитесь по формату хвоста. Резолвер, который бросает исключение на классических ссылках, работает правильно; тот, что молча возвращает пустые значения, — нет.
  • Пропуск проверки на нечётную длину. Обрезанный hex вызывает невнятную ошибку где-то в глубине bytes.fromhex. Проверяйте чётность длины hex заранее.
  • Забытый ведущий 0x00 в контрольной сумме. CRC по «голому» payload даёт расхождение на каждой валидной ссылке и отправляет вас искать несуществующий баг.
  • Проверка контрольной суммы на payload с маской. Сначала снимите маску; не сверяйте CRC с замаскированными байтами.
  • Чтение только первого вхождения повторяемого поля. Protobuf допускает повторяющиеся теги; стикеры и чармы реально повторяются. Собирайте их в список.
  • Отсутствие URL-декодирования. Ссылки, скопированные из браузеров и мессенджеров, приходят с %20 вместо пробела, а иногда и с переводом строки на конце.
  • Успешный парсинг вместо валидации. Случайный hex довольно часто разбирается без ошибок. Применяйте проверки на наличие полей.
  • Захардкоженный маппинг названий предметов. Paint index-ы накапливаются с каждым новым кейсом. Обновляйте схему, а не вшивайте таблицу.

Что теперь с float-API

Практический итог: резолв float превратился из инфраструктурной задачи в задачу парсинга — по крайней мере для ссылок, сгенерированных после марта 2026. То, что раньше означало бот-аккаунт, сессию с GC, ретраи и очередь, теперь занимает микросекунды локального CPU. Лимиты исчезают. Простои исчезают. Хорошо задокументированная ненадёжность GC перестаёт быть вашей проблемой — для таких ссылок.

Остаётся неромантичное, но реальное расщепление в любом продакшн-резолвере: быстрый локальный путь для hex-хвостов и медленный, хрупкий, зависящий от аккаунтов путь для легаси-указателей. Мы держим ровно такую развилку, и честный итог такой: локальный путь обслуживает подавляющее большинство текущего трафика, а путь через GC отказывается умирать окончательно.

Можно ли узнать float CS2 без API в 2026 году?
Да, для современных ссылок. Если хвост после csgo_econ_action_preview — чистый шестнадцатеричный код, float закодирован внутри самой ссылки и декодируется локально, без обращения к сети. Классическим ссылкам S/A/D по-прежнему нужно подключение к game coordinator.
CSFloat inspect API умер?
Как путь резолва — да. Репозиторий csfloat/inspect заархивирован 25 марта 2026 с пометкой, отправляющей к библиотеке-сериализатору, а api.csfloat.com больше не резолвится в DNS. Рекомендуемый подход — локальное декодирование.
Чем отличается замаскированная inspect-ссылка от незамаскированной?
Обе оборачивают один и тот же protobuf-payload. Буферы без маски начинаются с 0x00 и заканчиваются 4-байтовой контрольной суммой, которую можно проверить. Буферы с маской начинаются с ненулевого ключевого байта, и каждый байт проксорен этим ключом. Проксорьте весь буфер его первым байтом, чтобы вернуть форму без маски.
Почему декодированный float получается огромным числом вроде 1046925193?
Вы привели целое paintwear вместо переинтерпретации его битов. Поле объявлено как uint32, но эти 32 бита — представление IEEE-754 float32. Используйте struct.unpack("<f", struct.pack("<I", value))[0].
Можно ли получить название предмета из inspect-ссылки?
Напрямую нет. Ссылка несёт числовые defindex и paintindex. Чтобы сопоставить их с «AK-47 | Redline», нужна схема предметов игры, которую вы поддерживаете отдельно от декодера.
Содержит ли inspect-ссылка стикеры и чармы?
Да. Стикеры — поле 12, чармы (keychains) — поле 20; оба повторяемые и используют одну и ту же форму вложенного сообщения со slot, id, wear, rotation, offset и pattern. Поле 22, variations, устроено так же.
Может ли кто-то подделать inspect-ссылку?
Да. Контрольная сумма ловит повреждение, а не подделку — валидную сумму любой посчитает для придуманного им payload. Декодированная ссылка это непроверенное утверждение о предмете. Прежде чем принимать финансовое решение, подтвердите актив независимо.

Декодирование — простая половина. Настоящая работа начинается там, где paintseed 661 превращается в вердикт по паттерну, а float — в кондицию и справедливую цену. Ровно для этого и существуют наши бесплатные инструменты.

3 дня полного доступа бесплатноВход через Steam, без карты. Нужен уровень 3+.Открыть
← Все статьи
OmniSkin Wiki — База цен CS2, Dota 2, Rust и TF2 по всем площадкам. RU EN CS2Dota 2RustTeam Fortress 2 О сервисе Таблица арбитража → Портфель →