Search the docs⌘ K
  • Install Apps and find an appSilicon Apps · Start
  • Publish an appSilicon Apps · Start
  • Add sign-in to your appSilicon Accounts · Start
  • Get a Silicon accountSilicon Accounts · Start
  • Sign a Silicon into an appSilicon Accounts · Start
  • Verify a proofSilicon Accounts · Start
  • Receive webhooksSilicon Accounts · Start
  • Exchange, refresh, check and revoke tokensSilicon Accounts · Start
  • HTTP API referenceSilicon Accounts · Reference
  • ErrorsSilicon Accounts · Reference
  • silicon-accounts CLI referenceSilicon Accounts · Reference

Sign a Silicon into an app

As a Silicon, sign into any app with a short-lived token. You keep your STK to yourself, and the app exchanges the token for its own session.

InstructionsUpdated MarkdownEdit on GitHub
On this page

You as a Silicon sign into an app in two moves, through the CLI or the API. First you sign in to Silicon Accounts with your si:id and STK. Then you ask us for a short-lived token, an SLT, for the app you want to use.

You hand that token to the app, and the app exchanges it with us for access and refresh tokens, much like it exchanges the code from a Carbon’s browser sign-in. The app never receives your STK.

Shell
printf '%s' "$STK" | silicon-accounts login --silicon si:scout --stk-stdin   # once; the CLI keeps the session
SLT=$(silicon-accounts login --app remind -q)                                 # slt_…: one app, one use, 2 minutes
curl -s -X POST https://remind.example/silicon-login \
  -H 'Content-Type: application/json' -d "{\"slt\":\"$SLT\"}"

How an app takes the token is up to the app (here it's remind's POST /silicon-login), and step 3 covers what to look for. The whole exchange looks like this:

Text
Silicon ── si:id + STK ───────────▶ Silicon Accounts   POST /v1/silicons/login          → first-party tokens
Silicon ── app_id ────────────────▶ Silicon Accounts   POST /v1/me/short-lived-tokens   → slt_…
Silicon ── slt_… ─────────────────▶ the app            however the app asks for it
the app ── slt_… + its app secret ▶ Silicon Accounts   POST /v1/oauth/token (grant_type …:slt) → the Silicon's tokens + account

The app never sees your STK, and the token it gets works only for that app. Silicons and custodians explains why we built the flow this way.

1. Sign in

Give the CLI your si:id and STK. Pass the STK on stdin, so it never shows up in a process list or your shell history:

Shell
printf '%s' "$STK" | silicon-accounts login --silicon si:scout --stk-stdin
Text
Signed in as si:scout (Scout), a Silicon.
uuid          8HV
url           https://accounts.teamofsilicons.com
access token  2026-10-07T03:01:30Z (in 29m) (refreshed automatically)
session ends  2029-03-25T02:31:29Z (in 899d)

There are other ways to pass your credentials:

  • ACCOUNTS_SILICON=si:scout ACCOUNTS_STK=stk-… silicon-accounts login, for an environment that injects secrets as variables;
  • silicon-accounts login --silicon si:scout in a terminal prompts for the STK without echoing it;
  • --stk <value> works, but the CLI warns you, because arguments are visible to every process on the machine.

The CLI stores the session in {home}/.accounts/session.json (mode 0600). The access token lasts 30 minutes, and the CLI refreshes it on its own when less than a minute is left. The session itself ends 900 days after you signed in, however often it is refreshed. A CLI home holds one session. Signing in as another account there signs the previous one out, and running silicon-accounts login again while you're signed in answers Already signed in as si:scout (add --force to sign in anyway).

Check the session from a script:

Shell
silicon-accounts login status --json
JSON
{
  "authenticated": true,
  "display_name": "Scout",
  "expires_at": "2026-10-07T03:01:30.006Z",
  "id": "si:scout",
  "kind": "silicon",
  "refresh_expires_at": "2029-03-25T02:31:29.998Z",
  "url": "https://accounts.teamofsilicons.com",
  "uuid": "8HV",
  "verified": true
}

With --json it always exits 0, so read authenticated ({"authenticated":false} when you're not signed in); without --json it exits 1 when you're not signed in. verified says whether we confirmed the session just now; --offline reads only the stored file.

Over HTTP

Shell
curl -s -X POST https://accounts.teamofsilicons.com/v1/silicons/login \
  -H 'Content-Type: application/json' \
  -d '{"id":"si:scout","stk":"stk-59e5f08f3bbe","client_label":"scout on build-box"}'
JSON
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRldi0xIn0.eyJpc3MiOi…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "sar_peF7apNSIlunmOgn_5eeR3naY_mVAs1_FBGNSOoAkvU",
  "refresh_token_expires_at": "2029-03-25T02:35:39.401Z",
  "scope": "profile",
  "membership_id": "silicon-accounts:8HV",
  "account": {
    "uuid": "8HV",
    "membership_id": "silicon-accounts:8HV",
    "kind": "silicon",
    "id": "si:scout",
    "display_name": "Scout",
    "pfp_url": "https://iris.teamofsilicons.com/pfp/silicon?id=8HV",
    "custodian": {
      "uuid": "zQo",
      "id": "c:saket"
    },
    "updated_at": "2026-10-07T02:35:33.741Z",
    "version": 2
  }
}

These are first-party tokens (audience silicon-accounts). They act on your own account; they are not for apps. client_label (up to 100 characters) names this sign-in in silicon-accounts sessions list.

Refresh the access token before it expires, and store the new refresh token every time:

Shell
curl -s -X POST https://accounts.teamofsilicons.com/v1/oauth/token \
  -d grant_type=refresh_token -d client_id=silicon-accounts -d "refresh_token=$REFRESH_TOKEN"

Refresh tokens rotate on every use. If you present one that was already used, we revoke the whole sign-in, because only a stolen copy would ever be presented twice:

JSON
{"error":"invalid_grant","error_description":"This refresh token was already used once. Presenting a used refresh token revokes the whole sign-in to protect the account, so this sign-in is now revoked; sign in again."}

In Rust

Here's signing in and getting a short-lived token (step 2) with the silicon-accounts-client package:

Rust
use silicon_accounts_client::AccountsClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = std::env::var("ACCOUNTS_URL").unwrap_or_else(|_| "https://accounts.teamofsilicons.com".into());
    let client = AccountsClient::new(url)?;
    let si_id = std::env::var("ACCOUNTS_SILICON")?; // si:scout
    let stk = std::env::var("ACCOUNTS_STK")?; // stk-…

    let tokens = match client.silicon_login(&si_id, &stk, Some("scout on build-box")).await {
        Ok(tokens) => tokens,
        Err(err) if err.is_code("custodian_pending") => {
            eprintln!("{err}"); // message and hint: who has to accept, and until when
            return Ok(());
        }
        Err(err) => return Err(err.into()),
    };

    // A token for one app: single use, 2 minutes. Print it for the app.
    let session = client.with_token(tokens.access_token.expose());
    let slt = session.short_lived_token("remind").await?;
    println!("{}", slt.slt.expose());

    // Before the access token expires (30 minutes), rotate the pair; keep the new refresh token.
    if let Some(refresh) = &tokens.refresh_token {
        let renewed = client.refresh_first_party(refresh.expose()).await?;
        eprintln!("refreshed; access token valid for {} s", renewed.expires_in);
    }
    Ok(())
}

When signing in fails

codestatusCLI exitwhywhat to do
invalid_credentials4013no Silicon has this si:id, or the STK is wrong; both get the same answer so ids can't be probedcheck both; use the current si:id (ids can change); a lost STK is replaced by your custodian
login_locked423610 wrong STKs in a row; sign-in is locked for 60 seconds, and during the lock even the right STK is refusedwait details.retry_after_seconds (also in Retry-After)
custodian_pending4033your custodian hasn't accepted yet; details has the request id, its expiry and the custodianwait, or poll the request (Get a Silicon account)
custodian_declined, custodian_expired4033the account was released and never became activecreate it again, naming a Carbon who will accept
account_deleted4033the account was deleted by its custodianask your former custodian, or get a new account
invalid_stk4222not stk- plus 8 to 32 hex characterssend the STK exactly as it was shown
invalid_id4222not an si:id (a c: id, a typo)Carbons sign in with silicon-accounts login instead
rate_limited4296more than 60 Silicon sign-in attempts per minute from your networkwait details.retry_after_seconds

The tenth wrong STK in a row is itself answered with login_locked, and a correct sign-in resets the count. The CLI checks the STK's format before sending it, so a malformed STK fails right there on your machine with exit code 2.

2. Get a short-lived token

Shell
silicon-accounts login --app remind
Text
Short-lived token for remind as si:scout: single use, valid until 2026-10-07T02:33:45Z (in 1m). The app exchanges it with grant_type=urn:silicon:params:oauth:grant-type:slt.
slt_vcB-NXP89QQd386p0r0BTGcO6pC5tudXjLNuf6nBIpQ

The token goes to stdout and the explanation to stderr, so SLT=$(silicon-accounts login --app remind -q) captures just the token. With --json:

JSON
{
  "app_id": "remind",
  "expires_at": "2026-10-07T02:33:45.489Z",
  "slt": "slt_wc7V6A5o6pskrp0KYbZknpmBc2w6rmfjcgVzPYvmK7g"
}

Not signed in yet? Do both in one command:

Shell
printf '%s' "$STK" | silicon-accounts login --silicon si:scout --stk-stdin --app remind -q

Over HTTP, with your first-party access token:

Shell
curl -s -X POST https://accounts.teamofsilicons.com/v1/me/short-lived-tokens \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' -d '{"app_id":"remind"}'

201 Created:

JSON
{"app_id":"remind","expires_at":"2026-10-07T02:37:46.761Z","scope":"profile timezone","slt":"slt_cO2a3ENrXHZBq36SkLVBtAuQMINeLP4k2xM93ur-2HE"}

In Rust it's client.with_token(access_token).short_lived_token("remind").await?, as in the program above.

What you should know about the token:

  • Single use. The first exchange uses it up, whether it succeeds or not.
  • Valid for 120 seconds. Ask for it right before you hand it over.
  • Bound to one app. If any other app presents it, it's refused and used up.
  • Already scoped. scope is what the app will see: always profile, plus timezone and dob when the app's sign-in setup asks for them. Silicons have no email or phone, so an app that requires an email still lets you in; it just never receives one.
codestatuswhy
unknown_app404no app has this app_id
app_disabled403the app exists but is disabled in Silicon Apps
first_party_app422silicon-accounts is Silicon Accounts itself, which you are already signed in to
not_signed_in, session_ended(CLI)no session in this home, or it ended (signed out, revoked, STK rotated): sign in again

3. Hand it to the app

The app tells you how to deliver the token: an HTTP endpoint like POST /silicon-login, a header, or a field in its own CLI. For those two minutes, treat the token like a password. Send it over https only and never log it. If the app reports a failure, get a new token, because a used, expired or refused token can't be retried.

4. Exchange the token (for apps)

This part is for your app. When a Silicon hands you an SLT, exchange it at our token endpoint with your app's own credentials:

Shell
curl -s -u "remind:$REMIND_APP_SECRET" https://accounts.teamofsilicons.com/v1/oauth/token \
  -d grant_type=urn:silicon:params:oauth:grant-type:slt -d "slt=$SLT"
JSON
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRldi0xIn0.eyJpc3MiOi…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "sar_lpYj7WW7VfC0xv2yCQxmbQwQ_OJ05-dNVn6OYZc7vNw",
  "refresh_token_expires_at": "2029-03-25T02:31:52.745Z",
  "scope": "profile timezone",
  "membership_id": "remind:8HV",
  "account": {
    "uuid": "8HV",
    "membership_id": "remind:8HV",
    "kind": "silicon",
    "id": "si:scout",
    "display_name": "Scout",
    "pfp_url": "https://iris.teamofsilicons.com/pfp/silicon?id=8HV",
    "timezone": "Europe/Berlin",
    "custodian": {
      "uuid": "zQo",
      "id": "c:saket"
    },
    "updated_at": "2026-10-07T02:31:16.356Z",
    "version": 2
  }
}

We also accept grant_type=slt as a short alias. Your app's credentials go in HTTP Basic auth (or as client_id and client_secret form fields). The access token is an EdDSA-signed JWT made for your app:

JSON
{
  "iss": "https://accounts.teamofsilicons.com",
  "sub": "8HV",
  "aud": "remind",
  "exp": 1791342112,
  "iat": 1791340312,
  "nbf": 1791340312,
  "jti": "01a11433-f8ac-7323-8be0-9d164ab50069",
  "kind": "silicon",
  "id": "si:scout",
  "mid": "remind:8HV",
  "fid": "01a11433-f8ac-7323-8be0-9d1521cfde01",
  "scope": "profile timezone"
}

Key your records on account.uuid (or membership_id), never on account.id. The si:id can change, and the uuid never does (Ids and uuids). A Silicon's custodian is always there, and email and phone never are. What apps see covers every field and scope.

Here's the app's POST /silicon-login in TypeScript (Node 22 or later):

TypeScript
const ACCOUNTS_URL = process.env.ACCOUNTS_URL ?? 'https://accounts.teamofsilicons.com';

// POST /silicon-login {"slt": "slt_…"}: exchange it and start this app's own session.
export async function exchangeSlt(slt: string) {
  const res = await fetch(`${ACCOUNTS_URL}/v1/oauth/token`, {
    method: 'POST',
    headers: {
      Authorization: 'Basic ' + Buffer.from(`${process.env.APP_ID}:${process.env.APP_SECRET}`).toString('base64'),
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({ grant_type: 'urn:silicon:params:oauth:grant-type:slt', slt }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error}: ${body.error_description}`); // e.g. invalid_grant: …already used…
  return body as {
    access_token: string; refresh_token: string; expires_in: number; scope: string;
    account: { uuid: string; membership_id: string; kind: 'carbon' | 'silicon'; id: string;
               display_name: string; timezone?: string; custodian?: { uuid: string; id: string } };
  };
}

In Rust:

Rust
use silicon_accounts_client::AccountsClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let slt = std::env::args().nth(1).ok_or("usage: exchange <slt_…>")?;
    let url = std::env::var("ACCOUNTS_URL").unwrap_or_else(|_| "https://accounts.teamofsilicons.com".into());
    let client = AccountsClient::new(url)?;
    let app = client.as_app("remind", std::env::var("APP_SECRET")?);

    let tokens = app.exchange_slt(&slt).await?;
    let account = tokens.account.expect("token responses carry the account");
    println!("{} signed in as {} (membership {})", account.id, account.uuid, account.membership_id);
    Ok(())
}

From the command line, for testing:

Shell
printf '%s' "$REMIND_APP_SECRET" | silicon-accounts app --app-id remind --app-secret-stdin token slt "$SLT"

A successful exchange is a sign-in. The Silicon joins your app's user base (source slt), and your app gets a refresh token valid for 900 days from that moment. Every refused exchange answers 400 invalid_grant with the exact reason, and still uses the token up:

error_description starts withwhy
The short-lived token was already usedit was exchanged before
The short-lived token expired at …more than 120 seconds passed
The short-lived token was issued for the app 'remind', not for 'briefcase'another app presented it
The short-lived token is not knownmistyped, or never issued
slt must be a short-lived token (it starts with slt_), but this is a refresh token.the wrong kind of token
The short-lived token was issued at … by a sign-in of si:rusty that ended when its custodian rotated its STK at …the STK was rotated after the token was issued

Wrong app credentials answer 401 invalid_client, and a missing slt answers 400 invalid_request. An SLT issued before the Silicon removed your app's access is refused too. One issued after that is a new sign-in, and it restores the access.

Keep the sign-in, or end it

The app keeps its own session. It refreshes with its refresh token (which rotates, as above) and listens on its webhook for changes: account.id_changed when the si:id changes, account.updated when a field it can see changes, silicon.custodian_changed after a transfer, and membership.signed_out or membership.access_removed when the sign-in ends. See Receive webhooks.

Rotating an STK ends the Silicon’s sign-ins. Its CLI session answers session_ended, its refresh tokens stop working, and introspection reports its tokens as inactive. Each app gets membership.signed_out with reason: stk_rotated. The Silicon has to sign in again with the new STK and get another SLT.

One catch: a local signature check can't see that a session has ended. An existing access token can still pass a check against the JWKS until it expires, up to 30 minutes later. If your app needs to cut access off immediately, use introspection or handle the sign-out webhook.

The Silicon can see and leave the apps it signed into:

Shell
silicon-accounts apps list
Text
APP     NAME    STATUS  SHARED            LAST SIGN-IN
remind  Remind  active  profile timezone  2026-10-07T02:43:20Z

silicon-accounts apps remove remind revokes the app's tokens for you and the User verification proofs it issued about you, marks the membership access_removed, and tells the app (membership.access_removed). If you exchange a new SLT there later, the membership is active again.

Run several Silicons on one machine

Give each Silicon its own CLI home, so their sessions don't replace each other:

Shell
SILICON_HOME=/srv/silicons/scout  silicon-accounts login --app remind -q
SILICON_HOME=/srv/silicons/ledger silicon-accounts login --app remind -q

Many processes can share one home. The CLI refreshes the session under a file lock, so two processes never present the same refresh token (which would end the session). The details are in Use the silicon-accounts CLI.

Next

Related

Silicon Accounts · Learn · ExplanationSilicons and custodiansWhy every Silicon has a custodian, how its STK is looked after, why it signs in to apps with short-lived tokens, and how it runs in CI and the cloud with no stored secret.Silicon Accounts · Start · InstructionsGet a Silicon accountCreate your own Silicon account and name your Carbon as its custodian, or let your Carbon create it for you. Save the STK the moment you see it, and wait for approval if you need to.Silicon Accounts · Start · InstructionsExchange, refresh, check and revoke tokensTurn a sign-in into tokens, keep the session going, check who a token belongs to and end it when the account signs out.Silicon Accounts · Learn · ExplanationTokens and sessionsWhat access and refresh tokens do, why a refresh token changes every time you use it, and what ends a sign-in.Silicon Accounts · Learn · ExplanationWhat your app sees about an accountWhich account details your app gets, how a Carbon agrees to share them, and how webhooks keep your copy up to date.Silicon Accounts · Reference · Referencesilicon-accounts CLI referenceEvery silicon-accounts command, flag, environment variable and exit code, with examples for reading results and handling failures in scripts.

Every page is plain Markdown at its address plus .md. Silicons can read llms.txt, llms-full.txt or the docs index, or call the MCP server.