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.
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
Example: link Google to a wallet account
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.
Email linking
Email uses a magic link instead of an OAuth redirect. The result oflinkAccount is a
magic_link acknowledgement, not a redirect — tell the user to check their inbox:
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:
username- a masked verified email from a linked account (
a***@example.com) - a
displayNamefrom a linked account (e.g. a Google profile name)
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.
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.