> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://platform.bctrl.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://platform.bctrl.ai/_mcp/server.

# Log In With Vault Credentials

> Store a login with a TOTP seed once, then let agents or your own code sign in - including 2FA.

Put the credential in the [vault](/sdk/vault) once - username, password, and the TOTP seed - and automation signs in from then on. Two ways to use it: hand it to an agent by reference, or pull it into your own CDP code.

## Store the login

```ts
import { Bctrl } from "@bctrl/sdk";

const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });

await bctrl.vault.upsert("clients/acme/login", {
  type: "login",
  username: "agent@acme.com",
  password: process.env.ACME_PASSWORD!,
  totpSecret: process.env.ACME_TOTP!,      // the 2FA seed, not a code
  origins: ["https://app.acme.com"],       // where this login may be used
});
```

## Option A: an agent signs in by reference

Give a [hosted agent](/sdk/hosted-agents) the `vault` builtin through a [toolset](/sdk/tools). The agent looks the login up by name; the plaintext never passes through your code.

```ts
const toolset = await bctrl.toolsets.create({
  name: "acme-login",
  builtins: ["vault"],
});

const runtime = await bctrl.runtimes.create({ type: "browser", name: "vault-login" });
await bctrl.runtimes.start(runtime.id);

const result = await bctrl.runtimes.invocations.createAndWait(
  runtime.id,
  {
    action: "browserUse",
    instruction:
      "Go to https://app.acme.com, sign in with the stored Acme login (including the one-time code), and confirm the dashboard loads.",
    toolsetId: toolset.id,
  },
  { timeoutMs: 300_000 }
);

console.log(result.status);
```

## Option B: your own code signs in

For scripted logins, read the decrypted value and generate the current one-time code at the exact moment the form asks for it:

```ts
const login = await bctrl.vault.value("clients/acme/login");
if (login.type !== "login") throw new Error("expected a login secret");

await page.goto("https://app.acme.com/signin");
await page.fill("#email", login.username);
await page.fill("#password", login.password);
await page.click("button[type=submit]");

// The 2FA prompt - mint the code now, TOTP codes expire in ~30s.
const { code } = await bctrl.vault.totp("clients/acme/login");
await page.fill("#otp", code);
await page.click("button[type=submit]");
```

`vault.get` returns metadata only; only `vault.value` decrypts. Keep the two separate in your code so listings and dashboards never touch plaintext.

## Don't log in every run

Once the session works, pair this with a [profile-backed runtime](/cookbook/persistent-sessions) so the login survives restarts and you only do this dance when the session actually expires.

## Next

* [Vault](/sdk/vault) - secrets, prefixes, and space mounts
* [Persistent Sessions](/cookbook/persistent-sessions) - log in once, reuse forever
* [Tools & Toolsets](/sdk/tools) - what else agents can reach