Glyph inspect tool: structural match, not semantic correctness¶
Why this page exists: when you paste a Radiant txid, contract id, outpoint, or raw script hex into something called an “inspector,” the natural assumption is that a green check mark means the bytes are correct — that the FT will spend, the dMint contract will mint, the NFT actually exists. The pyrxd inspect tool does not make that claim. It is a structural-match classifier: it tells you which on-chain shape a script or transaction matches, and most shapes come with a qualifier making the trust boundary explicit. This page explains what the tool does, what the qualifier rules out, which shapes and surfaces are missing one, and what the two delivery variants (CLI and browser) actually run.
What the tool does¶
You hand it one of four inputs:
Input form |
Example |
Needs network? |
|---|---|---|
Hex script |
|
no |
Contract id |
|
no |
Outpoint |
|
no ( |
Txid |
64 hex chars |
yes ( |
A 32-byte locking script is also 64 hex, and one real shape lands there: a CLTV
time-lock whose deadline is a wall-clock time (every Unix deadline from 1985 to
2038 encodes as a 4-byte push, giving a 32-byte script). Those are the
wall-clock HTLC refund legs, so 64 hex that parses as an exact CLTV/CSV
P2PKH template is read as a script rather than a txid. The preference is
deliberately narrow: it runs the production template parser, which pins the
P2PKH tail, the OP_DROP, the time-lock opcode and a minimal value push, so a
real txid cannot fall into it by accident.
It runs the bytes through the structural classifier in
src/pyrxd/glyph/_inspect_core.py
and returns one of the recognised shapes. The page keeps a single list
of them, under “Two classifiers: script-level and envelope-level”
below. For a fetched transaction it also decodes the 4 canonical
scriptSig pushes when vin[0] is a dMint mint claim.
The tx-shape banner (e.g. “this is a V1 dMint deploy commit”, “this is
a dMint claim at height 41/625000”) is browser-only. It is computed
in inspect.js from the classified outputs plus the reveal metadata;
nothing in the Python classifier produces one, and
pyrxd glyph inspect --fetch prints no equivalent in either human or
--json mode.
Both variants share the same Python classifier — there is exactly one implementation. The browser variant loads that same code into Pyodide and runs it client-side.
The two variants¶
CLI: pyrxd glyph inspect¶
Implementation in
src/pyrxd/cli/glyph_inspect.py
(the inspect_cmd Click command and its renderers, with helpers
re-imported from pyrxd.glyph._inspect_core so the CLI module owns
presentation only, not classification logic). glyph_cmds.py only
attaches it to the group — glyph_group.add_command(inspect_cmd) —
and re-exports the name.
$ pyrxd glyph inspect 76a914aaaa…aaaa88ac
$ pyrxd glyph inspect b45dc453…a2a8:0
$ pyrxd glyph inspect <txid> --fetch # network call to ElectrumX
$ pyrxd glyph inspect <txid:vout> --resolve # fetch source tx, classify that vout
$ pyrxd glyph inspect <op_return hex> --verify-wave # names a verified HashMark signer owns
The CLI is read-only by design: no broadcast, no wallet load, no
mnemonic prompt. Pass --json (or pipe stdout) for machine output;
the --json schema is documented in the command’s docstring and is
stable across patch releases.
Browser: Pyodide-hosted at /inspect/¶
The static page at
https://mudwoodlabs.github.io/pyrxd/inspect/ loads the same
Python code into Pyodide and runs the entire classifier in-browser.
Source lives in the repo at docs/inspect_static/inspect/ —
index.html is the page shell, inspect.js is the boot + DOM glue,
shared.js is the part a second page also needs (runtime boot,
ElectrumX wire, verdict colour, file-check mechanics), and glue.py
runs inside Pyodide and calls into pyrxd.glyph.inspect.
What runs server-side: nothing. GitHub Pages serves static bytes;
there is no application backend. ElectrumX is only contacted for the
txid auto-fetch path, and that connection is a direct WebSocket
from the browser to a single hard-coded server pinned in the page’s
Content-Security-Policy connect-src.
No key material is ever loaded. No transactions are ever signed or broadcast. The page is a diagnostic, not a wallet.
The other browser page: /verify/¶
https://mudwoodlabs.github.io/pyrxd/verify/ is the same runtime
aimed at a different reader. /inspect/ assumes you know what a Glyph
script is and shows you every field of one; /verify/ assumes nothing
and answers the four questions pyrxd verify answers about a HashMark
— who vouched for it, what was fingerprinted, which block carried it,
and whether a file you have matches — in plain language, for someone
who arrived from a link with no context.
It is not a second implementation of anything. Both pages read the one
wheel, the one glue.py and the one manifest built by the docs CI
step, and every verdict on both comes out of the same Python. The
split is presentation only, which is why shared.js holds everything
a reader could check one page against the other on — including the two
sentences that are claims rather than facts: what a mark proves, and
the promise that a chosen file never leaves the machine.
Its normal signature outcome is NOT CHECKED, on both pages and for
every v2 record, because pyrxd installs under Pyodide with
deps=False and coincurve ships no pure-Python wheel. That is a
missing capability of the reader’s browser and never a verdict on the
record — it is rendered neutral, says whose limitation it is, and
points at the CLI, which has the curve library.
The trust boundary¶
Structural match does not equal protocol-semantic correctness. Most classified shapes carry a qualifier spelling out what the pattern match does not verify — but not all of them, and not on every surface. The exact coverage is in “Footguns the tool guards against” §1 below; read it before relying on the qualifier’s presence as a safety net.
For example, the FT/NFT qualifier from the CLI’s
_render_script_human:
structural pattern match: bytes match the FT/NFT script template; does NOT verify the ref points to a valid Glyph contract
And for dMint contracts:
structural pattern match; does NOT verify the contract_ref points to a valid mint chain or that the parameters match a deployed token
Concretely, things inspect will tell you and things it will NOT
tell you:
The tool says… |
…which means |
…but it does NOT say |
|---|---|---|
|
The 75-byte script matches the FT template, with the conservation fingerprint |
That a transfer spending this UTXO will satisfy the FT covenant’s input/output balance check at broadcast. |
|
The 241-byte script matches the V1 dMint contract layout; those numbers were pushed onto the stack. |
That the parameters match the deployed token’s CBOR metadata, that anyone has successfully mined from this contract, or that the contract is the head of its mint chain. |
|
The script matches the commit-ft hashlock template. |
That a reveal tx exists or that the CBOR behind the hashlock decodes to anything valid. |
Tx-shape banner: “V1 dMint deploy commit” |
The output counts match the commit shape: at least one commit-ft, at least one commit-nft, and at least three P2PKH outputs (change plus the ref-seeds). |
That a successful reveal followed, that the deployer broadcast valid CBOR, or that any mining will happen. Nor that those P2PKH outputs really are 1-photon ref-seeds — the banner counts them, it never reads their values. |
Tx-shape banner: “dMint claim at height 41/625000” |
Some output classified as |
That the on-chain covenant accepted this spend — the covenant has runtime conditions (PoW, FT conservation, reward output shape) the byte-level classifier cannot evaluate without re-executing the script. Nor that |
|
The bytes are an exact round-trip against pyrxd’s soulbound covenant builder for the reported ref and owner. The only spends the lock permits are a byte-identical self-clone or a burn. |
That the bound ref names a live Glyph singleton, that the singleton is actually held at this output, or that the covenant is free of defects — it is a pre-external-audit prototype. |
|
The script binds a singleton ref and contains a self-replication-or-burn structure. |
That it is a soulbound token. Container and vault covenants self-replicate too, and these bytes match no covenant pyrxd builds. This tier is markers, not identification. |
|
The script is |
Whether the delay has elapsed — that needs this output’s confirmation height, which a locking script does not carry. |
|
No classifier claims the shape, but the opcode-aware walk found OP_PUSHINPUTREF-family refs in it. |
What the script does. It says only that spending this UTXO as plain funding would destroy the token it carries. |
The boundary is intentional. inspect reads bytes; it does not
execute scripts, and no classification is ever taken from a server —
no ref chain is walked, no contract is resolved, no type comes back
from an index.
That is not the same as “no network.” Three paths do talk to an
ElectrumX server, which is an indexer: --fetch and --resolve issue
blockchain.transaction.get to obtain the raw transaction, and the
CLI’s --verify-wave issues a lookup for the WAVE names a verified
HashMark signer owns. The browser variant does the same fetch over a
WebSocket to the single server pinned in the page’s CSP connect-src.
The server hands over bytes; every verdict is then computed locally
from those bytes. If you need “this UTXO is really the head of this
mint chain” or “this FT output will be accepted by the network when
spent,” that’s a different tool.
Two classifiers: script-level and envelope-level¶
inspect runs two independent classifiers, and confusing them
sends people hunting for byte patterns that cannot exist.
Script-level — reads a locking script and reports a type. This
is what you get from pyrxd glyph inspect <hex>, and what fills each
row of the outputs[] list on a fetched transaction. Every value
_inspect_core can emit:
p2pkh, p2pkh-cltv, p2pkh-csv, p2sh, nft, ft, mut,
commit-nft, commit-ft, commit-dat, dmint, container-legacy,
delegate-token, delegate-burn, authority-gated-nft,
soulbound-covenant, self-replicating-covenant, op_return,
op_return-msg, op_return-hashmark-v1, op_return-hashmark-v2,
op_return-burn,
unknown.
Plus one that only ever appears in a fetched transaction’s outputs[]
row, never from a pasted script: error, when classifying that one
output raised. The row keeps its vout and satoshis, and the error
field is the exception’s class name and nothing more. The remaining
rows are unaffected, which is the point of the per-output try.
commit-dat is the commit half of a DAT (data-storage) glyph. It
differs from commit-nft / commit-ft by having no
OP_REFTYPE_OUTPUT block, which is the whole point: its reveal obliges
no token output and mints nothing. What survives is the payload in the
reveal’s scriptSig. It also carries an extra dat marker push, so
every field after the payload hash sits at a different offset than in
the other two commits.
op_return-burn is a Glyph BURN proof — an OP_RETURN declaring that a
token was destroyed. Read the note on that row before believing it:
anyone can write one, about any token, in a transaction that never held
it. The proof is a claim, and it becomes a burn only when the
transaction also spent the token and no output carries its ref.
verify_burn reports which of those two it could establish, and it
cannot establish the first from the burning transaction alone — the
spent outputs live in earlier transactions.
authority-gated-nft is a 101-byte item whose script carries
OP_REQUIREINPUTREF on an issuer’s authority ref ahead of its own
singleton. Consensus refuses to create such an output unless the
transaction holds the authority token, so it cannot be minted by a
counterfeiter.
The label answers “is this output gated now”, which is weaker than
it sounds and is why the row carries a note. Measured on a node
(tests/test_authority_regtest_e2e.py): the holder can transfer the
item to a plain 63-byte nft script — same ref, no gate, nobody’s
permission needed — so a live UTXO’s gatedness is whatever its holder
last chose. Authority gating is a claim about an item’s genesis, and
only the transaction that minted it establishes one. What the gate does
buy is real: minting a further gated item from an existing one is
rejected, so the issuer’s authority is a genuine supply cap.
delegate-token and delegate-burn are the two halves of a delegate,
which is how a token’s in/by claim can be authorised without the
minter holding the parent singleton. A delegate-token output is the
same 63 bytes as nft, differing only in the opcode (0xd0
OP_PUSHINPUTREF rather than 0xd8 OP_PUSHINPUTREFSINGLETON), so
anything classifying by length alone gets it wrong; it is token-bearing
either way, and spending one as ordinary funding destroys it. A
delegate-burn is the unspendable 42-byte marker a mint emits to prove
it consumed one.
Both name a base ref, not a container or author ref. The base is
where the authorisation actually lives, and the inspector cannot follow
it from the transaction in front of it — resolving a delegated claim
means fetching the base transaction, which glyph inspect --fetch
does and a pasted script cannot. A relationship that reads
UNRESOLVED alongside a delegate_burns entry means exactly that: the
lookup did not happen, not that the claim was forged.
The three op_return-* values are refinements of op_return, added
when the data carrier self-identifies (see “OP_RETURN data carriers”
below). Treat the prefix, not the exact string, as the stable part: a
consumer switching on type == "op_return" will miss them.
Envelope-level — reads the CBOR payload out of the reveal
transaction’s scriptSig and reports the highest-specificity Glyph
protocol label under metadata.classification. It sees: wave,
container, authority, timelock, encrypted, dmint, mut,
dat, ft, nft, and unknown when the envelope claims no protocol
the classifier knows. This only appears on the fetched-transaction
forms (--fetch, and --resolve on an outpoint), because there is no
envelope to read without the reveal transaction.
The distinction that trips people:
A TIMELOCK token and an ENCRYPTED token are envelope-level only. Their protocol flags live in the CBOR; their locking script is an ordinary NFT/MUT singleton. There is no script pattern to detect, so no amount of staring at the scriptPubKey will find one.
p2pkh-cltv/p2pkh-csvare script-level, and are unrelated to the TIMELOCK protocol. They are plain BIP-65 / BIP-112 time-locked P2PKH outputs — in this codebase, most often an HTLC refund leg.A “soulbound” NFT can be either.
policy.transferable: falsein the envelope is advisory — a flag an honest wallet may choose to respect and any other wallet ignores. Only the script-levelsoulbound-covenanttype reflects a restriction consensus enforces. A token can carry the metadata flag with a plainnftlocking script, and that combination restricts nothing.
V1 mint scriptSig decode¶
When vin[0] of a fetched transaction matches the dMint mint claim
shape, the inspect tool decodes the four canonical pushes:
Push |
Field |
Width |
|---|---|---|
1 |
nonce (LE) |
4 bytes = V1, 8 bytes = V2 |
2 |
input hash |
32 bytes — |
3 |
output hash |
32 bytes — |
4 |
OP_0 |
1 byte |
The version is distinguished by nonce width: a 4-byte nonce (V1) gives
a 72-byte scriptSig; an 8-byte nonce (V2) gives 76 bytes. The two
hashes are literal SHA256d outputs, not preimage halves — this
distinction is load-bearing because the M1 release shipped with the
preimage halves pushed instead of the SHA256d outputs, and every
on-chain mine was rejected by the covenant until the fix landed at
mainnet txid
c9fdcd3488f3e396bec3ce0b766bb8070963e7e75bb513b8820b6663e469e530
(see the 0.5.0 CHANGELOG entry and the related solutions doc on the
M1 V1 mint scriptSig divergence).
The inspect tool surfaces all four pushes plus the inferred version hint, so a reader can verify by eye that a mint claim’s scriptSig matches the convention.
OP_RETURN data carriers¶
OP_RETURN outputs are classified explicitly, and the trailing data
bytes are split out from the leading 0x6a opcode into a separate
data_hex field. This is a cosmetic but useful separation: a reader
scanning a dMint claim sees that vout[2] is the OP_RETURN message
script the mint covenant hashes (and whose SHA256d appears in the
scriptSig at vin[0]), without having to manually strip the opcode
prefix.
The baseline is type=op_return with data_hex and nothing more —
no CBOR is assumed, no encoding is guessed. But the classifier does
look for two self-identifying prefixes, and refines the type when it
finds one:
Marker |
Type becomes |
What is decoded |
|---|---|---|
Photonic |
|
The trailing push, decoded as UTF-8 into |
HashMark: |
|
Version, algorithm, digest, optional label, and for v2 the committed signer hash160 and signature. |
For a well-formed HashMark the classifier goes one step further and
verifies the v2 signature: verify_attestation recovers the signing
key from the signature over the canonical statement and requires it to
hash to the signer committed in the record. The result lands in
hashmark.attestation as valid, invalid_signature, unverifiable,
or not_attested. Radiant mainnet’s genesis hash is part of the signed
statement and a pasted script carries no chain context, so mainnet is
assumed, and the assumption is reported in the output rather than
hidden — the same bytes on another chain are a different statement.
unverifiable is the one to understand. Verifying needs secp256k1, and
the Pyodide page installs pyrxd without it (coincurve ships no
pure-Python wheel), so for a long time every v2 record pasted into
/inspect/ — and every mark checked on /verify/ — came back
unverifiable, and the public page’s headline question went permanently
unanswered.
Both pages now install a vendored @noble/secp256k1 as a recovery
backend (docs/inspect_static/inspect/secp256k1-bridge.js, pinned in
tests/fixtures/noble_secp256k1_upstream_pin.json), so the browser
reaches a real valid or invalid_signature. Only one operation crosses
into JavaScript — recover a public key from a message hash, r, s and
a recovery id. The canonical statement, the varint framing, the
double-SHA256, the low-S and range checks, hash160, and the comparison
against the committed signer all stay in this one Python implementation,
which is what stops the browser and pyrxd verify ever disagreeing about
a rule. tests/test_signature_backend_differential.py runs both curves
over the same records and fails if their verdicts diverge.
unverifiable therefore now means the curve did not load in that tab —
a SHA mismatch against the manifest, a blocked file, a browser without
dynamic import(). It remains a missing capability of the reader, not a
fact about the record: the digest, the label and the signer the record
names all still reach you, and only the verdict is withheld. Both
surfaces render it as NOT CHECKED, never as a failure — telling
someone an honest mark’s claim does not hold, on the strength of a
library that did not load on their machine, would be the worst thing
either tool could do. Nothing in the load path can reach a failing
verdict: if the curve does not arrive, no backend is registered and
verify_attestation returns unverifiable by the path it already had.
Note the asymmetry with the write side, which is deliberate: MarkPlan
treats unverifiable as a refusal, because funding a transaction
needs the same curve that signs it and there is no honest way to proceed
without it. Reading, there is.
Two consequences worth stating plainly:
This is real cryptographic verification inside a tool otherwise described as byte-pattern matching.
attestation.outcome == "valid"means a signature checked out. It still does not establish authorship, ownership, originality, or the truth of the marked file’s contents — only that whoever holds that key made this statement about this digest no later than the confirming block. The claim it reaches is key custody at that block, and nothing wider: not who wrote the file, not who owns it, and not where anyone was.The browser page adds one thing the CLI cannot: it will hash a file you choose and compare it against the record’s digest, using the algorithm the record’s own
algorithm_idnames. The file is read inside the browser and never leaves the machine — the same promisepyrxd markmakes from the other end, where the digest goes on chain and the contents do not. A match says the bytes in front of you are the bytes the record commits to. It says nothing about who made them.Anything that does not carry one of the two markers stays plain
op_return, deliberately. A scanner meets thousands of other protocols’ data outputs, and treating them as errors buries the real ones. If a reader needs to know what those bytes mean, they decode them out-of-band.
HashMark is a third-party format (MIT,
github.com/cdonnachie/hashmark.rxd). The inspector is read-only, and
that is the whole of what this page describes; pyrxd can also WRITE a v2
record (script/hashmark.py, encode_hashmark), but nothing on this
page does.
The signature check is also why HashMark decoding is a CLI-only
capability today: it reaches for coincurve, which is not installed in
the browser’s Pyodide runtime, so a HashMark OP_RETURN fails to classify
there. See “Why share one classifier across CLI and browser” below.
Browser variant: install-time integrity¶
The browser inspector is served from GitHub Pages, and its install-time checks are worth understanding precisely, because two of them are narrower than they look.
Artifact |
Source |
What is actually checked |
|---|---|---|
Pyodide loader ( |
jsdelivr CDN |
Subresource Integrity (SHA-384) on the |
Everything else Pyodide loads — |
jsdelivr CDN, under the |
No SRI. These are fetched by the loader script itself, after it runs. Whatever integrity Pyodide applies to its own packages is anchored in a lockfile that comes from the same |
|
same-origin ( |
SHA-256 compared against |
Vendored |
same-origin ( |
SHA-256 compared against |
|
same-origin |
SHA-256 compared against |
|
same-origin |
Not hashed. It is fetched with no integrity check, and every expected SHA-256 above is read out of it. |
If any SHA-256 mismatches the manifest, the install aborts loudly with
an error citing the mismatch — the tool does not fall through to “try
without the integrity check.” The verification code is in
docs/inspect_static/inspect/inspect.js
(fetchAndVerify).
What the manifest check does and does not defend against¶
The manifest and the wheels are written by the same CI job and deployed together, and the page reads the manifest from the same origin as the bytes it describes. So the check is a deploy-integrity check — “did these bytes come from the run that wrote this manifest?” — not a supply-chain trust anchor.
It catches:
a partial, stale or corrupted deploy: a wheel updated without its manifest (or the reverse), a truncated transfer, a stale cached wheel served alongside a fresh manifest;
a manifest that tries to point an install somewhere else.
manifest.wheelandmanifest.cbor2_wheelmust be bare basenames (no/, no.., no URL-encoded separators), so a poisoned manifest cannot redirect a wheel install off-origin or up a directory;a manifest that tries to disable a check by supplying a malformed digest: every SHA-256 field must be exactly 64 lowercase hex characters or boot fails, rather than the field being treated as “no hash”.
It does not defend the deployed origin against itself. An attacker who
can rewrite the GitHub Pages deploy rewrites manifest.json in the same
act, and could equally rewrite inspect.js — which is also same-origin
and carries no hash — removing the check altogether. Closing that would
need a digest anchored somewhere the deploy cannot reach.
The page’s Content-Security-Policy denies PyPI as a script source, and
micropip.install(..., deps=False) is used for the pyrxd wheel so no
transitive PyPI metadata fetch happens during bootstrap. The cbor2 wheel
is pinned to 5.4.6 because cbor2 6.x ships C-only and a Pyodide install
that depends on a PyPI fetch creates an off-origin trust path the
same-origin SHA-256 approach is designed to avoid.
Footguns the tool guards against¶
“It’s classified, so it must be correct.” Most classified shapes carry a structural-match qualifier: the CLI prints it in parentheses below the per-script body, the browser shows it as a
structural-noteparagraph. Where it is emitted it is not optional and not suppressible.It is not emitted everywhere, and the gaps do not line up between the four rendering surfaces. Coverage as it stands:
Script
typeCLI, pasted script
CLI, tx output row
Browser (either)
ft,nft,mut,commit-ft,commit-nft,dmint,p2sh,p2pkh-cltv,p2pkh-csvqualifier
—
qualifier
container-legacy,soulbound-covenant,self-replicating-covenantthe classifier’s
note, spelled outone-line warning (
UNSPENDABLE,markers only — NOT proof of soulbound)the classifier’s
noteunknown“does not match any known Glyph or P2PKH layout”
token-bearing lines only
pasted script: a “doesn’t match any known template” note; tx row: —
op_return— (the human renderer prints the head line and stops; not even
data_hex)—
qualifier
op_return-msgthe decoded message, no qualifier
—
—
op_return-hashmark-v1/-v2the HashMark caveat, no qualifier
—
— (and see the
coincurvegap above)p2pkh—
—
—
errorn/a
(classifier error: …)the
errorfield, no qualifierTwo mechanical causes are worth knowing, because they explain the shape of the table rather than each cell:
The browser’s
_structuralQualifierNoteis a lookup on an exact type string over ten hand-written keys. The threeop_return-*refinements therefore lose even the generic OP_RETURN qualifier that plainop_returngets, andp2pkh,unknownanderrorwere never in the table.The CLI’s fetched-transaction renderer,
_render_txid_human, emits no qualifier on any row, for any type. Since a pasted txid is how most people meet the tool, that is the surface where the trust boundary is least visible. Paste the individual script hex to get the qualified form.
Read the qualifier as a bonus, not a guarantee: its absence says nothing about how much the classification proves.
“It looks like a V2 dMint contract on mainnet.” The classifier recognises V2 shapes for forward-compatibility, but no V2 dMint contract has been deployed on Radiant mainnet to date. The browser’s dMint-claim banner says so explicitly, though only when
vin[0]carried a decodable mint scriptSig — the deploy-reveal banner carries no such note, and the CLI emits no banner at all. If you paste a V2-shaped contract and seeversion=v2in the output, that is a synthetic input or a future deploy — it is not a mainnet token to date.Hostile manifests, poisoned wheels, and CDN compromise. The browser variant treats the wheels and
glue.pyas untrusted bytes until their SHA-256 matches the manifest: a mismatch aborts loudly, and basename validation rejects path traversal. The CSP forbidsscript-srcfrom anywhere other than the page itself and the pinned jsdelivr origin.Be precise about the two limits, both spelled out under “Browser variant: install-time integrity” above. The SRI hash covers
pyodide.jsand nothing else —pyodide.asm.wasmand the rest of the runtime are fetched by that script, after it runs, from that same CDN path and with nointegrityattribute — so it catches a tampered loader, not a jsdelivr compromise in general, and WASM does run from unverified bytes. And the manifest that supplies every SHA-256 is itself fetched unverified from the deploy it is meant to police, which makes the wheel check a deploy-integrity check rather than a defence against a compromised deploy.Spoofed token names in CBOR metadata. Reveal-tx metadata strings are sanitised through
sanitize_display_string(strips Unicode control / format / combining codepoints) and capped viatruncate_for_human. Token names and tickers are run through the TR39 confusables skeleton check (looks_confusable_with_latin) — a Cyrillic-spoofed “USDC” is flagged before the user sees the rendered metadata: the CLI prints a*** WARNING ***line ABOVE the field it applies to, and the browser page paints a warning band on the card.It reports mimicry, not foreignness. A wholly non-Latin name like
トークンor中文is not flagged — a warning that fires on every legitimate Japanese token is the false positive that trains a reader to ignore the real one.This paragraph described the check as live while
looks_confusable_with_latinhad no production caller — a definition, a façade re-export and its tests. The CLI ran no confusables check at all and the browser used a weaker script-mixing heuristic. Wired 2026-09-03; both surfaces now read one verdict computed in_inspect_core.OP_RETURN ambiguity. Data carriers are classified as
op_returnwith the data split out, not silently grouped with “unknown” scripts. A reader looking at a dMint claim can see at a glance that the OP_RETURN atvout[2]is the message script whose hash appears in the scriptSig. Two self-identifying formats are decoded further and get their own types — see “OP_RETURN data carriers” above, including the signature verification that runs on a HashMark record and the shapes that do not reach it in the browser.
Source-of-truth references¶
Classifier core.
src/pyrxd/glyph/_inspect_core.py(private; pure-Python, no heavy deps at import time)Public façade.
src/pyrxd/glyph/inspect.pyCLI command.
src/pyrxd/cli/glyph_inspect.py— theinspect_cmdClick command,_render_script_human,_render_txid_humanand the rest of the presentation layer.CLI group wiring.
src/pyrxd/cli/glyph_cmds.py— attaches the command to theglyphgroup and re-exports the name. It holds no inspect logic of its own.Browser host.
docs/inspect_static/inspect/index.html,docs/inspect_static/inspect/inspect.js,docs/inspect_static/inspect/glue.pyCHANGELOG entries. 0.4.0 (Glyph inspect — CLI / browser) and 0.5.0 (V1 mint scriptSig parsing, V1 deploy detection) in the repository’s
CHANGELOG.md.Related concept. Radiant FTs are on-chain (not metadata-on-P2PKH) — what the 75-byte FT shape the inspector matches actually means at the consensus layer.