Hi — we read the full skill (all 24 modules, v2.3.2) while building a read-only security agent on XPR Network, then verified the load-bearing claims against mainnet. Six findings below, all reproducible.
None of these is a security vulnerability, so per SECURITY.md this is the public tracker rather than security@metallicus.com. Happy to send a PR for any or all of them if that is easier than fixing from here.
Verified against main (659d4d3) — none appears fixed since the 2.3.2 release. All chain reads via proton.protonuk.io / proton.eosusa.io on 2026-08-22.
Context for the framing: the v2.3.0 "Fast finality" → "Fast inclusion (~2s first block; LIB ~3 minutes)" correction is what made us check our own timestamps, and we found we had been stamping head_block_num and presenting it as settled. That fix earned its keep. The monthly drift-audit discipline is visible in the CHANGELOG and it is why we trusted the skill enough to build on it.
1. metalx-dex.md and defi-trading.md contradict each other on what a wrong DEX memo does — and it is the one flagged CRITICAL
SKILL.md safety reminder #6 calls the empty-memo rule a top-level fund-loss item. The two detail pages then say different things:
metalx-dex.md:
"tokens being permanently stuck in the contract with no way to recover them. The withdrawall action will NOT return tokens deposited with a wrong memo."
defi-trading.md (in depositToDex):
"memo MUST be empty string. Using 'deposit' or other non-empty memo will cause the transfer to be treated as a regular transfer, not a DEX deposit."
"Irrecoverable" and "misrouted but present" are different outcomes and imply different recovery advice. A reader who hits this needs to know which one is true.
Suggested fix: pick whichever is correct and make the other match. If recovery depends on circumstances, saying so explicitly is better than two confident and incompatible sentences.
Worth noting the contrast the skill already draws elsewhere: alcor-dex.md's malformed-memo table says Alcor fails closed — the transfer reverts, funds stay in the wallet. That is a genuinely useful comparison and it makes the dex behaviour more important to state precisely.
2. accounts-permissions.md: "XPR accounts cannot contain dots" is not correct
skill/accounts-permissions.md:
| No dots | Unlike EOS, XPR accounts cannot contain dots |
Repeated in resources.md ("Account names 1-12 characters (a-z, 1-5)").
Mainnet, 2026-08-22:
xmd.token EXISTS created 2022-06-29T18:05:22.500
xmd.treasury EXISTS created 2022-06-29T18:05:17.000
Plus eosio.token, eosio.proton, metal.kyc, wlcm.proton, loan.token, lending.loan, shares.loan, eosio.msig, dex.protonnz — several of which appear in the skill's own tables on the same page as the rule (resources.md → System Contracts).
Why it matters beyond pedantry: we hit this building account-name validation. An agent that encodes the rule as written flags xmd.token — the canonical Metal Dollar contract — as a malformed name.
Likely origin: smart-contracts.md gives the identical rule for table names, where it is correct:
"Table Naming Rules — 1-12 characters, lowercase a-z and digits 1-5 only, no dots, dashes, or uppercase"
Suggested fix: something like "user-registerable names cannot contain dots; dotted names exist as system/premium accounts (eosio.token, xmd.token) and are valid to reference."
3. webauth-identity.md: the published getKYCLevel() returns NaN on mainnet
// webauth-identity.md
return Math.max(...profile.kyc.map(k => parseInt(k.kyc_level)));
with a documented levels table (0 not verified through 3 enhanced).
On mainnet kyc_level holds a comma-separated claim list, not an integer. Read live from eosio.proton::usersinfo:
account: protonnz verified: 0
kyc_provider: "metal.kyc"
kyc_level: "metal.kyc:address,metal.kyc:birthdate,metal.kyc:selfie,
metal.kyc:frontofid,metal.kyc:backofid,metal.kyc:firstname,
metal.kyc:lastname"
parseInt(kyc_level) -> NaN
Same shape on metallicus. Math.max(NaN, ...) is NaN, so the helper returns NaN for every KYC'd account — and getKYCLevel(x) >= 2 is false for everyone, which fails open or closed depending on the caller.
Also: the UserInfo interface types kyc_level: string while the levels table describes it as 0–3, so the interface is right and the helper and table disagree with it.
Suggested fix: count claims rather than parsing a level, and document that kyc_level is a claim string. If a numeric tier is wanted, derive it explicitly and label it as derived.
Related, same module: the UserCard example renders verified as "✓ Verified" beside a KYC badge. Those two fields are close to independent on mainnet — metallicus and protonnz both carry 7 claims with verified: 0 — so the example can show a fully KYC'd account as unverified. Might be worth a note that verified is an opt-in display flag rather than a KYC summary.
4. rpc-queries.md: /v2/history/get_transfers is documented with a worked example and returns 404
Listed in the History Endpoints table and given a full getTransfers() implementation.
GET https://proton.eosusa.io/v2/history/get_transfers?account=genietoken&symbol=XPR
-> {"statusCode":404,"error":"Not Found",
"message":"Route GET:/v2/history/get_transfers... not found"}
Same on proton.protonuk.io. The route is not deployed on the endpoints the skill recommends.
/v2/history/get_actions?filter=eosio.token:transfer returns the same information and works.
Suggested fix: replace the endpoint and the example, or mark it as Hyperion-deployment-dependent.
(Cost us a real bug: our code used the documented route and silently treated the 404 as "no transfers found", reporting that 25 of 25 accounts had no inbound XPR.)
5. real-time-events.md: streaming is enabled server-side but the documented client cannot connect
The module is built on:
const ws = new WebSocket('wss://proton.eosusa.io/stream');
with action_stream / delta_stream subscriptions, and rpc-queries.md recommends streaming over polling.
Measured across three endpoints:
/v2/health streaming {"enable":true,"traces":true,"deltas":true} <- all three
raw WS wss://<host>/stream -> handshake fails
/socket.io/?EIO=4 -> 404
/stream/socket.io/?EIO=4 -> uWebSockets "File Not Found"
So the capability is real and enabled — Hyperion streaming is socket.io over uWebSockets, and the module documents a raw WebSocket against a bare path. A reader following it cannot connect, and there is no mention of the socket.io-client dependency.
Suggested fix: document the socket.io client and the correct mount path (we did not resolve it — happy to test a correction if you can point at the right one).
This is the one we would most like fixed, because the polling advice in rpc-queries.md is good and streaming is the better answer for exactly the case that advice is about.
6. Minor: the oracle feed table differs across four modules
| module |
feeds listed |
rpc-queries.md |
5 (3, 4, 5, 7, 13) |
resources.md |
5 (same) |
loan-protocol.md |
13 |
oracles-randomness.md |
15 |
Not wrong anywhere — the short lists just read as complete. A cross-reference to the fullest table, or a note that the short ones are excerpts, would remove the ambiguity.
Not filed here
simpledex.md's early-bird / graduation / creation-fee values all matched mainnet exactly, as did the token contract registry, the 8-endpoint capability table, the LOAN unbounded-supply correction, and the endpoint etiquette section. The etiquette section in particular changed how we built — the 429-vs-403 distinction and "don't keep retrying, that confirms you're a bot" is advice we had not seen stated anywhere else, and our detector now aborts permanently on 403 rather than rotating.
Thanks for maintaining this — the drift-audit cadence is unusual and it shows.
Hi — we read the full skill (all 24 modules, v2.3.2) while building a read-only security agent on XPR Network, then verified the load-bearing claims against mainnet. Six findings below, all reproducible.
None of these is a security vulnerability, so per
SECURITY.mdthis is the public tracker rather thansecurity@metallicus.com. Happy to send a PR for any or all of them if that is easier than fixing from here.Verified against
main(659d4d3) — none appears fixed since the 2.3.2 release. All chain reads viaproton.protonuk.io/proton.eosusa.ioon 2026-08-22.Context for the framing: the v2.3.0 "Fast finality" → "Fast inclusion (~2s first block; LIB ~3 minutes)" correction is what made us check our own timestamps, and we found we had been stamping
head_block_numand presenting it as settled. That fix earned its keep. The monthly drift-audit discipline is visible in the CHANGELOG and it is why we trusted the skill enough to build on it.1.
metalx-dex.mdanddefi-trading.mdcontradict each other on what a wrong DEX memo does — and it is the one flagged CRITICALSKILL.mdsafety reminder #6 calls the empty-memo rule a top-level fund-loss item. The two detail pages then say different things:metalx-dex.md:defi-trading.md(indepositToDex):"Irrecoverable" and "misrouted but present" are different outcomes and imply different recovery advice. A reader who hits this needs to know which one is true.
Suggested fix: pick whichever is correct and make the other match. If recovery depends on circumstances, saying so explicitly is better than two confident and incompatible sentences.
Worth noting the contrast the skill already draws elsewhere:
alcor-dex.md's malformed-memo table says Alcor fails closed — the transfer reverts, funds stay in the wallet. That is a genuinely useful comparison and it makes thedexbehaviour more important to state precisely.2.
accounts-permissions.md: "XPR accounts cannot contain dots" is not correctskill/accounts-permissions.md:Repeated in
resources.md("Account names 1-12 characters (a-z, 1-5)").Mainnet, 2026-08-22:
Plus
eosio.token,eosio.proton,metal.kyc,wlcm.proton,loan.token,lending.loan,shares.loan,eosio.msig,dex.protonnz— several of which appear in the skill's own tables on the same page as the rule (resources.md→ System Contracts).Why it matters beyond pedantry: we hit this building account-name validation. An agent that encodes the rule as written flags
xmd.token— the canonical Metal Dollar contract — as a malformed name.Likely origin:
smart-contracts.mdgives the identical rule for table names, where it is correct:Suggested fix: something like "user-registerable names cannot contain dots; dotted names exist as system/premium accounts (
eosio.token,xmd.token) and are valid to reference."3.
webauth-identity.md: the publishedgetKYCLevel()returnsNaNon mainnetwith a documented levels table (0 not verified through 3 enhanced).
On mainnet
kyc_levelholds a comma-separated claim list, not an integer. Read live fromeosio.proton::usersinfo:Same shape on
metallicus.Math.max(NaN, ...)isNaN, so the helper returnsNaNfor every KYC'd account — andgetKYCLevel(x) >= 2isfalsefor everyone, which fails open or closed depending on the caller.Also: the
UserInfointerface typeskyc_level: stringwhile the levels table describes it as 0–3, so the interface is right and the helper and table disagree with it.Suggested fix: count claims rather than parsing a level, and document that
kyc_levelis a claim string. If a numeric tier is wanted, derive it explicitly and label it as derived.Related, same module: the
UserCardexample rendersverifiedas "✓ Verified" beside a KYC badge. Those two fields are close to independent on mainnet —metallicusandprotonnzboth carry 7 claims withverified: 0— so the example can show a fully KYC'd account as unverified. Might be worth a note thatverifiedis an opt-in display flag rather than a KYC summary.4.
rpc-queries.md:/v2/history/get_transfersis documented with a worked example and returns 404Listed in the History Endpoints table and given a full
getTransfers()implementation.Same on
proton.protonuk.io. The route is not deployed on the endpoints the skill recommends./v2/history/get_actions?filter=eosio.token:transferreturns the same information and works.Suggested fix: replace the endpoint and the example, or mark it as Hyperion-deployment-dependent.
(Cost us a real bug: our code used the documented route and silently treated the 404 as "no transfers found", reporting that 25 of 25 accounts had no inbound XPR.)
5.
real-time-events.md: streaming is enabled server-side but the documented client cannot connectThe module is built on:
with
action_stream/delta_streamsubscriptions, andrpc-queries.mdrecommends streaming over polling.Measured across three endpoints:
So the capability is real and enabled — Hyperion streaming is socket.io over uWebSockets, and the module documents a raw
WebSocketagainst a bare path. A reader following it cannot connect, and there is no mention of thesocket.io-clientdependency.Suggested fix: document the socket.io client and the correct mount path (we did not resolve it — happy to test a correction if you can point at the right one).
This is the one we would most like fixed, because the polling advice in
rpc-queries.mdis good and streaming is the better answer for exactly the case that advice is about.6. Minor: the oracle feed table differs across four modules
rpc-queries.mdresources.mdloan-protocol.mdoracles-randomness.mdNot wrong anywhere — the short lists just read as complete. A cross-reference to the fullest table, or a note that the short ones are excerpts, would remove the ambiguity.
Not filed here
simpledex.md's early-bird / graduation / creation-fee values all matched mainnet exactly, as did the token contract registry, the 8-endpoint capability table, the LOAN unbounded-supply correction, and the endpoint etiquette section. The etiquette section in particular changed how we built — the 429-vs-403 distinction and "don't keep retrying, that confirms you're a bot" is advice we had not seen stated anywhere else, and our detector now aborts permanently on 403 rather than rotating.Thanks for maintaining this — the drift-audit cadence is unusual and it shows.