Account Linking
Link additional auth providers to an already signed-in user
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, or email — 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/* and finishes with a
short-lived confirm token, so the new provider is bound to the current principal.
The flow
client.linkAccount({ provider, redirectUrl }) → POST /users/me/link-account/start
→ redirect the browser to the provider
→ provider calls back to AGG, which redirects to your redirectUrl?link_confirm_token=…
client.linkAccountConfirm(token) → POST /users/me/link-account/confirm
→ the Account row is written to the current user
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.
Example: link Google to a wallet account
// User is already signed in with SIWE
console.log(client.isAuthenticated); // true
// Start the link — redirectUrl is REQUIRED and must be an allowed origin for your app
const response = await client.linkAccount({
provider: "google",
redirectUrl: "https://yourapp.com/auth/callback",
});
if (response.type === "redirect") {
window.location.href = response.url; // send the user to Google
}
After the provider callback, the browser lands back on your redirectUrl with a
link_confirm_token query param. Exchange it for the linked account:
// On the redirect target page
const token = new URLSearchParams(window.location.search).get("link_confirm_token");
if (token) {
await client.linkAccountConfirm(token);
// The Account row is now written to the current user.
const profile = await client.getCurrentUser();
console.log(profile.accounts);
// [
// { type: "siwe", provider: "wallet", providerAccountId: "0xABC..." },
// { type: "oauth", provider: "google", providerAccountId: "google-user-id" }
// ]
}
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.
// Anywhere under <AggAuthProvider>, kicking off the link:
import { useLinkAccount } from "@agg-build/hooks";
const { startLink, isLoading, error } = useLinkAccount();
await startLink({ provider: "google", redirectUrl: window.location.href });
// …provider round-trip… on return, AggAuthProvider auto-confirms.
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:
const response = await client.linkAccount({
provider: "email",
email: "user@example.com",
redirectUrl: "https://yourapp.com/auth/callback",
});
// response = { type: "magic_link", success: true } — no redirect here.
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.
try {
await client.linkAccountConfirm(token);
} catch (err) {
if (err.status === 409) {
// This Google/Twitter/Apple/email identity is linked to another account.
}
}
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
const profile = await client.getCurrentUser(); // GET /users/me
profile.accounts.forEach((account) => {
console.log(`${account.provider}: ${account.providerAccountId}`);
});
Wallet-to-wallet linking (SIWE/SIWS) is not supported yet — the signature protocol needs a principal-binding step before it can be done safely. Only Google, Twitter, Apple, and email can be linked today.