solana-mobile/solana-mobile-skillsVor der Ausführung prüfen
SKILL DETAIL
seeker-connect
solana-mobile/solana-mobile-skills/seeker-connect
Connect a web dapp to the Seeker device's built-in wallet with Seeker Connect. Use when adding wallet connection, sign-in with Solana, message signing, or transaction signing to a website that runs in the browser on a Seeker phone, registering the "Seeker Connect" Wallet Standard wallet, setting the Nostr relay for relayDomain, using the seeker-connect-button element, or debugging SeekerConnectError codes like association-failed.
Installationen · 950Quelle ansehen
Installation
npx skills add https://github.com/solana-mobile/solana-mobile-skills --skill seeker-connect
Skill-Dateien
SKILL.md
Zuletzt synchronisiert · 21.10.2026
references/imperative-api.md›
# Imperative API and injection points
The Wallet Standard wallet is the canonical entry point. Reach for the pieces below when the
dapp does not use Wallet Standard, or when a default (storage, UI, transport) needs
replacing.
## `transact`: one session, one callback
`@solana-mobile/seeker-connect-web` exposes the `SeekerLink` implementation directly:
```ts
import { createNostrSeekerLink, SeekerConnectError } from '@solana-mobile/seeker-connect-web';
const link = createNostrSeekerLink();
const nonce = await fetch('/api/siws/nonce').then((response) => response.text());
const issuedAt = new Date();
const authorization = await link.transact(config, async (wallet) => {
return wallet.authorize({
signInPayload: {
domain: window.location.host,
uri: window.location.origin,
statement: 'Sign in to My Dapp',
version: '1',
chainId: config.chain ?? 'solana:mainnet',
nonce,
issuedAt: issuedAt.toISOString(),
expirationTime: new Date(issuedAt.getTime() + 5 * 60_000).toISOString(),
},
});
});
```
`config` is the same `SeekerConnectConfig` object `registerSeekerConnect` takes (`identity`,
`relayDomain`, optional `chain`, `firstConnectWalletBaseUri`, `associationTimeoutMs`). The
relay rule from the skill applies unchanged: set `relayDomain` to `relay.solanamobile.com`
and tell the developer to accept Solana Mobile's relay terms.
Fill the payload in, every time. This path hands `signInPayload` to the wallet verbatim: unlike
the `solana:signIn` feature on the Wallet Standard wallet, it does not default `domain`, and
nothing anywhere invents a nonce. A payload carrying only a `statement` produces a signature
over a message with no site to bind it to, no single-use value, and no expiry — it proves
someone holds the key, and nothing else, so it is replayable forever by anyone who sees it.
The nonce comes from the server, is used once, and is checked there against the returned
message and signature along with the domain and the validity window, before any session is
created. The `seeker-genesis-token` skill walks through that server side.
`transact(config, callback, options?)` establishes one wallet session, runs the callback
against it, and always tears the session down when the callback settles. The `wallet` handle
is only valid inside the callback. Options:
| Option | Notes |
| --- | --- |
| `signal` | `AbortSignal`; aborting closes the session and rejects the `transact` promise |
| `walletUriBase` | Endpoint-specific wallet URI from a prior authorization; targets that wallet directly instead of the first-connect URI |
Rejections are always `SeekerConnectError` (see the code table in the skill).
Note there is **no built-in UI on this path** — the progress overlay and error dialog belong
to the presenter, which only the Wallet Standard wallet wires up by default. Either render
your own feedback around `transact`, or use `createSeekerConnectPresenter()` from
`@solana-mobile/seeker-connect-ui` manually.
## The session-scoped wallet handle
Inside the callback, `wallet` is a `SeekerWallet`:
| Method | Notes |
| --- | --- |
| `authorize(request?)` | Fresh consent, or pass `authToken` to reauthorize silently. Optional `chain`, `signInPayload` (SIWS). Returns `accounts`, `authToken`, `walletUriBase`, and `signInResult` when a sign-in payload was sent |
| `deauthorize({ authToken })` | Invalidates a token wallet-side. The Wallet Standard wallet never calls this |
| `getCapabilities()` | Supported transaction versions, feature identifiers, `supportsSignAndSendTransactions`, batch limits |
| `signAndSendTransactions({ payloads, options? })` | Submits; resolves raw signature bytes per transaction. Options: `minContextSlot`, `commitment`, `skipPreflight`, `maxRetries`, `waitForCommitmentToSendNextTransaction` |
| `signMessages({ addresses, payloads })` | Base58 addresses of the signing accounts; resolves signed message bytes (signature = final 64 bytes) |
| `signTransactions({ payloads })` | Serialized transaction bytes in, signed bytes out. Only if the wallet reports the `solana:signTransactions` feature |
Addresses are base58 throughout — the adapter converts from MWA's base64 wire encoding.
To stay "connected" across interactions, persist what `authorize` returns (`authToken`,
`accounts`, `walletUriBase`, capabilities) and replay the token in the next session's
`authorize` — exactly what the Wallet Standard wallet's `AuthorizationCache` does.
## Overriding the defaults in `registerSeekerConnect`
```ts
registerSeekerConnect({
...config,
authorizationCache: createMemoryAuthorizationCache(), // instead of localStorage
presenter: myPresenter, // instead of the built-in UI
seekerLink: myLink, // instead of the Nostr transport
});
```
**`authorizationCache`** persists the authorization between page loads. Exports:
`createLocalStorageAuthorizationCache()` (default) and `createMemoryAuthorizationCache()`
(useful for tests, or when persistence is unwanted). Custom implementations provide
`get()`, `set(authorization)`, and `clear()`, all async.
**`presenter`** owns the UI shown around interactions. The interface is two methods:
`interactionStarted(cancel)` — called when an interaction begins; invoking `cancel` aborts
it (that is what produces the `cancelled` error code); returns a closer invoked on
settlement — and `interactionFailed(error)` for failures the user should be told about.
Implementations must tolerate any call ordering and never throw. Replace it to integrate
Seeker Connect into an app's own modal system.
**`seekerLink`** replaces the transport entirely — the main use is injecting a fake wallet
in tests.
references/troubleshooting.md›
# Troubleshooting
## `association-failed` on every connect
The wallet app never completed the session handshake. Causes, most common first:
1. **Not on a Seeker.** The wallet is launched via an app link on the same device, so the
dapp must be open in the browser on the Seeker itself. Desktop browsers always end here —
usually within seconds via the failed launch detection below, not after the timeout. This
is expected, not a bug in the integration.
2. **The wallet never launched.** The protocol detects a successful launch by the page
losing focus within 3 seconds of the association URL opening. If the page stays focused,
association fails immediately rather than after the timeout.
3. **Relay unreachable or misconfigured.** Session traffic rides the Nostr relay in
`relayDomain`. Check that the value is `relay.solanamobile.com` — not a `<relay-domain>`
placeholder, a guessed hostname, or a host with a `wss://` scheme in front — and that the
device can reach it.
4. **Timeout too tight.** `associationTimeoutMs` (default 30s) covers association only —
wallet launched but not yet connected. The interaction itself is never timed out.
## A wallet call never settles
The overlay keeps saying "Continue in your Seeker Wallet" and the promise neither resolves nor
rejects, even though the wallet may already have signed and sent the transaction. The session
dropped and the transport never told the caller.
Upgrade to `@solana-mobile/seeker-connect-wallet-standard` `0.1.2` or later. It runs on
Mobile Wallet Adapter 3.0, which keeps the relay connection alive while a request is pending
and rejects pending requests with `session-closed` when the session ends. Check `npm ls
@solana-mobile/mobile-wallet-adapter-protocol` resolves to `3.x`.
After `signAndSendTransaction`, `session-closed` does not mean the transaction failed, and the
error carries no signature to look it up by. Do not retry automatically: check for the
transaction's effect on chain (for example the fee payer's recent signatures) until its
blockhash expires, or for a durable-nonce transaction until the nonce advances, and only send
it again after that. If the wallet offers `signTransaction`,
signing there and submitting from the app gives the app the signature before it is sent.
## First connect opens a generic wallet chooser
`firstConnectWalletBaseUri` is unset, so the first connect falls back to the generic
`solana-wallet:` Android intent and Android asks which wallet to use. That is the safe default,
and it only affects the first connect: after the first authorization the wallet returns its own
`walletUriBase`, which is cached and used for every session after that.
Setting it is not a cosmetic tweak. The value is an `https:` App Link, and on that path the
protocol navigates the page to it — `window.location.assign` on the association URL, with the
association parameters attached. The only check is that the string parses as an `https:` URL,
so whichever host is named receives the handshake and gets to answer as the wallet. A guessed
domain, or a look-alike of the right one, is handed the session.
Leave it unset unless Solana Mobile has published the Seeker wallet's App Link and you are
pasting that exact value. The SDK ships no default for it, and one wallet chooser tap on the
first connect is the cheaper trade.
## `signTransaction` / `signAndSendTransaction` feature missing
These two features are derived from the wallet's `get_capabilities` response, learned during
the first connect. Before any connect both are assumed present; afterwards the set is
re-derived and a `standard:events` `change` event fires with the new `features`. Always
feature-detect (`if (wallet.features[SolanaSignTransaction]) ...`) instead of assuming.
The exact rule: `signTransaction` appears only when the wallet reports the
`solana:signTransactions` feature. `signAndSendTransaction` appears when the wallet reports
`supportsSignAndSendTransactions` — and also when it reports *neither* route, because MWA
2.0 makes sign-and-send mandatory, so a missing report means a non-reporting wallet, not a
non-signing one. The only combination that hides `signAndSendTransaction` is a wallet that
reports sign-only support.
## Connection state seems wrong
- **`connect({ silent: true })` resolved with zero accounts** — there was no cached
authorization. That is the contract: a silent connect must never launch the wallet. Show
the connect button.
- **Reconnected without a consent prompt** — expected. Operations replay the cached
`authToken` (MWA reauthorize), which re-grants without fresh consent until the wallet
revokes it.
- **Cache vanished after a declined request** — only `authorization-declined` during a
token replay wipes the cache, because it means the wallet no longer honors the token.
Transport failures never wipe it.
- **Disconnect didn't revoke anything wallet-side** — by design. Disconnect forgets the
token locally and never calls `deauthorize`, which would launch the wallet app just to
disconnect.
## Every operation shows the overlay and bounces to the wallet
Working as intended. MWA sessions are per-interaction: each connect or sign opens a fresh
session with its own progress overlay. Dismissing the overlay aborts the session and rejects
with the `cancelled` code.
## Exercising the flow off-device
For automated tests, two hooks make the SDK drivable without a Seeker:
- **Fake the transport**: pass a custom `seekerLink` to `registerSeekerConnect` and serve
scripted responses — no relay, no wallet.
- **Fake the launch detection**: if a test harness serves a wallet endpoint over a real
relay, the browser side still needs the page to blur within 3s of association (see above).
Dispatching a synthetic `blur` event on `window` satisfies the check — this is what the
repository's own E2E setup does.
Also `createMemoryAuthorizationCache()` keeps test runs from leaking authorization state
through localStorage.
SKILL.md›
---
name: seeker-connect
description: Connect a web dapp to the Seeker device's built-in wallet with Seeker Connect. Use when adding wallet connection, sign-in with Solana, message signing, or transaction signing to a website that runs in the browser on a Seeker phone, registering the "Seeker Connect" Wallet Standard wallet, setting the Nostr relay for relayDomain, using the seeker-connect-button element, or debugging SeekerConnectError codes like association-failed.
---
# Seeker Connect for web dapps
Seeker Connect links a web page open in the browser **on the device** to a Solana Mobile
certified device's own wallet — Seeker is the first such device. It is a dapp-only SDK built
on top of the official Mobile Wallet Adapter libraries: a UX/DX layer, not a protocol
reimplementation. Where generic MWA centers on a "pick a wallet" chooser, Seeker Connect
launches the device wallet directly through the MWA spec's Endpoint-specific URI (Android
App Link) mechanism, carries the session over a Nostr relay, and ships Seeker-branded
progress and error UI — all exposed to the app as an ordinary Wallet Standard wallet named
**"Seeker Connect"**.
Targets: **Web is available today.** React Native and Android (Kotlin) are planned. Until
they ship, wallet connection inside an Expo or React Native app uses the
`solana-mobile-wallet` skill instead.
## The mental model — read this before writing code
- **There is no long-lived connection.** Every wallet interaction (connect, sign message,
sign transaction) opens its own short-lived MWA session, runs one request, and tears down.
Each interaction launches the wallet app and shows a branded progress overlay — the one
exception is a silent connect, which only reads the cache and never opens a session. This
is by design — do not build reconnect loops or keep-alive logic around it.
- **"Connected" means "holds a cached authorization."** The first `connect()` stores a
wallet-issued `authToken` plus accounts and capabilities (localStorage by default). Later
operations replay that token silently — no repeated consent prompt.
- **Disconnect only forgets the token locally.** It deliberately never calls the wallet's
`deauthorize`, because that would launch the wallet app just to disconnect.
- **It only completes on the device.** On desktop, connect fails with `association-failed` —
typically within seconds, when nothing answers the wallet launch; the association timeout
only applies to a wallet that launches but never connects. Register the wallet
unconditionally; it simply won't get past association elsewhere.
- **The SDK ships no default relay.** Use Solana Mobile's, `relay.solanamobile.com`: it is
free, including in production. Using it requires agreeing to Solana Mobile's terms, so tell
the developer to accept them — Step 2.
## Step 1: install
**New project:** the `react-kit-shadcn` template from the Solana Mobile CLI ships with
Seeker Connect already wired up on Solana Mobile's relay. The next steps `create` prints tell
the developer to accept the relay terms on the quickstart page:
```bash
npx solana-mobile@latest create my-app --template react-kit-shadcn
```
**Existing app:** install the Wallet Standard entry point plus the Wallet Standard helpers
the snippets below import from:
```bash
npm install @solana-mobile/seeker-connect-wallet-standard @solana/wallet-standard-features @wallet-standard/app @wallet-standard/features
```
Use `0.1.2` or later. Earlier releases sit on Mobile Wallet Adapter 2.x, which can leave a
wallet call pending forever when the session drops; `0.1.2` moves to MWA 3.0, which rejects
it with `session-closed` instead.
The Wallet Standard package pulls in the other Seeker Connect packages (`core`, `ui`, `web`)
as dependencies. They are not re-exported, though — so also install any of them the app
imports from directly (`@solana-mobile/seeker-connect-ui` for the optional button below,
`@solana-mobile/seeker-connect-web` for the imperative path in the references); strict
package managers like pnpm refuse imports of undeclared transitive dependencies.
| Package | Role |
| --- | --- |
| `@solana-mobile/seeker-connect-core` | Shared contracts: config, error taxonomy, `SeekerLink` port |
| `@solana-mobile/seeker-connect-ui` | Lit elements: progress overlay, error dialog, branded button |
| `@solana-mobile/seeker-connect-wallet-standard` | **The entry point** — the Wallet Standard wallet |
| `@solana-mobile/seeker-connect-web` | MWA-over-Nostr transport, plus an imperative SDK |
## Step 2: set the relay
Seeker Connect carries MWA's extended remote communication protocol over a Nostr relay,
named by `relayDomain`. Payloads are end-to-end encrypted between dapp and wallet; the relay
carries ciphertext only.
**Use `relay.solanamobile.com`.** Solana Mobile runs it for Seeker Connect, and it is free to
use, including in production. Pass the bare host; the SDK adds the `wss://` scheme itself.
**Tell the developer to accept the relay terms.** Using Solana Mobile's relay requires
agreeing to Solana Mobile's terms. Whenever you wire it in, say so, and point the developer at
the quickstart page where they accept them:
https://docs.solanamobile.com/solana-mobile-stack/seeker-connect-quickstart
## Step 3: register once at startup
Call `registerSeekerConnect` before the UI renders, so wallet discovery sees it from the
first render. It must run in the browser — the snippet below reads `window`, so under SSR
(Next.js and similar) put the call in a client-only module or guard it with
`typeof window !== 'undefined'`; in a plain client-rendered app, module scope of the entry
file is fine:
```ts
import { registerSeekerConnect } from '@solana-mobile/seeker-connect-wallet-standard';
registerSeekerConnect({
identity: {
name: 'My Dapp',
uri: window.location.origin,
icon: '/icon.png', // resolved relative to uri; shown in the wallet's consent UI
},
relayDomain: 'relay.solanamobile.com', // requires accepting Solana Mobile's terms — Step 2
});
```
Configuration:
| Option | Required | Notes |
| --- | --- | --- |
| `associationTimeoutMs` | no | How long a launch may take before `association-failed`. Default 30s |
| `chain` | no | Chain requested at authorization. Default `solana:mainnet` |
| `firstConnectWalletBaseUri` | no | **Leave unset** unless Solana Mobile publishes a value to paste. Unset, first connects use the generic `solana-wallet:` scheme; set, the page navigates to that host — see [references/troubleshooting.md](references/troubleshooting.md#first-connect-opens-a-generic-wallet-chooser) |
| `identity` | yes | `name`, `uri`, optional `icon`. Shown in the wallet's consent UI |
| `relayDomain` | yes | Nostr relay that carries the session traffic. Use `relay.solanamobile.com` — Step 2 |
`registerSeekerConnect` also accepts `seekerLink`, `authorizationCache`, and `presenter`
overrides — see [references/imperative-api.md](references/imperative-api.md).
## Step 4: use it like any other wallet
After registration, "Seeker Connect" appears in the Wallet Standard registry, so
wallet-adapter, ConnectorKit, `@solana/react-hooks`, or a raw `@wallet-standard/app` listing
all pick it up with no further wiring. Existing connect buttons and signing code keep
working.
Features exposed: `standard:connect`, `standard:disconnect`, `standard:events`,
`solana:signMessage`, `solana:signIn`, and — depending on the wallet's reported capabilities
after the first connect — `solana:signTransaction` and/or `solana:signAndSendTransaction`.
Working with the wallet directly:
```ts
import { SeekerConnectWalletName } from '@solana-mobile/seeker-connect-wallet-standard';
import { getWallets } from '@wallet-standard/app';
import { StandardConnect } from '@wallet-standard/features';
const seeker = getWallets()
.get()
.find((wallet) => wallet.name === SeekerConnectWalletName);
const { accounts } = await seeker.features[StandardConnect].connect();
```
**Restore the session on page load** with a silent connect — it reads the cache and never
launches the wallet, resolving with zero accounts when there is nothing cached:
```ts
const { accounts } = await seeker.features[StandardConnect].connect({ silent: true });
```
**Feature-detect the signing routes.** Until the first connect, both transaction features
are assumed; after it they are re-derived from the wallet's actual capabilities, announced
via a `standard:events` `change` event. Check before calling:
```ts
import { SolanaSignAndSendTransaction } from '@solana/wallet-standard-features';
const feature = seeker.features[SolanaSignAndSendTransaction];
if (feature) {
const [{ signature }] = await feature.signAndSendTransaction({
account,
chain: seeker.chains[0],
transaction,
});
}
```
`chain` is part of the Wallet Standard input, but this wallet ignores it — the network is the
one passed to `registerSeekerConnect`, replayed from the cached authorization on every call.
Nothing cross-checks the two, so a per-call `solana:devnet` against a mainnet registration
submits on mainnet without complaint. Pass `seeker.chains[0]` so the two can never disagree,
and change networks at registration.
Transactions cross the feature boundary as raw serialized bytes (`Uint8Array`), legacy and
v0 both supported — serialize with whichever client library the app already uses. Sign-in
follows the SIWS spec via `solana:signIn`; `domain` defaults to `window.location.host`.
When sign-in authenticates a user, the server is the authority, not the client: issue a
single-use, short-lived nonce server-side, and verify the returned message and signature —
including the expected domain and validity window — on the server before creating a
session. The `seeker-genesis-token` skill walks through that server flow step by step.
## Handle errors by code
Wallet outcomes reject with a `SeekerConnectError` carrying a `code`:
| Code | Meaning |
| --- | --- |
| `association-failed` | No wallet completed the launch: timeout, relay unreachable or misconfigured, or not on a Seeker |
| `authorization-declined` | The user declined authorization. The cached token is wiped; the next connect prompts fresh consent |
| `cancelled` | The user dismissed the progress overlay. **A normal outcome — never surface it as an error** |
| `request-declined` | The wallet declined to sign or submit |
| `session-closed` | The session ended before the interaction completed. After `signAndSendTransaction` the transaction may still have landed, and the error carries no signature — never retry automatically — see [references/troubleshooting.md](references/troubleshooting.md#a-wallet-call-never-settles) |
| `wallet-error` | Any other wallet-reported error |
Three failures are plain `Error`s instead, and they are misuse rather than wallet outcomes:
`signAndSendTransaction` on a wallet that reported no support for it, any signing call before a
successful connect, and a sign-in the wallet answered without a result.
That distinction decides who shows the error. The branded dialog fires for `SeekerConnectError`
and nothing else — every code above except `cancelled`, which is a normal outcome and stays
silent. The three plain `Error`s never reach the presenter, so if the app shows nothing they
fail invisibly. Branch on the type first, then on `.code`:
```ts
import { SeekerConnectError, SeekerConnectErrorCode } from '@solana-mobile/seeker-connect-wallet-standard';
try {
await seeker.features[StandardConnect].connect();
} catch (error) {
if (error instanceof SeekerConnectError) {
if (error.code === SeekerConnectErrorCode.cancelled) {
return; // user closed the overlay; nothing to report
}
// The SDK has shown its dialog. Log, and leave the UI in the disconnected state.
console.error(error);
return;
}
// No dialog was shown for this one. Surface it yourself.
showToast('Could not connect to the wallet.'); // your app's own error UI
console.error(error);
}
```
## Optional: the branded connect button
`@solana-mobile/seeker-connect-ui` ships a `<seeker-connect-button>` custom element in
Shadow DOM (no host CSS needed). The SDK defines its elements on first use; call
`defineSeekerConnectElements()` at startup to define them eagerly:
```ts
import { defineSeekerConnectElements } from '@solana-mobile/seeker-connect-ui';
defineSeekerConnectElements();
```
```html
<seeker-connect-button theme="dark" variant="sign-in"></seeker-connect-button>
```
Attributes: `disabled`, `theme="light" | "dark"` (names the host page's theme), and
`variant="connect"` (default) or `variant="sign-in"` for the label. Wire `click` to the
connect or sign-in call yourself — the button is presentation only.
## Reference material
- [references/imperative-api.md](references/imperative-api.md) — the non-Wallet-Standard
path via `createNostrSeekerLink().transact`, and the `seekerLink` /
`authorizationCache` / `presenter` overrides
- [references/troubleshooting.md](references/troubleshooting.md) — association failures,
desktop testing, capability-derived features, cache behavior
## Related skills
- `seeker-domains` — display `.skr` names instead of raw addresses
- `seeker-genesis-token` — verify Seeker device ownership after connecting
- `solana-mobile` — the CLI, templates, and toolchain behind `solana-mobile create`
- `solana-mobile-wallet` — wallet connection in React Native apps (Mobile Wallet Adapter
directly)
## Links
- MWA web docs: https://docs.solanamobile.com/get-started/web/installation
- Seeker Connect docs: https://docs.solanamobile.com/solana-mobile-stack/seeker-connect
- Seeker Connect quickstart (relay terms live here): https://docs.solanamobile.com/solana-mobile-stack/seeker-connect-quickstart
- Seeker Connect repository: https://github.com/solana-mobile/seeker-connect