AGG is currently in alpha. APIs, components, and docs may change.
Agg.market — Prediction Market Aggregator logo

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.

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