Skip to main content
Linking reuses the same redirect/callback machinery as Authentication — but it uses a separate set of endpoints from sign-in. Do not use authStart() to link.
This page is about linking multiple auth providers to one AGG account. If you need to link an AGG user to your own partner-side user ID, use Partner External ID Linking.
A user who is already signed in (e.g. with a wallet) can attach more providers — Google, Twitter, Apple, email, or an additional wallet (SIWE/SIWS) — to the same account. Because the caller is already authenticated, linking is a distinct flow from sign-in: it runs against POST /users/me/link-account/*. OAuth/email finish with a short-lived confirm token; wallet linking finishes with a signature over a server-issued challenge. In every case the new provider is bound to the current principal.

The flow

Use linkAccount(), not authStart(). authStart() starts a fresh sign-in and cannot attach a provider to the signed-in principal — pointing it at an authenticated session produces a competing redirect, not a link.
After the provider callback, the browser lands back on your redirectUrl with a link_confirm_token query param. Exchange it for the linked account:

React: this is handled for you

If your app is wrapped in <AggAuthProvider> from @agg-build/auth, the confirm step runs automatically. On mount it reads link_confirm_token from the URL, calls linkAccountConfirm(), refreshes the user, and strips the token from the address bar — the same handler that finishes sign-in callbacks.
The page your redirectUrl lands on must have <AggAuthProvider> mounted (or call useAggAuthCallback() on a dedicated callback route). This is the same requirement as sign-in OAuth — if the auth provider isn’t mounted where the redirect returns, nothing consumes link_confirm_token and the link silently never completes.

Email linking

Email uses a magic link instead of an OAuth redirect. The result of linkAccount is a magic_link acknowledgement, not a redirect — tell the user to check their inbox:
When the user clicks the emailed link, AGG redirects them to your redirectUrl with a link_confirm_token, and the same confirm step above completes the link. The confirm call is bound to the signed-in bearer, so the link must be opened in the browser where the user is still authenticated.

Collisions

linkAccountConfirm() resolves to { status: "linked" } on first link and { status: "already_linked_same" } if that identity was already attached to the same user (idempotent — safe to retry). If the provider identity already belongs to a different AGG user, the confirm call rejects with HTTP 409 — catch it and tell the user the account is already in use elsewhere.

Wallet linking (SIWE/SIWS)

A signed-in user can link an additional wallet (Ethereum or Solana) to their account. Unlike OAuth/email there is no redirect: /start returns a wallet_challenge containing a message the user signs verbatim with their wallet, then you submit the signature to /confirm.

Prerequisite: the account needs a name for the challenge

The challenge message names the signed-in account so the user can tell it is theirs before signing. If the account has nothing to name, /start fails closed with HTTP 400 no_identity_for_link_challenge instead of issuing an anonymous challenge. This applies to wallet linking only — OAuth and email linking never hit it, because neither builds a message for the user to sign. The server uses the first of these it finds:
  1. username
  2. a masked verified email from a linked account (a***@example.com)
  3. a displayName from a linked account (e.g. a Google profile name)
A wallet-only user — signed in with SIWE, no username, no OAuth provider linked — has none of the three, so every wallet link attempt 400s until one exists. Mitigation: make sure a username is set before calling linkAccount(). It is the only one of the three you can set directly.
There is deliberately no server-side fallback here. The identity has to be something the user already had before the link started — a value minted during the flow would be one the attacker holds and the victim has nothing to check against. An app-name-only statement is no better, since a phisher running the same app produces an identical one.
Sign the message exactly as returned — the server compares it byte-for-byte at confirm, so any client-side reconstruction or edit fails. The message names the signed-in account (username, masked email, or a linked handle) so the user can verify it is their own before signing.
Wallet confirm returns the same statuses as OAuth/email: linked on first link, already_linked_same if that wallet was already attached to the same user (idempotent), and HTTP 409 if the wallet is already linked to a different AGG user.

Auto-linking by email

If a user signs in with a provider that carries a verified email (e.g. Google) and another account already exists with that email, the accounts are linked automatically during sign-in — no explicit link step needed.

Viewing linked accounts