| Audited | docs.solanamobile.com, all published pages, pages fetched 11 October 2026 |
|---|---|
| Date | 11 October 2026 |
| How it ran | local run on our workstation, full audit, Standard review |
| Verdict after review | Pass with notes (rule: Fail if a High finding remains after review, otherwise Pass with notes) |
Related: Solana Mobile documentation and SDKs (June 2026) · Solana Mobile stack, third audit (September 2026)
Each finding keeps the number it has in the audit report. The rating shown first is the one after review; the first automated rating is listed with it. 27 findings were first rated Medium or High; 2 of them are Medium or High after review. Text marked "From the report" is quoted from the audit report; fixes are suggestions and were not tested. Locations are paths inside the fetched copy of the documentation.
Medium after review (2)
get-started/react-native/quickstart.md:122 (same text in _llms-full.txt:4116)From the report
const result = await signIn({ address: account?.address, chainId: chain,
domain: "your-app-domain.com", statement: "Sign in to Your App", uri: identity.uri });
console.log("Signed in:", result.account.address);
this is the most-read entry point for React Native. It promises ownership verification but builds the payload on the client and only logs the result.
take the payload (nonce, issuedAt, expirationTime) from the backend and send the result back for verification. Link to the server flow in the Seeker detection recipe.
Review: get-started/react-native/quickstart.md:111 says "Use signIn to connect to a wallet and verify ownership in a single step", and the sample at :122-131 builds {address, chainId, domain, statement, uri} on the client, awaits the result and only does console.log("Signed in:", ...). No nonce, issuedAt, expirationTime or verification step appears on the page (grep for nonce/verify finds only the signMessages text), and the "Next steps" cards at :250-257 do not point to the server flow. The correct pattern exists only in recipes/general/detecting-seeker-users.md:117-190 and as a warning at invoke-mwa-sessions-directly.md:373-375. The Kotlin quickstart (get-started/kotlin/quickstart.md:90,104-105, finding 2) repeats the claim and the sample. This is the entry-point sample for new apps, the promise ("verify ownership") is false as written, and a copy-paster ships authentication that proves nothing with no signal. It is guidance, not a bug, so MEDIUM and not HIGH. The Kotlin reference page (finding 1) stays LOW because it has its own "Verifying the sign-in result" section.
build.gradledapp-store/build-and-sign-an-apk.md:121 (also dapp-store/publishing-from-google-play.md:68-78 and recipes/general/publishing-from-google-play.md:114-122)From the report
storePassword "your_keystore_password"
keyAlias "my-app-name"
keyPassword "your_key_password"
build.gradle is normally committed. Following the sample puts release-signing passwords into source control, and the dApp Store key cannot be rotated for a published app. Only the credentials.json variants carry a do-not-commit warning.
read the passwords from environment variables or an untracked gradle.properties, and add the warning.
Review: dapp-store/build-and-sign-an-apk.md:116-124 puts storePassword "..." and keyPassword "..." in app/build.gradle, and publishing-from-google-play.md:62-79 and recipes/general/publishing-from-google-play.md:114-122 do the same for two keystores. build.gradle is normally committed, and the sample also points at keystores/*.keystore inside the project. The only commit warning in these pages is for the credentials.json variants (publishing-from-google-play.md:151, recipes/.../publishing-from-google-play.md:69), which shows the authors know the risk; the Gradle variants carry none, and build-and-sign-an-apk.md:37 stresses that all updates must use the same key. The values are placeholders and the failure is a late compromise with no signal, so MEDIUM is fair but it is a security-hygiene omission, not a bug.
Low after review (66)
android-native/using_mobile_wallet_adapter.md:166From the report
SignInWithSolana.Payload("solana.com", "Sign in to Ktx Sample App")
the payload is built on the client with no nonce, issue time or expiry, and the domain is not the app's own. The verification advice at line 188 says only "verify that the message was correctly signed". A backend that follows it accepts a replayed or foreign sign-in result as proof of wallet ownership.
fetch the payload from the backend with a single-use nonce, issuedAt and expirationTime. Verify the result server-side, including the domain and nonce consumption. Copy the production warning that the React Native page has.
Review: Payload is SignInWithSolana.Payload("solana.com", "Sign in to Ktx Sample App") (real). But the next section (:179-188) is titled "Verifying the sign-in result" and links a Kotlin and a server-side example. Reference page, not a "you are signed in" claim.
get-started/web/ux-guidelines.md:91From the report
const input: SolanaSignInInput = { domain: window.location.host,
statement: "Sign in to My Web App", uri: window.location.origin, }
const output = await signIn(input);
the input carries no nonce or expiry, and the output is never verified. The page is about UX, but developers copy the snippet as is.
obtain the nonce and expiry from the server and post the output back for verification.
Review: const output = await signIn(input) at :96; no nonce.
get-started/react-native/invoke-mwa-sessions-directly.md:312From the report
sign_in_payload: { domain: "yourdomain.com",
statement: "Sign into React Native Sample App", uri: "https://yourdomain.com", },
the warning at line 373 is correct but comes after the code. verifySIWS checks the signature only against the input the caller supplies, which is not an authentication check.
put a placeholder such as await fetchSignInPayloadFromBackend() in the sample, and label verifySIWS as an illustration that does not replace server verification.
Review: Production warning exists at :373-375 (after the code, as stated).
recipes/general/detecting-seeker-users.md:217From the report
const signInPayload = await consumeSignInPayload(nonce);
if (!signInPayload) { return null; }
a request that carries a valid nonce and a bad signature burns the nonce, so the real user's sign-in then fails and has to restart.
look up the payload without consuming it, verify the signature and domain, then mark the nonce used atomically.
Review: As quoted. Burn-on-first-use is also a defensible choice (shape is checked first at :206-211).
signTransactions is deprecated on one page and offered as a normal option on othersget-started/react-native/invoke-mwa-sessions-directly.md:499From the report
Alternatively, you can request the wallet to just sign a transaction by issuing a `signTransactions` request.
android-native/using_mobile_wallet_adapter.md:331 marks the method deprecated under the current protocol version and recommends signAndSendTransactions. The React Native guide and its reference (get-started/react-native/mobile-wallet-adapter.md:480) carry no such note.
add the same deprecation warning to the React Native pages.
get-started/react-native/mobile-wallet-adapter.md:307From the report
If, during the current session, the specified auth token was returned by the most recent call to authorize or reauthorize, ...
reauthorize and clone_authorization (lines 214, 313) are legacy methods. The page documents silent reauthorization as authorize with auth_token, so readers look for methods that are not there.
remove the legacy method names or mark them legacy-only.
Review: reauthorize / clone_authorization at :213,312.
recipes/mobile-wallet-adapter/migrating-to-wallet-standard.md:98From the report
The legacy `@solana-mobile/wallet-adapter-mobile` library will be deprecated and enter maintenance mode, and only receive updates for bug fixes.
integrators cannot plan the migration. Other pages still document the legacy API without a pointer to this notice.
give dates or version gates for maintenance mode and end of support, and link this notice from the legacy reference.
Review: Line as quoted.
get-started/react-native/quickstart.md:75From the report
const signature = await signMessages(messageBytes);
console.log("Signature:", signature);
a user cancel becomes an unhandled promise rejection in every quickstart handler (connect, sign, sign in, send).
wrap the calls in try/catch and handle the declined case explicitly.
Review: await signMessages(...) then console.log with no try/catch.
connect() on a wallet that may not existsolana-mobile-stack/seeker-connect-quickstart.md:180From the report
const { accounts } = await wallet.features[StandardConnect].connect();
console.log("connected as", accounts[0]?.address);
wallet comes from a find() that can return undefined, and a cancelled consent throws.
guard for a missing wallet, catch rejection, and require at least one account.
Review: find() at :165-168 can return undefined.
solana-mobile-stack/seeker-genesis-token.md:218From the report
} catch (error) {
console.error("Error verifying SGT ownership:", error.message);
return null;
an RPC outage looks exactly like a user who owns no Seeker, so callers deny genuine users with no chance to retry.
rethrow, or return a distinct error result.
Review: catch returns null.
android-native/rpc-requests.md:27From the report
implementation("com.solanamobile:rpc-core:0.2.6")
get-started/kotlin/installation.md:32 pins 0.2.7, and two other pages use an undefined ${versions.KOTLIN_RPC_CORE_VERSION} placeholder. Mixed versions of the same stack cause subtle API mismatches.
publish one tested version set and reference it from every page.
Review: 0.2.6 here, 0.2.7 at get-started/kotlin/installation.md:32, placeholder at building-json-rpc-requests.md:31 and using-anchor-programs.md:32.
recipes/mobile-wallet-adapter/local-network-access.md:29From the report
Update `@solana-mobile/wallet-standard-mobile` to **v0.5.0 or later** to mitigate the issue.
dapp-store/build-and-sign-an-apk.md:171 and recipes/general/publishing-a-web-app.md:39 require v0.5.1. The install command at line 48 uses @latest.
state one floor, or explain the browser case against the shell case, and pin the version.
Review: v0.5.0 here, v0.5.1 at build-and-sign-an-apk.md:171 and publishing-a-web-app.md:39; @latest at :48.
mobile-wallet-adapter/mobile-apps.md:38From the report
<Card horizontal title="React Native" icon="react" href="/get-started/react-native/invoke-mwa-sessions-directly" />
get-started/mobile-wallet-adapter.md:48 points to the Wallet UI hooks, while this page points to the low-level protocol. Readers end up mixing both in one app.
point both pages to one recommended entry point, and say not to mix the two stacks.
get-started/web/apps.md:25From the report
It is compatible with any web app using the `@anza/wallet-adapter` libraries.
every sample imports @solana/wallet-adapter-*, and AI tools copy the wrong scope into install commands.
use the real package scope.
get-started/web/installation.md:49From the report
registerMwa({ appIdentity: { name: "My app", uri: "https://myapp.io",
wallets reject non-HTTPS origins other than localhost without a clear error. We searched the web pages and found no mention of the requirement.
state that the web app must be served over HTTPS (localhost only for development), and show a protocol check with a clear error.
Review: No HTTPS-requirement text in get-started/web/*. Whether wallets reject non-HTTPS cannot be checked from the docs.
solana-mobile-stack/seeker-genesis-token.md:85From the report
const HELIUS_RPC_URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`;
the Seeker detection recipe says this runs on the backend, but this page does not. Copied into an app bundle, the key leaks. HELIUS_API_KEY is never declared.
mark the script backend-only and read the key from server configuration.
Review: HELIUS_API_KEY is used at :85 and never declared; the page never says "backend" (grep finds none).
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:352From the report
await wallet.deauthorize({ auth_token: currentAccount.authToken });
AsyncStorage.clear();
AsyncStorage.clear() deletes every key the app ever stored, including user settings and other libraries' caches, not just the two authorization keys. A developer who copies this sample loses user data every time a user disconnects.
await AsyncStorage.multiRemove(["authToken", "base64Address"]).
Review: 1: see 3; the page has a verification section and links. 23: AsyncStorage.clear() at caching-wallet-authorization.md:352 really wipes every key, but it sits in an advanced accordion for the bare library (:275), the neighbouring lines call deauthorize and setCurrentAccount(null) correctly, and a one-line replacement is obvious on first test. 24: the boot code at :295-301 does set the cached account as current, and the connect handler (:322-327) does not pass auth_token, so a revoked app still looks connected; the next sign request fails visibly and the cache is overwritten on the next authorize. Staleness, not a silent loss. 58: the page's own AsyncStorage warning (:32-38) sits above and the secure-store cache is the page's main recommendation (:126-267); the sample is the unencrypted bare-library illustration the warning already covers.
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:295From the report
if (cachedBase64Address && cachedAuthToken) {
const pubkeyAsByteArray = toByteArray(cachedBase64Address);
on boot the cached account becomes the connected account with no reauthorization, and the connect handler never passes the cached token to authorize. After the user revokes access in the wallet, the app still shows them as connected, and the cache never takes up a refreshed token.
reauthorize with the cached auth_token inside the first session. On failure clear the cache, and on success store the returned token and accounts.
Review: 1: see 3; the page has a verification section and links. 23: AsyncStorage.clear() at caching-wallet-authorization.md:352 really wipes every key, but it sits in an advanced accordion for the bare library (:275), the neighbouring lines call deauthorize and setCurrentAccount(null) correctly, and a one-line replacement is obvious on first test. 24: the boot code at :295-301 does set the cached account as current, and the connect handler (:322-327) does not pass auth_token, so a revoked app still looks connected; the next sign request fails visibly and the cache is overwritten on the next authorize. Staleness, not a silent loss. 58: the page's own AsyncStorage warning (:32-38) sits above and the secure-store cache is the page's main recommendation (:126-267); the sample is the unencrypted bare-library illustration the warning already covers.
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:329From the report
AsyncStorage.setItem("authToken", auth_token);
AsyncStorage.setItem("base64Address", firstAccount.address);
the two keys form a pair, but they are written by two unawaited calls and then state is set. A failure in between leaves storage and memory out of step.
use one saveAccount() and one clearAccount() helper that awaits a single multiSet (or one JSON value) and then sets state.
Review: Two un-awaited setItem calls at :329-330. Same lines as 58.
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:136From the report
const STORAGE_KEY = "authorization-cache";
"authToken" and "authorization-cache" live in storage the whole app and other libraries share, which invites collisions.
prefix the keys with an owner name, such as <app>_wallet_authorization.
Review: STORAGE_KEY = "authorization-cache" is in the SecureStore cache, which is app-scoped; collision risk is low. A nit.
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:136From the report
const STORAGE_KEY = "authorization-cache";
an authorization is issued per chain and identity. When a build switches from devnet to mainnet or changes identity, the old authorization is served as current.
include the chain and identity.uri in the key, or store them in the value and compare them on read.
Review: Same line as 26.
get-started/react-native/invoke-mwa-sessions-directly.md:224From the report
const authorizationResult = await wallet.authorize({
auth_token: storedAuthToken ? storedAuthToken : undefined,
the token that authorize returns can change, and nothing writes it back, so the next launch reuses a stale token.
show persistAuthToken(authorizationResult.auth_token) next to the read, and what to do when reauthorization fails.
Review: Sample ends with "Rest of transact code goes below..." (:230).
get-started/react-native/invoke-mwa-sessions-directly.md:278From the report
// Pass in the prior auth token to invalidate it.
await wallet.deauthorize({ auth_token: previouslyStoredAuthToken });
the wallet invalidates the token but the app's stored copy survives, so the next launch sends a dead token.
clear the stored token in the same step as the deauthorize call.
Review: As quoted.
android-native/using_mobile_wallet_adapter.md:73 (also get-started/kotlin/setup.md:91)From the report
val previouslyStoredAuthToken = maybeGetStoredAuthToken()
walletAdapter.authToken = previouslyStoredAuthToken
only the restore half is shown. Nothing persists the token after connect, and disconnect never clears the stored copy, so an invalidated token comes back on the next launch.
add a saveAuthToken() and clearAuthToken() pair backed by encrypted storage, and call it on connect, on token change and on disconnect.
Review: Also get-started/kotlin/setup.md:91; text at :68 says the client stores the token itself.
solana-mobile-stack/seeker-connect-quickstart.md:186From the report
"Connected" means your app holds a wallet-issued authorization token ... silently reauthorizes with the cached token
we searched the Seeker Connect and web pages for disconnect, deauthorize and clear, and found nothing. Readers do not know where the token lives or how to revoke it.
document where the token is cached, how disconnect clears it, and what happens when it is invalid.
Review: grep for disconnect/deauthorize/clear/revoke in seeker-connect*.md and get-started/web/*.md finds nothing. True.
recipes/solana-development/anchor-integration.md:257From the report
the hook polls for confirmation afterwards and invalidates the counter query on settle
polling with no interval, timeout or backoff can hammer a public RPC endpoint. A subscription is available.
prefer a signature subscription, and give the poll interval, upper bound and backoff.
Review: Prose at :257.
solana-mobile-stack/seeker-genesis-token.md:154From the report
} while (paginationKey); // Continue until no more pages
the caller supplies the wallet address, so a wallet with very many token accounts keeps one backend request looping for a long time.
cap the page count and the elapsed time, and fail when either cap is reached.
Review: while (paginationKey) at :154.
solana-mobile-stack/seeker-genesis-token.md:38From the report
To implement this, the app should check for 3 properties:
an eligibility check and seen-before ledger that run on the client can be bypassed.
state that all three checks and the mint ledger run on the server.
Review: "the app should check for 3 properties".
recipes/general/detecting-seeker-users.md:279From the report
// Store the mint address to enforce uniqueness across transfers.
return await checkWalletForSGT(walletAddress);
when the check and the record are separate steps, two concurrent claims with the same token can both pass.
show an atomic insert-if-absent keyed on the mint address, for example a unique constraint.
Review: Comment "Store the mint address..." with no insert-if-absent shown.
get-started/react-native/quickstart.md:236 (also the connect button at line 39)From the report
return <Button title="Send Transaction" onPress={handleSendTransaction} />;
a double tap can start two wallet sessions or two sends.
disable the button or reuse the pending promise while a request is in flight.
Review: Also connect button at :39.
minContextSlot is required, but the reference sample omits itget-started/react-native/mobile-wallet-adapter.md:431From the report
const transactionSignatures = await wallet.signAndSendTransactions({
transactions: [transferTransactionMessage],
quickstart.md:243 states that signAndSendTransactions requires minContextSlot, without saying the warning applies only to the hook. Readers cannot tell which contract is right.
align the two samples, or scope the warning to the hook.
Review: quickstart.md:243 says required; reference lists it as plain minContextSlot: number (:407); invoke-mwa-sessions-directly.md:467 also omits it.
get-started/react-native/create-solana-mobile-app.md:30From the report
npm create solana-dapp@latest
recipes/solana-development/anchor-integration.md:53 uses npx solana-mobile@latest create. Readers do not know which one is current.
document one canonical command, or explain when to use each.
Review: npm create solana-dapp@latest vs npx solana-mobile@latest create at anchor-integration.md:53.
mobile-wallet-adapter/mobile-apps.md:62From the report
| Mobile Web - Chrome (Android) | ✅ | Automatic integration if using `@solana/wallet-adapter-react`. |
recipes/mobile-wallet-adapter/migrating-to-wallet-standard.md:87-91 says @solana/wallet-adapter-react 1.0.0 and later no longer include MWA by default. A web developer who follows this page gets no mobile wallet option and no explanation. Line 70 also still lists Seed Vault Wallet as "Coming soon!".
update the table to say that registerMwa is required, and refresh the wallet list.
Review: 39: the compat row (mobile-apps.md:62) is genuinely stale against migrating-to-wallet-standard.md:87-92 (and :70 "Coming soon!"), but the migration page explains it directly, the web installation page teaches registerMwa, and mobile-apps.md is itself missing from every index (finding 76). Visible stale statement, not a trap. 40: the confirmation at quickstart.md:228-231 indeed ignores value.err, but the log line is "Transaction signature:", which is not a success claim, and the wallet already returned a signature from signAndSendTransactions. invoke-mwa-sessions-directly.md:480 shows the stricter check.
get-started/react-native/quickstart.md:228From the report
await connection.confirmTransaction(
{ signature, ...latestBlockhash }, "confirmed",
);
the confirmation's value.err is never read, so a transaction that failed on chain is logged as sent. invoke-mwa-sessions-directly.md:480 does check it.
capture the result and treat value.err as a failure.
Review: 39: the compat row (mobile-apps.md:62) is genuinely stale against migrating-to-wallet-standard.md:87-92 (and :70 "Coming soon!"), but the migration page explains it directly, the web installation page teaches registerMwa, and mobile-apps.md is itself missing from every index (finding 76). Visible stale statement, not a trap. 40: the confirmation at quickstart.md:228-231 indeed ignores value.err, but the log line is "Transaction signature:", which is not a success claim, and the wallet already returned a signature from signAndSendTransactions. invoke-mwa-sessions-directly.md:480 shows the stricter check.
dapp-store/submit-an-update.md:50From the report
* The APK is signed with the same Android signing key you used for your initial release.
* The `versionName` and `versionCode` value is properly incremented between each update.
submission uploads to Arweave and mints a release NFT before review. A wrong key or version code is found out late.
add apksigner verify --print-certs with a fingerprint comparison, and a versionCode check.
Review: Bullets as quoted (:48-50).
dapp-store/publishing-from-google-play.md:183From the report
apksigner verify --print-certs app-release.apk
the build produces app-dappStore-release.apk. The page's critical requirement is a key that differs from the Google Play key, and printing certs does not check that.
use the real path, and compare the certificate fingerprint against the Play signing certificate.
Review: Real: build output is app-dappStore-release.apk (:112), verify uses app-release.apk.
dapp-store/build-and-sign-an-apk.md:265From the report
Every dApp Store update needs a higher `versionCode`. Bump `SOLANA_MOBILE_VERSION_CODE` ...
the first release has Verify and Test steps, but the update path ends at the build command.
repeat the verify and test steps for updates, and add a check that the signing certificate matches the previous release.
Review: "Updating your app" section ends at the build command.
dapp-store/publishing-cli.md:82From the report
dapp-store \
--apk-file ./app/build/outputs/apk/release/app-release.apk \
the pipeline has no pre-publish signature check and no read-back of the release status.
add apksigner verify before publishing and a status check after.
Review: As quoted.
dapp-store/publishing-cli.md:64From the report
dapp-store --apk-url https://example.com/your_apk_name.apk --keypair ./path/to/keypair.json ...
an APK fetched by URL goes into an on-chain release with no hash or signature check described.
document an expected-hash option or a verify step, or state plainly that the tool performs no check.
Review: As quoted.
dapp-store/build-and-sign-an-apk.md:271From the report
Because the shell loads your live site, changes you ship to the web app itself reach users without a new APK.
the verified APK is only part of what users run. A site release can break wallet flows inside the shell with no review step in between.
recommend re-testing connect, sign and send inside the shell for every site release.
Review: Sentence as quoted.
recipes/solana-development/anchor-integration.md:261From the report
Add an instruction to the program, then rebuild and regenerate:
changing the account layout breaks existing accounts, and nothing tells the reader to re-run the whole flow.
add a re-test step (initialize, increment, read) and a note on account-layout migration.
Review: "Add an instruction to the program, then rebuild and regenerate".
recipes/solana-development/using-anchor-programs.md:149From the report
.setRecentBlockhash(blockhashResponse.result!!.blockhash)
the same "create client, fetch blockhash" block appears in five Kotlin pages with three different result shapes and versions.
define one canonical snippet with error handling and link to it.
Review: Actual count is 3 Kotlin pages / 4 setRecentBlockhash calls (kotlin/quickstart.md:158, using-anchor-programs.md:149,186) plus rpc-requests.md:61, not six copies in five pages.
recipes/general/publishing-from-google-play.md:5From the report
# Publishing from Google Play to the dApp Store
compared with dapp-store/publishing-from-google-play.md, this copy uses different keystore and alias names and different Gradle syntax, and only the other copy has a verify step.
keep one page and link to it from the other.
Review: Two files, 172 vs 197 lines, diff differs from line 7.
get-started/development-setup.md:80From the report
it does not store a persistent keypair and the wallet is reset each time the app is exited.
recipes/general/test-with-any-android-device.md:34-38 describes key importing and a required secure lock screen. A developer who follows the setup page alone installs a wallet that fails without a lock screen.
describe the wallet in one place, including the lock-screen requirement.
Review: Lock-screen warning is at recipes/general/test-with-any-android-device.md:34-38; setup page says "does not store a persistent keypair".
seeker/release-notes.md:5From the report
# Seeker Release Notes
seeker/release-notes/index.md is identical, and neither lists MR4 to MR9.
keep one index and link the releases from it.
Review: diff with seeker/release-notes/index.md shows them identical; MR4-MR9 pages exist in seeker/release-notes/.
recipes/mobile-wallet-adapter/migrating-to-wallet-standard.md:25From the report
### 1. Install Mobile Wallet Standard
the old and new registrations can coexist, so stale call sites keep working silently and the wallet may be listed twice.
add a step to remove the legacy package and its imports, and a check that only one entry is listed.
recipes/solana-development/anchor-integration.md:293From the report
npm run anchor:deploy:devnet
the funding step is checked, but the deploy is not, so a failed deploy surfaces later as a confusing app error.
add solana program show <id> --url devnet, or an initialize-and-increment check, after the deploy.
Review: Funding step has a check at :281-285, deploy has none.
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:329 (same text in _llms-full.txt:5714)From the report
AsyncStorage.setItem("authToken", auth_token);
AsyncStorage.setItem("base64Address", firstAccount.address);
the page's own warning (lines 32-38) says AsyncStorage is unencrypted and readable from backups. The copy-paste sample does exactly that with a bearer token that lets the wallet reauthorize silently.
use the SecureStore cache from the same page, or repeat the warning inside the sample.
Review: 1: see 3; the page has a verification section and links. 23: AsyncStorage.clear() at caching-wallet-authorization.md:352 really wipes every key, but it sits in an advanced accordion for the bare library (:275), the neighbouring lines call deauthorize and setCurrentAccount(null) correctly, and a one-line replacement is obvious on first test. 24: the boot code at :295-301 does set the cached account as current, and the connect handler (:322-327) does not pass auth_token, so a revoked app still looks connected; the next sign request fails visibly and the cache is overwritten on the next authorize. Staleness, not a silent loss. 58: the page's own AsyncStorage warning (:32-38) sits above and the secure-store cache is the page's main recommendation (:126-267); the sample is the unencrypted bare-library illustration the warning already covers.
get-started/web/ux-guidelines.md:49 (also line 96)From the report
const handleConnectClick = () => {
... await connect();
} else if (mobileWalletAdapter) {
the samples use await in non-async handlers, and mobileWalletAdapter, select and SolanaSignInInput are never declared or imported. The recommended connect flow fails to build.
make the handlers async, take select from useWallet(), find the MWA wallet in wallets, and import the type.
Review: All verified real and all fail at build or parse time, which is the best kind of failure. 59: await inside non-async handlers (ux-guidelines.md:45-49, :87-96), undeclared select, mobileWalletAdapter, SolanaSignInInput. 62: Base58.encodeToString(signedTxBytes) at using_mobile_wallet_adapter.md:272 while the lambda binds it; the sibling at kotlin/quickstart.md:170 is right. 64: rpc.sendTransaction (the client is rpcClient, :82) and if (response.result) on a nullable object (rpc-requests.md:63,90). 65: commitment undefined, block-body function without return (building-json-rpc-requests.md:139-160). 68: implementation('...${versions...}') in a .kts block (using-anchor-programs.md:33); the ${versions.*} placeholders are also undefined on that page. Each stops the developer immediately and carries no money or security effect.
signMessages reference reads a field that authorize does not returnget-started/react-native/mobile-wallet-adapter.md:592 (also line 615 and reference/typescript/mobile-wallet-adapter-legacy.md:440)From the report
addresses: [authorizationResult.address],
the documented result exposes accounts[].address, so the sample passes undefined as the signing address. authorizeSession is also undefined.
use authorizationResult.accounts[0].address and define the helper.
Review: The claim that authorize returns no address is true of the documented result (:226), but the helper's return shape is unknown. 61: charCodeAt truncation is real (also :574, :609, legacy :435), but the message is the ASCII "Hello world!", and quickstart.md:99-103 tells readers to use TextEncoder. 63: blockhashResponse.result!! (kotlin/quickstart.md:158, using-anchor-programs.md:149,186) throws on a failed RPC call; the exception is visible and the transaction was never sent.
get-started/react-native/mobile-wallet-adapter.md:586 (also invoke-mwa-sessions-directly.md:574 and the legacy page)From the report
const messageBuffer = new Uint8Array(
message.split("").map((c) => c.charCodeAt(0)),
charCodeAt returns UTF-16 units, and Uint8Array truncates them, so any character above U+00FF changes the signed bytes. The quickstart's own note says to use TextEncoder.
new TextEncoder().encode(message).
Review: The claim that authorize returns no address is true of the documented result (:226), but the helper's return shape is unknown. 61: charCodeAt truncation is real (also :574, :609, legacy :435), but the message is the ASCII "Hello world!", and quickstart.md:99-103 tells readers to use TextEncoder. 63: blockhashResponse.result!! (kotlin/quickstart.md:158, using-anchor-programs.md:149,186) throws on a failed RPC call; the exception is visible and the transaction was never sent.
android-native/using_mobile_wallet_adapter.md:272From the report
txSignatureBytes?.let {
println("Transaction signature: " + Base58.encodeToString(signedTxBytes))
the main signing sample fails to compile.
Base58.encodeToString(it), as get-started/kotlin/quickstart.md:170 does.
Review: All verified real and all fail at build or parse time, which is the best kind of failure. 59: await inside non-async handlers (ux-guidelines.md:45-49, :87-96), undeclared select, mobileWalletAdapter, SolanaSignInInput. 62: Base58.encodeToString(signedTxBytes) at using_mobile_wallet_adapter.md:272 while the lambda binds it; the sibling at kotlin/quickstart.md:170 is right. 64: rpc.sendTransaction (the client is rpcClient, :82) and if (response.result) on a nullable object (rpc-requests.md:63,90). 65: commitment undefined, block-body function without return (building-json-rpc-requests.md:139-160). 68: implementation('...${versions...}') in a .kts block (using-anchor-programs.md:33); the ${versions.*} placeholders are also undefined on that page. Each stops the developer immediately and carries no money or security effect.
get-started/kotlin/quickstart.md:158 (also recipes/solana-development/using-anchor-programs.md:149 and :186)From the report
.setRecentBlockhash(blockhashResponse.result!!.blockhash)
a rate limit or network error leaves result null, and !! throws in the middle of the open wallet session.
check blockhashResponse.error, handle null, and fetch the blockhash before opening the session.
Review: The claim that authorize returns no address is true of the documented result (:226), but the helper's return shape is unknown. 61: charCodeAt truncation is real (also :574, :609, legacy :435), but the message is the ASCII "Hello world!", and quickstart.md:99-103 tells readers to use TextEncoder. 63: blockhashResponse.result!! (kotlin/quickstart.md:158, using-anchor-programs.md:149,186) throws on a failed RPC call; the exception is visible and the transaction was never sent.
android-native/rpc-requests.md:88 (also line 63 and recipes/solana-development/using-anchor-programs.md:209)From the report
val response = rpc.sendTransaction(transaction)
if (response.result) {
nullable objects are used as Boolean conditions, and the client is declared as rpcClient, not rpc.
use rpcClient, and test response.result != null and response.error != null.
Review: All verified real and all fail at build or parse time, which is the best kind of failure. 59: await inside non-async handlers (ux-guidelines.md:45-49, :87-96), undeclared select, mobileWalletAdapter, SolanaSignInInput. 62: Base58.encodeToString(signedTxBytes) at using_mobile_wallet_adapter.md:272 while the lambda binds it; the sibling at kotlin/quickstart.md:170 is right. 64: rpc.sendTransaction (the client is rpcClient, :82) and if (response.result) on a nullable object (rpc-requests.md:63,90). 65: commitment undefined, block-body function without return (building-json-rpc-requests.md:139-160). 68: implementation('...${versions...}') in a .kts block (using-anchor-programs.md:33); the ${versions.*} placeholders are also undefined on that page. Each stops the developer immediately and carries no money or security effect.
android-native/building-json-rpc-requests.md:145From the report
val request = createBlockhashRequest(commitment, requestId)
commitment is undefined, the block-bodied function has no return, and it calls a suspending request from a non-suspending function.
add a commitment parameter, mark the function suspend, and return the value.
Review: All verified real and all fail at build or parse time, which is the best kind of failure. 59: await inside non-async handlers (ux-guidelines.md:45-49, :87-96), undeclared select, mobileWalletAdapter, SolanaSignInInput. 62: Base58.encodeToString(signedTxBytes) at using_mobile_wallet_adapter.md:272 while the lambda binds it; the sibling at kotlin/quickstart.md:170 is right. 64: rpc.sendTransaction (the client is rpcClient, :82) and if (response.result) on a nullable object (rpc-requests.md:63,90). 65: commitment undefined, block-body function without return (building-json-rpc-requests.md:139-160). 68: implementation('...${versions...}') in a .kts block (using-anchor-programs.md:33); the ${versions.*} placeholders are also undefined on that page. Each stops the developer immediately and carries no money or security effect.
reference/typescript/mobile-wallet-adapter-legacy.md:428 (also lines 187-194 and 318)From the report
const result = return await transact(async (wallet: Web3MobileWallet) => {
the samples contain const x = return, a missing comma, an unbalanced }));, an unawaited authorize, and a cluster field the page does not document.
correct the syntax, await authorize, and use the documented chain parameter.
Review: All verified real and all fail at build or parse time, which is the best kind of failure. 59: await inside non-async handlers (ux-guidelines.md:45-49, :87-96), undeclared select, mobileWalletAdapter, SolanaSignInInput. 62: Base58.encodeToString(signedTxBytes) at using_mobile_wallet_adapter.md:272 while the lambda binds it; the sibling at kotlin/quickstart.md:170 is right. 64: rpc.sendTransaction (the client is rpcClient, :82) and if (response.result) on a nullable object (rpc-requests.md:63,90). 65: commitment undefined, block-body function without return (building-json-rpc-requests.md:139-160). 68: implementation('...${versions...}') in a .kts block (using-anchor-programs.md:33); the ${versions.*} placeholders are also undefined on that page. Each stops the developer immediately and carries no money or security effect.
recipes/solana-development/using-anchor-programs.md:94From the report
class Args_increment(val amount: UInt)
line 57 says the instruction takes amount: u64. UInt serialises as 4 bytes, so the program receives malformed data. amount is also undefined, while line 143 declares an unused incrementAmount.
use ULong, and pass the variable the sample defines.
Review: 67: class Args_increment(val amount: UInt) (using-anchor-programs.md:94) against amount: u64 (:57) is a real width bug (and amount is undefined at :102, while incrementAmount at :143 is unused). The wrong-size instruction data makes the program reject the call with a deserialization error on devnet, so nothing is spent or lost. 69: the only account is the non-signer counter PDA (:124), so the message has no fee payer and the "sign with a keypair" branch (:190) signs with an unbound signer. Real, and the transaction cannot be sent, so it fails before reaching the network.
recipes/solana-development/using-anchor-programs.md:33From the report
implementation('io.github.funkatronics:kborsh:${versions.KOTLIN_KBORSH_VERSION}')
in build.gradle.kts, single quotes are a Char literal, so the dependency block fails to build.
use double quotes and a concrete, tested version.
Review: All verified real and all fail at build or parse time, which is the best kind of failure. 59: await inside non-async handlers (ux-guidelines.md:45-49, :87-96), undeclared select, mobileWalletAdapter, SolanaSignInInput. 62: Base58.encodeToString(signedTxBytes) at using_mobile_wallet_adapter.md:272 while the lambda binds it; the sibling at kotlin/quickstart.md:170 is right. 64: rpc.sendTransaction (the client is rpcClient, :82) and if (response.result) on a nullable object (rpc-requests.md:63,90). 65: commitment undefined, block-body function without return (building-json-rpc-requests.md:139-160). 68: implementation('...${versions...}') in a .kts block (using-anchor-programs.md:33); the ${versions.*} placeholders are also undefined on that page. Each stops the developer immediately and carries no money or security effect.
recipes/solana-development/using-anchor-programs.md:145From the report
Message.Builder()
.addInstruction(
incrementInstruction
the only account is the non-signer counter address, so the message has no fee payer. Line 158 says the fee payer must sign, and the keypair path signs with an arbitrary signer that is not bound to any account in the message. The transaction cannot be submitted as built.
add the user's wallet as fee payer and signer, and sign with that same account.
Review: 67: class Args_increment(val amount: UInt) (using-anchor-programs.md:94) against amount: u64 (:57) is a real width bug (and amount is undefined at :102, while incrementAmount at :143 is unused). The wrong-size instruction data makes the program reject the call with a deserialization error on devnet, so nothing is spent or lost. 69: the only account is the non-signer counter PDA (:124), so the message has no fee payer and the "sign with a keypair" branch (:190) signs with an unbound signer. Real, and the transaction cannot be sent, so it fails before reaching the network.
dapp-store/build-and-sign-an-apk.md:62From the report
For signing, EAS can automatically generate and manage a keystore for you on your first build.
line 40 says a separate key is mandatory for apps also on Google Play. A project already built for Play with EAS reuses that project keystore, and the submission is rejected.
say that such projects must use local credentials with a dedicated keystore for the dApp Store profile, as the Google Play guide does.
Review: 71: build-and-sign-an-apk.md:62 offers EAS-managed keystores in the Expo tab, while the Warning at :39-42 (same page, above the tabs) says a separate key is mandatory for apps also on Google Play. Inconsistent wording, with the rule stated on the page; and the Google Play guide covers the Expo case with local credentials. 72: publishing-cli.md:77-86 treats the API key as the secret and the keypair as a path (./path/to/keypair.json). It is a docs omission about a publisher keypair that also needs the portal API key to act, and ${{ secrets.* }} is GitHub Actions syntax shown in a bash block (cosmetic).
dapp-store/publishing-cli.md:78From the report
secret in your CI environment and provide the signer keypair file via `--keypair`:
only the API key is treated as a secret. The keypair controls publishing for the app, and a repository path like ./path/to/keypair.json invites committing it. The ${{ secrets... }} expression also only works inside a workflow file, not in plain bash.
document injecting the keypair from a secret store into a temporary file that is deleted afterwards, preferably for a dedicated publisher wallet.
Review: 71: build-and-sign-an-apk.md:62 offers EAS-managed keystores in the Expo tab, while the Warning at :39-42 (same page, above the tabs) says a separate key is mandatory for apps also on Google Play. Inconsistent wording, with the rule stated on the page; and the Google Play guide covers the Expo case with local credentials. 72: publishing-cli.md:77-86 treats the API key as the secret and the keypair as a path (./path/to/keypair.json). It is a docs omission about a publisher keypair that also needs the portal API key to act, and ${{ secrets.* }} is GitHub Actions syntax shown in a bash block (cosmetic).
get-started/react-native/invoke-mwa-sessions-directly.md:435From the report
const latestBlockhash = await connection.getLatestBlockhash();
const txSignature = await transact(async (wallet: Web3MobileWallet) => {
the blockhash is fetched before the user sees the wallet, so a slow approval expires it. toPublicKey is never defined, and confirmation by signature alone cannot detect expiry.
fetch the blockhash with its context inside the session, pass minContextSlot, define the recipient, and confirm with the blockhash strategy.
Review: 73: the blockhash is fetched before transact (invoke-mwa-sessions-directly.md:435) and toPublicKey is never defined (:452); minContextSlot is omitted (:467), contradicting quickstart.md:243. Real, but a slow approval is needed to hit it and the result is a visible failure. 74: the kit signTransactions sample (:511-518) has no authorize first, which the web3.js tab does via "transaction code from above" (:540); the wallet will refuse the privileged call, visibly, and the method is itself deprecated. 75: the bare-library connect handler (caching-wallet-authorization.md:324-326) has identity: {name} only; setup.md:104 says every field is optional in the protocol and only "recommends" a wallet decline when uri is missing; the other samples on the page set uri (:257-259).
signTransactions sample calls a privileged method without authorizingget-started/react-native/invoke-mwa-sessions-directly.md:511From the report
const signedTx = await transact(async (wallet: KitMobileWallet) => {
// Request to sign the transaction.
const signedTxs = await wallet.signTransactions({
signing is privileged and needs authorize first in the session. As written, the wallet rejects the request.
call authorize (or reauthorize with a stored token) before signTransactions, as the web3.js tab does.
Review: 73: the blockhash is fetched before transact (invoke-mwa-sessions-directly.md:435) and toPublicKey is never defined (:452); minContextSlot is omitted (:467), contradicting quickstart.md:243. Real, but a slow approval is needed to hit it and the result is a visible failure. 74: the kit signTransactions sample (:511-518) has no authorize first, which the web3.js tab does via "transaction code from above" (:540); the wallet will refuse the privileged call, visibly, and the method is itself deprecated. 75: the bare-library connect handler (caching-wallet-authorization.md:324-326) has identity: {name} only; setup.md:104 says every field is optional in the protocol and only "recommends" a wallet decline when uri is missing; the other samples on the page set uri (:257-259).
recipes/mobile-wallet-adapter/caching-wallet-authorization.md:324From the report
identity: {
name: "My amazing app",
},
get-started/react-native/setup.md:104 says the protocol specification recommends that wallets decline authorization when the identity has no uri. Copied as is, the connect flow can fail on such wallets.
include an absolute uri and an icon, as the other samples do.
Review: 73: the blockhash is fetched before transact (invoke-mwa-sessions-directly.md:435) and toPublicKey is never defined (:452); minContextSlot is omitted (:467), contradicting quickstart.md:243. Real, but a slow approval is needed to hit it and the result is a visible failure. 74: the kit signTransactions sample (:511-518) has no authorize first, which the web3.js tab does via "transaction code from above" (:540); the wallet will refuse the privileged call, visibly, and the method is itself deprecated. 75: the bare-library connect handler (caching-wallet-authorization.md:324-326) has identity: {name} only; setup.md:104 says every field is optional in the protocol and only "recommends" a wallet decline when uri is missing; the other samples on the page set uri (:257-259).
_llms.txt:1 (also _sitemap.xml, _URLS.txt, _URLS_llms.txt and _llms-full.txt)From the report
(no entry for android-native/*, marketing/*, mobile-wallet-adapter/mobile-apps, reference/typescript/mobile-wallet-adapter-legacy)
these pages exist and are linked from indexed pages, for example the Kotlin quickstart's "JSON RPC Requests" card. Search engines and AI tools that follow the indexes cannot reach them. FAQ is also listed twice, and the release-notes URL differs between index files.
regenerate all index files from the full page set, and remove the duplicate.
Review: Verified, with a correction. get-started/faq.md appears twice in _llms.txt (lines 23 and 55). Several of the missing pages are legacy or marketing pages that a docs platform often hides on purpose, and only the Kotlin quickstart card (kotlin/quickstart.md:189) links one of them. Discoverability for AI crawlers, with no effect on a developer following the visible docs.
dapp-store/link-to-dapp-listing-page.md:49From the report
Linking.canOpenURL(url)
.then((supported) => {
on Android 11 and later, canOpenURL returns false unless the scheme is declared under <queries>. Only the Kotlin tab mentions the manifest change, so the React Native button always logs "Unable to link". The Kotlin manifest snippet is also malformed (<manifest without >).
add the <queries> entry to the React Native tab (an Expo config plugin or AndroidManifest.xml), or call openURL directly and catch the error.
Review: Also real: the manifest snippet is <manifest with no > (:66). The failure is a visible "Unable to link to dApp Store" log in a convenience link, and the fix is one manifest entry. I could not run Android to check the package-visibility behaviour, which is general platform knowledge.
Info after review (8)
recipes/mobile-wallet-adapter/wallet-signing-on-ios.md:181From the report
if url.scheme == "your-dapp-scheme" && url.host == "connect-response" {
let connectData = parseConnectResponse(url)
any app can open a custom scheme. Without a per-request state value, a forged response can inject a wallet address or token. The surrounding text speaks of Universal Links, but the code uses a custom scheme.
bind each request to a random state value, check it on the response, and say so in the example.
Review: The snippet illustrates why deep links are a poor protocol; the page itself says "carries risk" at :~192. Not offered as a pattern to copy.
signMessages result is not awaited in the direct-session samplesget-started/react-native/invoke-mwa-sessions-directly.md:586 (also line 617)From the report
const signedMessages = wallet.signMessages({
addresses: [authorizationResult.accounts[0].address],
the promise is returned unawaited, unlike every other sample, so a wallet rejection is not handled where the reader expects.
await the call and show a try/catch for a declined request.
Review: Also :617. The promise is returned from an async callback, so transact awaits it and rejection still propagates. Style inconsistency, no behaviour difference.
solana-mobile-stack/seeker-genesis-token.md:206From the report
// If both extensions match then it is an SGT
if (hasCorrectMetadata && hasCorrectGroupMember) {
return mint.address;
the program id is used only as a query filter, and the metadata pointer is a value any mint can set. Today the group-member field carries the guarantee, but the check documents no defence in depth for a gate on rewards.
also assert the mint's owning program, and check the mint and group authorities against published values.
Review: Speculative. Defence-in-depth wish only.
solana-mobile-stack/skr.md:26From the report
* **Token type**: SPL Token
the SGT page uses Token Extensions, so "SPL Token" is ambiguous for anyone verifying SKR transfers.
give the owning program id next to the mint address.
Review: Token type: SPL Token.
cli/device.md:23From the report
Install APKs from local files, directories, or the built-in APK catalog.
catalog downloads are cached and installed with --grant, and the page describes no signature check.
document the check, or state that there is none.
Review: --grant is an opt-in flag (cli/device.md:61, "Grant all runtime permissions"), not the default. The "installed with --grant" premise is wrong; the missing integrity statement is a docs wish.
--force and of existing targets is undocumentedcli/webshell.md:47 (similar gaps in cli/templates.md:78, cli/create.md:19 and cli/emulator.md:23)From the report
The directory defaults to the current one. `--force` overwrites an existing directory.
readers cannot tell whether running the command against an existing project, emulator or directory is refused, merged or overwritten.
document the behaviour without --force, and what --force discards.
Review: The cited lines already state what --force does (webshell.md:47; templates.md:78 "synced files may be overwritten or removed"). The no---force behaviour is the gap, for webshell, create and emulator.
recipes/solana-development/anchor-integration.md:280From the report
This generates a program keypair, writes the new address into `lib.rs` and `Anchor.toml` ...
a second run replaces the program address, and the earlier deployment loses its keypair reference.
say what happens on a re-run, and tell the reader to back up the program keypair.
Review: Line says it generates a new keypair and rewrites the address (:280). The "orphaned deployment" outcome is the report's inference, on a devnet demo scaffold.
android-native/building-json-rpc-requests.md:114 (also dapp-store/link-to-dapp-listing-page.md:82)From the report
HttpClient(Android).use { client ->
client.request(request.url) {
request.properties.forEach { (k, v) ->
inside the builder, request and this resolve to different objects than a reader expects, which invites mistakes when the snippet is adapted.
label the lambdas or extract the inner loop. Build the Intent first and then start it.
Review: Snippet exists (:113-123) but request is a plain parameter and this is not used, so there is no real ambiguity. Also dapp-store/link-to-dapp-listing-page.md:82 (style only).
Excluded on review (1)
Findings the review showed to be wrong or a repeat of another finding. They are not counted above.
- 2. Kotlin quickstart says sign-in verifies ownership but never verifies anything (duplicate): Same root cause as 3:
:90"verify ownership in a single step",:104-105onlyprintln("Signed in successfully").