We don't belong in your reality, your real life. In your reality, your real life, you can merely meet our avatars in any version. So, stay alert and beware of scams!
POST /auth/passkey/register/verify β verify attestation; persist credential; return { session_jwt? }address, validate domain/uri/chainId, ensure nonce unused and unexpired, then consume nonce; bind EOA β identity; issue/refresh session.audit.readroles.assign[ User ]
β
ββ If new: Sign up BGC/iB account
β
ββ Login (Credentials or Passkey/WebAuthn)
β ββ [ Auth Service ] β issues session/JWT { user_id, roles }
β
ββ PostβLogin: Wallet Setup
β ββ A) Create EOA (Embedded/InβApp) ββββββββββββββΊ [ Wallet Service ]
β β β
β β βββΊ [ AA Factory ] β deploy **Smart Account (AA)**
β β (deterministic per chain: factory + salt)
β ββ B) Connect Existing Wallet (SIWE) ββββββββββββΊ [ SIWE Verify ] ββββββ
β
ββ Persist mapping βββββββββββββββββββββββββββββββββββββΊ [ Identity Service ] β { identity_id, eoa, aa }
β βββΊ [ Audit Log ] (immutable)
β
ββ Apps (BGC Web, iB Web/Mobile) ββββββββββββββββββββββΊ consume identity & roles
Onβchain:
[ EOA ] (owner/controller) ββββββββββΊ [ Smart Account (AA) ] ββββββββββΊ (optional) [ Paymaster / Bundler ]// WalletSetupCreate.ts
// Trigger embedded wallet (**email OTP** or **phone OTP**) β get EOA β build AA.
import { connectEmbeddedWallet, connectSmartAccount } from "@/lib/wallet"; // wrap actual SDK calls
export async function createEmbeddedAA(params: {
email?: string;
usePasskey?: boolean;
chainId: number;
sponsorGas?: boolean;
}) {
// 1) Create/attach an embedded EOA (choose one: **email OTP** or **phone OTP**)
const eoa = await connectEmbeddedWallet({
email: params.email,
passkey: params.usePasskey === true,
}); // returns { address, signMessage, ... }
// 2) Build a Smart Account (AA) on top of that EOA
const aa = await connectSmartAccount({
personalWallet: eoa,
chainId: params.chainId,
sponsorGas: params.sponsorGas ?? true,
}); // returns { address, sendUserOp, ... }
// 3) Persist on backend
await fetch("/api/wallet/provision", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ eoa: eoa.address, aa: aa.address }),
credentials: "include",
});
return { eoa: eoa.address, aa: aa.address };
}// WalletSetupConnect.ts
// Connect wallet (extension/WalletConnect), run SIWE, then build AA.
import { connectExternalWallet, connectSmartAccount } from "@/lib/wallet";
export async function connectSiweAA(params: {
chainId: number;
sponsorGas?: boolean;
}) {
// 1) Connect external wallet β get EOA
const eoa = await connectExternalWallet(); // { address, signMessage, ... }
// 2) SIWE: request challenge, sign, verify server-side
const challenge = await fetch("/api/auth/siwe/challenge", {
method: "POST",
credentials: "include",
}).then((r) => r.json()); // { message }
const signature = await eoa.signMessage(challenge.message);
await fetch("/api/auth/siwe/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ message: challenge.message, signature }),
credentials: "include",
});
// 3) Build Smart Account (AA)
const aa = await connectSmartAccount({
personalWallet: eoa,
chainId: params.chainId,
sponsorGas: params.sponsorGas ?? true,
});
// 4) Persist on backend
await fetch("/api/wallet/connect/siwe", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ eoa: eoa.address, aa: aa.address }),
credentials: "include",
});
return { eoa: eoa.address, aa: aa.address };
}// /api/auth/siwe/challenge.ts
import { randomBytes } from "crypto";
import { saveNonce } from "@/server/siweRepo";
export async function POST() {
const nonce = randomBytes(16).toString("base64url"); // β₯96-bit randomness
await saveNonce({ nonce, ttlMinutes: 5 }); // used=false by default
const message = {
domain: process.env.AUTH_DOMAIN!,
uri: `https://${process.env.AUTH_DOMAIN!}`,
version: "1",
chainId: Number(process.env.CHAIN_ID!),
nonce,
issuedAt: new Date().toISOString(),
statement: process.env.SIWE_STATEMENT ?? "Sign in with Ethereum",
};
return Response.json({ message });
}// /api/auth/siwe/verify.ts
import { verifySiwe, consumeNonce } from "@/server/siweService";
import { bindEoaToIdentity, startSession } from "@/server/identityService";
export async function POST(req: Request) {
const { message, signature } = await req.json();
// 1) Verify signature & invariants (domain / uri / chainId)
const { address, nonce, valid } = await verifySiwe({ message, signature });
if (!valid) return new Response("Invalid SIWE", { status: 401 });
// 2) Nonce must be unused & unexpired β consume
const ok = await consumeNonce(nonce);
if (!ok) return new Response("Nonce invalid/expired", { status: 401 });
// 3) Bind EOA to current user identity (session required)
const userId = /* read from session cookie */ await getUserId();
const identityId = await bindEoaToIdentity({ userId, address });
// 4) Issue/refresh session
const res = await startSession({ userId });
return Response.json({ ok: true, identityId }, res.headers);
}// /api/auth/passkey/register/options.ts
import { generateRegistrationOptions } from "@/server/webauthn";
export async function POST() {
const userId = await getUserIdOrSignupContext();
const options = await generateRegistrationOptions({ userId });
// Store challenge in server-side session/DB to verify later
return Response.json(options);
}// /api/auth/passkey/register/verify.ts
import { verifyRegistrationResponse } from "@/server/webauthn";
import { saveCredential } from "@/server/webauthnRepo";
export async function POST(req: Request) {
const body = await req.json();
const userId = await getUserIdOrSignupContext();
const result = await verifyRegistrationResponse({ userId, body });
if (!result.verified) return new Response("Invalid attestation", { status: 400 });
await saveCredential({
userId,
credentialId: result.credentialId,
publicKey: result.publicKey,
signCount: result.signCount,
transports: result.transports,
backupEligible: result.backupEligible,
backedUp: result.backedUp,
});
return Response.json({ ok: true });
}// /api/auth/passkey/login/options.ts
import { generateAuthenticationOptions } from "@/server/webauthn";
export async function POST() {
const options = await generateAuthenticationOptions();
return Response.json(options);
}// /api/auth/passkey/login/verify.ts
import { verifyAuthenticationResponse } from "@/server/webauthn";
import { startSession } from "@/server/identityService";
export async function POST(req: Request) {
const body = await req.json();
const result = await verifyAuthenticationResponse(body);
if (!result.verified) return new Response("Invalid assertion", { status: 401 });
// Verify sign_count is monotonic, then start session
const res = await startSession({ userId: result.userId });
return Response.json({ ok: true, userId: result.userId }, res.headers);
}// /api/wallet/provision.ts
import { getOrCreateEoaForUser, deploySmartAccount } from "@/server/walletService";
import { saveWalletMapping } from "@/server/identityRepo";
export async function POST(req: Request) {
const userId = await getUserId(); // from session cookie
const identityId = await getIdentityId(userId);
// 1) Ensure EOA exists (for embedded path)
const eoa = await getOrCreateEoaForUser(userId);
// 2) Deterministic AA deploy (factory + SALT(identityId))
const aa = await deploySmartAccount({
eoa,
chainId: Number(process.env.CHAIN_ID!),
salt: `ib-${identityId}`, // or keccak(identityId)
});
// 3) Persist mapping
await saveWalletMapping({
identityId,
eoa: eoa.address,
aa: aa.address,
chainId: Number(process.env.CHAIN_ID!),
});
return Response.json({ identity_id: identityId, eoa: eoa.address, aa: aa.address });
}// dto.ts
export type IdentityDTO = {
identity_id: string;
eoa?: string;
aa?: string;
};
export type SiweChallenge = { message: string };
export type SiweVerifyReq = { message: string; signature: string };
export type WalletPersistReq = { eoa: string; aa: string };::contentReference[oaicite:0]{index=0}Platform = $5
Gas_raw = (AA_deploy_count Γ 0.06) + (sponsored_actions Γ 0.03)
Gas_total = Gas_raw Γ 1.025 # thirdweb paymaster surcharge (2.5%)
SMS_cost = SMS_count Γ local_rate # e.g., ID 0.4414
Grand_total = Platform + Gas_total + SMS_cost