Glyph royalties are advisory, not consensus-enforced¶
The question¶
GlyphRoyalty has been decoded from the Glyph envelope since 0.9.0 and never
paid. Closing that gap needs an answer first: is a royalty on a Radiant token
enforceable, or is it a convention? Building a “royalty-honouring transfer”
without answering it risks shipping something that tells a creator they are
being paid when nothing guarantees it.
The answer¶
Advisory. A royalty on an ordinary transfer is a social convention: a compliant wallet honours it, a non-compliant one omits it, and the chain accepts both.
It can be made binding on a buyer, by putting the token into a covenant that only releases it against required outputs. It cannot be made binding on a holder, because the holder chooses the transaction.
Evidence¶
Checked, not assumed:
The locking scripts do not constrain payments.
build_nft_locking_script(src/pyrxd/glyph/script.py:127-133) isOP_PUSHINPUTREFSINGLETON <ref> OP_DROPfollowed by a bare P2PKH tail — 63 bytes, no output introspection.build_ft_locking_script(:135-143) is a bare P2PKH prefix plus a 12-byte conservation epilogue. Conservation constrains how many units of the ref may exist on the output side. It says nothing about where value goes, so it cannot require a payment to anybody.No shipped covenant references a royalty.
grep -riE "royalt"oversrc/pyrxd/gravity,src/pyrxd/swap,src/pyrxd/scriptandsrc/pyrxd/glyph/dmintreturns zero hits. The only pyrxd covenant that constrains an output script at all issrc/pyrxd/glyph/soulbound_covenant.py, an explicitly-labelled prototype that forbids transfer entirely rather than pricing it.pyrxd’s own type already said so.
GlyphRoyalty(src/pyrxd/glyph/types.py:171-180) documents itself as an “on-chain royalty hint for secondary-market wallets”, and itsenforcedfield means “whether wallets should enforce this royalty” — not the chain.Photonic Wallet reaches the same structural conclusion. Its royalty covenant (
packages/lib/src/royaltyCovenant.ts, header comment) describes the lifecycle explicitly: at rest the NFT lives in the ordinarynftScript(owner, ref)— a P2PKH-gated singleton; enforcement exists only once a holder voluntarily lists it intoroyaltySaleScript(...), where the seller’s wallet bakes the payout amounts in as literals and a buyer can only spend the UTXO by paying them. That file’s own “Honest scope” section records the two holes this leaves: it does not stop a malicious seller using non-wallet software from crafting a non-compliant listing, nor a holder gifting the token out of band with no sale.Photonic’s non-covenant royalty layer is not wired in.
packages/lib/src/royalty.tsexportsnftRoyaltyScript,buildRoyaltyOutputs,calculateRoyaltyandcheckRoyaltyCompliance;grep -rnacrosspackages/shows every one of them referenced only by its own unit test. The same pattern assoulbound.ts, which pyrxd already documented as dead code. And where it does branch, it treats a non-enforced royalty as producing no outputs at all (royalty.ts:191-193), i.e. “advisory” is implemented as “never paid”.
What pyrxd does about it¶
Pay it honestly, and never imply more:
pyrxd.glyph.royaltycomputes the payouts.royalty_due = min(max(minimum, floor(sale_price * bps / 10_000)), sale_price). Themaxhalf is byte-for-byte Photonic’scalculateRoyalty, so a pyrxd payment and a Photonic payment agree on the single-recipient path whenever the declared terms are sane. Theminhalf is a pyrxd bound Photonic does not have:GlyphRoyalty.minimumis a free integer chosen by the token’s creator with nothing on chain limiting it, and it is spent from the funding inputs of whoever moves the token. A royalty larger than the consideration is not a royalty. Consequence: aminimumwithsale_price=0now pays nothing, which removes the flat-fee-on-a-gift reading this module used to document.FtUtxoSet.build_airdrop_txtakes an optionalroyalty=and resolves it three ways:pay_royalty=None(the default) pays iffroyalty.enforced,Truepays an advisory royalty anyway,Falsenever pays. The builder used to ignoreenforcedentirely and pay whenever a royalty was supplied — butenforcedis the creator’s own statement about whether wallets should insist, it defaults toFalseeverywhere aGlyphRoyaltyis built, and paying anyway spends the sender’s photons on a payment the creator did not ask to be insisted on. Keeping theTrueoverride is what stops this collapsing into Photonic’s “advisory means never paid”. The decision is recorded in the result either way. A one-recipient airdrop is an ordinary transfer, so this covers the transfer case too.Payouts are plain 25-byte P2PKH outputs. They carry no ref, so they contribute to no conservation sum — which is why a royalty can ride on an FT transfer at all. Verified against a live
radiant-core:v3.1.1regtest node intests/test_ft_airdrop_regtest_e2e.py.The interface takes a decoded
GlyphRoyalty— the creator’s recorded terms — not loose--royalty-address/--royalty-bpsflags. Letting the payer type in the terms would look like royalty support while protecting no creator; see “deliberately not built” below.The CLI’s pre-broadcast metadata summary prints
(ADVISORY — recorded on chain, not enforced by consensus)next to any royalty, at the moment a creator is still deciding.
Two deliberate deviations from Photonic on the splits path, both because
Photonic’s version can pay the creator less than the terms they recorded:
minimum is honoured when splits are present (Photonic computes each split from
sale_price directly and never consults it), and the residue — flooring loss
plus any bps the splits do not cover — is routed to the top-level address
rather than dropped. The invariant is exact:
sum(payout.photons) == royalty_due(...).
Deliberately not built¶
A royalty listing covenant. It is the only thing that would make a royalty binding, it is a new consensus-adjacent script surface, and it belongs behind the same external-audit gate as the rest of the covenant work. Photonic’s design (seller-committed fixed amounts, no on-chain MUL/DIV) is the reference if and when it is built.
CLI
--sale-priceontransfer-ft/transfer-nft. The terms would have to come from the person paying, which cannot protect a creator. Honouring a royalty from the CLI needs the creator’s recorded terms, which means resolving a token’s ref to its reveal transaction and decoding the envelope — a lookup pyrxd does not have yet. Until it does, the library takes aGlyphRoyaltyand a marketplace built on pyrxd supplies it.A royalty on
build_nft_transfer_txorFtUtxoSet.build_transfer_tx. The NFT builder takes exactly one input, the dust-carrying singleton, so a royalty paid from it would come out of the token. The first cut of this work did exactly that on the FT transfer path: a review measured a 5% royalty on a 1,000,000-photon sale costing the recipient 390,000 units. Both parameters were removed rather than papered over.FtUtxoSet.build_transfer_txnow takes plain-RXDfunding(it is a single-recipientbuild_airdrop_tx), so the funding objection no longer applies to it — but the parameter stays off, becauseFtTransferResulthas no field for the payouts and paying a creator without reporting who was paid is worse than not offering the option. Usebuild_airdrop_txwith one recipient; it is the same transaction and it returnsroyalty_payouts.
What would change this¶
A Radiant consensus rule that constrains payments — there is none, and adding one is a hard fork. Short of that, the answer stays “advisory for transfers, enforceable-on-the-buyer inside a listing covenant”.