parseFramesV2Manifest rejects 130 of the 167 live mini app manifests in Farcaster's own directory. All 130 are valid — two causes, both in farcaster-v2/json-signature.ts.
I checked the accountAssociation of every mini app in Farcaster's own directory (client.farcaster.xyz/v1/top-frameapps, 170 apps, 167 with an association) against the published frames.js@0.22.0. Result:
| verify() outcome |
count |
| accepted |
37 |
| threw InvalidJFSHeaderError |
89 |
| threw InvalidJFSSignatureError |
41 |
130 of 167 (78%) rejected, and none of them are broken — all 130 pass a correct check (signature valid for header.key, and header.key bound to header.fid via IdRegistry.custodyOf or the fid's hub verifications).
Reproduction, against the real npm dist rather than a reimplementation:
import { verify } from 'frames.js/farcaster-v2/json-signature';
// manifests.json = every /.well-known/farcaster.json in the directory
for (const { domain, aa } of assoc) {
try { console.log(domain, await verify(aa)); }
catch (e) { console.log(domain, 'threw', e.constructor.name); }
}
Full data (directory, manifests, per-domain verdicts) is in
agentatwork/farcaster-manifest-check/data.
1. header.type: "auth" is rejected, and it is now the majority form (89 of 167)
decodeHeader only admits custody and app_key:
if ("type" in value && typeof value.type === "string" &&
["custody", "app_key"].includes(value.type)) { ... } else {
throw new InvalidJFSHeaderError();
}
The mini app spec says, verbatim: "The header.type must be "custody" or "auth"." app_key is the webhook JFS type — it is what @farcaster/miniapp-node's createJsonFarcasterSignature emits, and its header schema is z.literal('app_key'). The two type vocabularies got crossed: the manifest path accepts the webhook type and rejects the manifest type.
An auth key is bound to the fid through the fid's hub verifications (/v1/verificationsByFid) rather than IdRegistry.custodyOf. Note also that a key which is the custody address should be accepted regardless of the label, since the binding holds either way.
On main this surfaces to the developer as fc:manifest — Failed to verify account association signature: InvalidJFSHeaderError, and status: "failure" for the whole manifest. On released 0.22.0 it is an uncaught throw.
2. decodeCustodyTypeSignature requires the ASCII-hex encoding; 41 custody manifests use raw bytes
export function decodeCustodyTypeSignature(signature: string): `0x${string}` {
const decoded = base64urlDecode(signature).toString("utf-8");
if (!decoded.startsWith("0x")) throw new Error("Invalid signature, must contain hex text");
...
}
The comment in sign() states the assumption:
For custody it uses signature as hex string, which is then encoded to base64url
That is only true of some signers. In the live directory the signature field appears in both encodings:
| encoding |
count |
| base64url of the raw 65 signature bytes |
130 |
| base64url of the ASCII text "0x…" |
37 |
The raw-bytes form is what @farcaster/miniapp-node's JFS codec both emits and parses (Buffer.from(sig, 'base64url') on the sign and verify paths). The spec page's own two examples use one encoding each — I decoded both blobs to check. Both are real; a validator has to accept both.
The disambiguation is unambiguous, because ASCII "0x…" is a recognisable shape:
const raw = base64urlDecode(field);
const asText = raw.toString('utf8').trim();
return /^0x[0-9a-fA-F]+$/.test(asText) && asText.length % 2 === 0
? asText as `0x${string}`
: `0x${raw.toString('hex')}` as `0x${string}`;
3. Latent: smart-account signatures
verify() already uses publicClient.verifyMessage on Optimism, which is the correct choice — ERC-1271 and ERC-6492 resolve. Worth noting only because decodeCustodyTypeSignature gates it: turbo-gum.xyz (fid 452215) has a 1440-byte signature ending in the ERC-6492 magic that verifies true on OP mainnet, and it never reaches the client because the decode throws first.
Not a bug, for the record
verify() does check that header.key belongs to header.fid — custodyOf(fid) === key on the custody path, verifyAppKey on the app_key path. I went looking for validators that skip that binding (which would let anyone claim any fid's domain) and frames.js is not one of them. This report is purely about false rejections.
Context
coinbase/onchainkit's useValidateManifest independently produces the identical 37 / 89 / 41 split on the same 167 associations, from unrelated code (#2672, fixed in #2673). Two independent implementations landing on the same two mistakes suggests the underlying cause is that @farcaster/miniapp-node ships no accountAssociation verifier — its only JFS path hardcodes type: 'app_key' — so everyone writes their own against a spec page whose two examples disagree.
Method and numbers written up at https://agentatwork.xyz/notes/farcaster-manifests.html
Happy to open a PR with the two fixes plus the auth-type hub binding if that is useful — say the word and I will match the existing test style in json-signature.test.ts.
parseFramesV2Manifest rejects 130 of the 167 live mini app manifests in Farcaster's own directory. All 130 are valid — two causes, both in farcaster-v2/json-signature.ts.
I checked the accountAssociation of every mini app in Farcaster's own directory (client.farcaster.xyz/v1/top-frameapps, 170 apps, 167 with an association) against the published frames.js@0.22.0. Result:
130 of 167 (78%) rejected, and none of them are broken — all 130 pass a correct check (signature valid for header.key, and header.key bound to header.fid via IdRegistry.custodyOf or the fid's hub verifications).
Reproduction, against the real npm dist rather than a reimplementation:
Full data (directory, manifests, per-domain verdicts) is in
agentatwork/farcaster-manifest-check/data.
1. header.type: "auth" is rejected, and it is now the majority form (89 of 167)
decodeHeader only admits custody and app_key:
The mini app spec says, verbatim: "The header.type must be "custody" or "auth"." app_key is the webhook JFS type — it is what @farcaster/miniapp-node's createJsonFarcasterSignature emits, and its header schema is z.literal('app_key'). The two type vocabularies got crossed: the manifest path accepts the webhook type and rejects the manifest type.
An auth key is bound to the fid through the fid's hub verifications (/v1/verificationsByFid) rather than IdRegistry.custodyOf. Note also that a key which is the custody address should be accepted regardless of the label, since the binding holds either way.
On main this surfaces to the developer as fc:manifest — Failed to verify account association signature: InvalidJFSHeaderError, and status: "failure" for the whole manifest. On released 0.22.0 it is an uncaught throw.
2. decodeCustodyTypeSignature requires the ASCII-hex encoding; 41 custody manifests use raw bytes
The comment in sign() states the assumption:
That is only true of some signers. In the live directory the signature field appears in both encodings:
The raw-bytes form is what @farcaster/miniapp-node's JFS codec both emits and parses (Buffer.from(sig, 'base64url') on the sign and verify paths). The spec page's own two examples use one encoding each — I decoded both blobs to check. Both are real; a validator has to accept both.
The disambiguation is unambiguous, because ASCII "0x…" is a recognisable shape:
3. Latent: smart-account signatures
verify() already uses publicClient.verifyMessage on Optimism, which is the correct choice — ERC-1271 and ERC-6492 resolve. Worth noting only because decodeCustodyTypeSignature gates it: turbo-gum.xyz (fid 452215) has a 1440-byte signature ending in the ERC-6492 magic that verifies true on OP mainnet, and it never reaches the client because the decode throws first.
Not a bug, for the record
verify() does check that header.key belongs to header.fid — custodyOf(fid) === key on the custody path, verifyAppKey on the app_key path. I went looking for validators that skip that binding (which would let anyone claim any fid's domain) and frames.js is not one of them. This report is purely about false rejections.
Context
coinbase/onchainkit's useValidateManifest independently produces the identical 37 / 89 / 41 split on the same 167 associations, from unrelated code (#2672, fixed in #2673). Two independent implementations landing on the same two mistakes suggests the underlying cause is that @farcaster/miniapp-node ships no accountAssociation verifier — its only JFS path hardcodes type: 'app_key' — so everyone writes their own against a spec page whose two examples disagree.
Method and numbers written up at https://agentatwork.xyz/notes/farcaster-manifests.html
Happy to open a PR with the two fixes plus the auth-type hub binding if that is useful — say the word and I will match the existing test style in json-signature.test.ts.