# fixtags > fixtags (https://fixtags.dev) is a client-side FIX protocol log viewer plus a > zero-dependency Node CLI. Paste, drop, or live-stream a FIX log and browse it > parsed: decoded tags, order lifecycles across cancel/replace chains, latency > stats, and automatic findings. Everything runs in the browser — logs are never > uploaded. A "share" link carries the whole log compressed into the URL > fragment (the part after `#`), which browsers never send to any server, so > even sharing keeps the data off the wire. This file documents the two things > an agent most often wants: how to build a share link, and how to drive the CLI > headlessly. ## Share-URL format A share link is: https://fixtags.dev/#z=[&q=...][&m=orders][&t=...][&s=N] The payload is the raw log text, encoded as: utf8 bytes -> raw DEFLATE (zlib, no zlib/gzip header) -> base64url base64url = standard base64 with `+`→`-`, `/`→`_`, and `=` padding stripped. In Node this is exactly: zlib.deflateRawSync(Buffer.from(text, 'utf8'), { level: 9 }).toString('base64url') and it decodes with `zlib.inflateRawSync(Buffer.from(payload, 'base64url'))`. The browser reads it with `DecompressionStream('deflate-raw')`, which accepts the bytes Node's raw DEFLATE produces (and vice versa). Any DEFLATE level works — the decoder does not care — so the level is your choice; the reference CLI uses 9 for the smallest payload. Compression is not canonical: different encoders (or levels) may emit different payload bytes that all inflate back to the same text, so verify a link by DECODING it, not by re-compressing and comparing strings. The everything-after-`#z=` params are optional view state: - `&q=` — preload the search box (see query language below) - `&m=orders` — open in the Orders table view - `&t=`— open tracing that order's lifecycle - `&s=` — preselect the Nth (1-based) row Hard limit: the encoded payload must be <= 63000 characters or the page refuses to open it. If your log is bigger, filter it down first (see `--grep` / `fixtags share` below) rather than sharing the whole thing. ### Worked example (verifiable) This two-message log: 8=FIX.4.4|35=D|49=CLIENT|56=BROKER|11=ORD1|55=AAPL|54=1|38=100|40=2|44=150.50|10=000| 8=FIX.4.4|35=8|49=BROKER|56=CLIENT|11=ORD1|17=E1|150=0|39=0|55=AAPL|10=000| (a NewOrderSingle then its ExecutionReport, each line ending with a newline) encodes to this exact link: https://fixtags.dev/#z=Tc2xCsMwEAPQvR9j7uq71Bk0pIkLIaEppkM_Rh_fK8TQRYNATwWP9ZMsGbNjoY2Y97U-3_QB93ZstVEVR1uU7pim1043KHOBitAEV1oULsmFKpBoL-VfLT_1tEI9_a7qDTXSY8k8RvSbbn0B To verify, inflate the payload (everything after `#z=`) and you get the log back byte-for-byte: zlib.inflateRawSync(Buffer.from('Tc2x...bn0B', 'base64url')).toString('utf8') ## Query language The same query language powers the fixtags.dev search box, the `--grep` CLI mode, and the `&q=` share param. A query that contains no `@field` reference is treated as a plain regex (case-insensitive), falling back to a literal substring match if the regex is invalid. Fields: `@` or `@` — e.g. `@Symbol`, `@MsgType`, `@55`, `@35`. Names are the FIX field names (case-insensitive). Two synthetic fields exist: `@direction` (`@dir`) is `send`/`recv` as inferred from the log, and `@summary` is the human one-line summary of the message. Operators: - `==` / `=` equal (case-insensitive string compare) - `!=` not equal - `>` `<` `>=` `<=` numeric compare (both sides parsed as floats) - `contains` substring - `starts` / `ends` prefix / suffix Values are bare words, `"double"` or `'single'` quoted strings, or an enum alias written as another `@token` on the right-hand side. An enum alias resolves to the tag's coded value: `@OrdType == @Limit` becomes `40 == 2`, `@MsgType == @NewOrderSingle` becomes `35 == D`. Aliases match, in order: a built-in shorthand table (`NOS`/`NewOrderSingle`→D, `ER`/`ExecReport`/ `ExecutionReport`→8, `Logon`→A, `Logout`→5, `CancelReject`→9, `Heartbeat`→0, `BusinessReject`/`BizRej`→j, and more), then the field's own dictionary enum names or codes. Combine terms with `and`, `or`, `not`, and parentheses. Adjacent terms with no operator are implicitly AND-ed. Examples: @Symbol == "AAPL" and @OrdType == @Limit @MsgType == @ExecutionReport and @OrdStatus == @Rejected (@MsgType == @ER and @Symbol == "AAPL") or @Symbol == "SPY" not @MsgType == @Heartbeat @Price > 100 @Side == @Buy 35=8 # plain substring (no @ = regex/substring) ## CLI reference (for agents) Zero runtime dependencies, no build step. Install or run ad hoc: npm i -g fixtags # then: fixtags ... npx fixtags ... # no install ### fixtags --grep EXPR [file] Headless filter. Reads the file argument, or stdin (streamed line by line, so `tail -f | fixtags --grep ...` works). Prints matching lines and exits. No server, no browser, nothing written to disk. Exit codes: `0` = at least one match, `1` = no match, `2` = malformed query. Standalone — not combinable with any session/server flag. - `-c`, `--count` print the number of matching messages instead of the lines (counts messages, not lines — one line can hold several) - `--format MODE` output shape (default `raw`): - `raw` the matching lines, verbatim (default, unchanged) - `json` a JSON array, one object per matched message: `{ "n": <1-based index among all parsed messages>, "fields": [ { "tag":"35", "name":"MsgType", "value":"D", "enum":"NewOrderSingle" }, ... ] }` `name` is omitted for tags not in the dictionary; `enum` is omitted when the value has no decoded meaning; field order is wire order. - `md` a GitHub-flavored table with columns `# | Time(52) | MsgType(35) | ClOrdID(11) | Symbol(55) | Side(54) | Qty(38) | Px(44) | Status(39) | Text(58)`, enum tags decoded, absent tags left blank. - `txt` the same table, monospace-aligned with spaces — for surfaces that don't render markdown tables. Wrap it in a triple-backtick code block when posting to Slack. - `summary` one decoded line per message (`#n MsgType — `) — the most compact human-readable shape for chat replies. - `tags` the full decoded card per message: every tag in wire order (faithful to repeating groups), field name right-aligned with the tag number in parens, enums decoded after `=`, headed by a summary line. The right shape for explaining one message to a human (post inside a code block); grep down first — don't dump dozens of cards. Picking a format for humans: `md` where markdown renders (GitHub, Linear, model chat); `txt`/`tags` inside a code block for Slack and terminals; `summary` for prose. Never paste raw FIX at a human. Examples: fixtags --grep '@Symbol == "AAPL" and @OrdType == @Limit' fix.log tail -f fix.log | fixtags --grep '35=8' fixtags --grep '@MsgType == @NewOrderSingle' -c fix.log fixtags --grep '35=8' --format json fix.log fixtags --grep '@OrdStatus == @Rejected' --format md fix.log ### fixtags share [file] Builds a share link (see the format above) from a file argument or stdin and prints just the URL, newline-terminated, so it composes with pipes. The log is compressed into the URL fragment; nothing is sent anywhere. - `--q QUERY` add `&q=` (preload the search box) - `--trace ID` add `&t=` (open tracing a ClOrdID) - `--orders` add `&m=orders` (open the Orders view) - `--select N` add `&s=N` (preselect the Nth 1-based row) - `--base URL` origin to build the link against (default `https://fixtags.dev/`) - `--open` also open the link in a browser (honors `--browser`/config) - `--truncate` if the payload exceeds the 63000-char limit, share a fitting prefix and warn on stderr (default: refuse with exit 2) Exit codes: `0` = printed a link, `2` = empty input or oversize without `--truncate`. Filter before sharing when a log is large: fixtags --grep '@OrdStatus == @Rejected' fix.log | fixtags share fixtags share fix.log --trace ORD1 --orders cat fix.log | fixtags share --q '@Symbol == "AAPL"' ### --decode On a TTY, `--grep` and `--echo` accept `--decode` to append decoded enum names inline, e.g. `35=8‹ExecutionReport›`. The `‹…›` annotations are stripped by fixtags.dev on paste, so an annotated line still parses and checksums like the original. Ignored when output is piped (kept byte-exact). ## Parse FIX programmatically with core.js `core.js` is DOM-free and is served at https://fixtags.dev/core.js. An agent with Node can fetch it and `require()` it directly — no npm install, no build — to parse and query FIX the same way the app and CLI do: // node const src = await (await fetch('https://fixtags.dev/core.js')).text(); require('fs').writeFileSync('/tmp/fixtags-core.js', src); const core = require('/tmp/fixtags-core.js'); const msgs = core.extractFIXMessages('8=FIX.4.4|35=D|55=AAPL|10=000|'); const fields = core.parseFields(msgs[0].raw, msgs[0].delim); core.fv(fields, '55'); // "AAPL" core.fvn(fields, '35'); // "NewOrderSingle" (decoded) ESM works too (`import core from './core.js'` — CommonJS interop). For messy input — `‹…›`-annotated values, JSON-wrapped lines, multi-line dumps — run `core.normalizeInput(text)` first and extract from the result. Useful exports: `extractFIXMessages`, `parseFields`, `fv`/`fvn` (raw / decoded value by tag), `buildSummary`, `qlTokenize`/`qlParse`/`qlEval` (the query language), `isQueryLanguage`, `validateChecksum`, `validateBodyLength`, `buildCorrelationMaps`, `buildOrderSummaries`, `computeFindings`, and the full `DICT` tag/enum dictionary. ## Agent skill A ready-made, agent-agnostic skill (recipes for investigating FIX logs with fixtags) is served at https://fixtags.dev/skill.md and bundled with the CLI — `fixtags skill` prints it, so install it wherever your runtime keeps skills: fixtags skill > ~/.claude/skills/fixtags/SKILL.md