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.