Skip to main content
Choose where the user’s Unlink account lives first. That choice decides which SDK import you use.

Choose a model

  • Non-custodial browser app: import from @unlink-xyz/sdk/browser. The user’s spending key is created in the browser and never leaves it.
  • Custodial server app: import from @unlink-xyz/sdk/client. Your server holds the account and signs on the user’s behalf.
Both models use @unlink-xyz/sdk/admin on your backend for registration, authorization tokens, and backend reads. Keep the admin API key server-side.
Use @unlink-xyz/sdk/browser in browser bundles. Keep @unlink-xyz/sdk/admin on your backend.
Pick a hosted deployment by environment name on the Supported chains page before wiring either model.

Browser non-custodial

The user signs in the browser. Your backend only registers the user and issues auth tokens.
  • POST /api/unlink/register
  • POST /api/unlink/authorization-token
import { account, createUnlinkClient } from "@unlink-xyz/sdk/browser";

const { account: unlinkAccount } = await account.fromMetaMask({
  provider: window.ethereum,
  appId: "your-app-id",
  chainId: 84532,
});

const client = createUnlinkClient({
  environment: "base-sepolia",
  account: unlinkAccount,
});

await client.ensureRegistered();
The user’s spending key stays in the browser.

App backend

Mount these routes behind your normal app login. This is the canonical wiring for the two routes the browser client calls.
import {
  createUnlinkAdmin,
  createUnlinkAuthRoutes,
} from "@unlink-xyz/sdk/admin";

const admin = createUnlinkAdmin({
  environment: "base-sepolia",
  apiKey: process.env.UNLINK_API_KEY!,
});

const routes = createUnlinkAuthRoutes({
  admin,
  authenticate: async (request) => getAppSession(request),
  onRegister: async ({ session, registration }) => {
    await db.linkUnlinkAddress(session.userId, registration.address);
  },
  authorizeUnlinkAddress: async ({ session, unlinkAddress }) =>
    db.userOwnsUnlinkAddress(session.userId, unlinkAddress),
});

app.post("/api/unlink/register", (c) => routes.register(c.req.raw));
app.post("/api/unlink/authorization-token", (c) =>
  routes.authorizationToken(c.req.raw),
);
The two routes have distinct jobs. register links a newly derived Unlink address to your authenticated app user. authorization-token issues the short-lived tokens the client sends on later calls. Tokens are scoped by subject_type: unlink_address authorizes account actions, and user_storage authorizes the encrypted storage namespace for one app userId.

Server or custodial

Use this model when your server is allowed to hold user accounts.
import { createUnlinkAdmin } from "@unlink-xyz/sdk/admin";
import { account, createUnlinkClient } from "@unlink-xyz/sdk/client";

const admin = createUnlinkAdmin({
  environment: "base-sepolia",
  apiKey: process.env.UNLINK_API_KEY!,
});

const unlinkAccount = account.fromMnemonic({ mnemonic });
const unlinkAddress = await unlinkAccount.getAddress();

const client = createUnlinkClient({
  environment: "base-sepolia",
  account: unlinkAccount,
  register: (payload) => admin.users.register(payload),
  authorizationToken: {
    provider: () => admin.authorizationTokens.issue({ unlinkAddress }),
  },
});

await client.ensureRegistered();
The account passed to createUnlinkClient signs the user’s private actions, and the constructor you choose decides which operations it can perform. See Accounts and keys for the full constructor reference.

Backend reads

Use admin reads for backend dashboards and support tooling, and the user client for signed actions. See Reading data and status.