> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agg.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Account Linking

> Link additional auth providers to an already signed-in user

<Info>
  Linking reuses the same redirect/callback machinery as
  [Authentication](/recipes/authentication) — but it uses a **separate** set of
  endpoints from sign-in. Do not use `authStart()` to link.
</Info>

<Info>
  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](/recipes/external-id-linking).
</Info>

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
```

<Warning>
  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.
</Warning>

## Example: link Google to a wallet account

```typescript theme={null}
// 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:

```typescript theme={null}
// 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.

```tsx theme={null}
// 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.
```

<Warning>
  The page your `redirectUrl` lands on **must** have `<AggAuthProvider>` mounted (or call
  [`useAggAuthCallback()`](/recipes/authentication#dedicated-callback-pages) 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.
</Warning>

## 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:

```typescript theme={null}
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.

```typescript theme={null}
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

```typescript theme={null}
const profile = await client.getCurrentUser(); // GET /users/me
profile.accounts.forEach((account) => {
  console.log(`${account.provider}: ${account.providerAccountId}`);
});
```

<Note>
  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.
</Note>
