Sign in with Google
Give every user a Sui wallet from their Google account (zkLogin). tide checks the sign-in, keeps each user's salt so their address never changes, and makes the proof that lets their browser sign. Your app never holds a user's key.
How it works
- Your app makes an ephemeral key pair and a nonce from it, and sends the user to Google with that nonce.
- Google returns an ID token (JWT). Send it to
/v1/zklogin/saltfor the user's salt and Sui address. The same Google account always gets the same address in your app. - Send the token and the ephemeral public key to
/v1/zklogin/proof. The proof lets the ephemeral key sign for the address untilmaxEpoch. - Sign transactions with the ephemeral key wrapped in the proof (
ZkLoginSigner).
The salt keeps the link between a Google account and its Sui address private. tide derives it from a secret seed, the issuer, your client id and the user's id, so it never changes and needs no database to stay the same.
Set up
- Get a tide API key at tide.ad/developers/keys. For calls from the browser, set its allowed origins to your site.
- Make a Google OAuth client (type: web application) and add your sign-in page as an authorized redirect URI.
- Register its client id to your key. tide accepts only tokens whose
audis a client id registered to the calling key. Addjwt(any ID token for that client id) to verify that it is yours; importing salts needs a verified client id.
curl -X POST "https://api.tide.ad/v1/zklogin/clients" \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{ "clientId": "1234-abc.apps.googleusercontent.com" }'Example
The whole flow in a browser app with @mysten/sui. Step 1 runs before the redirect, steps 2 and 3 on the page Google sends the user back to.
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction } from "@mysten/sui/transactions";
import { ZkLoginSigner, generateNonce, generateRandomness, getExtendedEphemeralPublicKey } from "@mysten/sui/zklogin";
const tide = (path: string, body: object) =>
fetch(`https://api.tide.ad${path}`, { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "content-type": "application/json" }, body: JSON.stringify(body) }).then((r) => r.json());
const sui = new SuiGrpcClient({ network: "mainnet", baseUrl: "https://fullnode.mainnet.sui.io:443" });
// 1. Before sign-in: an ephemeral key and the nonce Google puts in the token (keep both in sessionStorage)
const ephemeral = new Ed25519Keypair();
const maxEpoch = Number((await sui.core.getCurrentSystemState()).systemState.epoch) + 2;
const randomness = generateRandomness();
const nonce = generateNonce(ephemeral.getPublicKey(), maxEpoch, randomness);
location.href = `https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_GOOGLE_CLIENT_ID&response_type=id_token&scope=openid&redirect_uri=${encodeURIComponent(location.origin + "/auth")}&nonce=${nonce}`;
// 2. Back on /auth: the user's address and a proof for the ephemeral key
const jwt = new URLSearchParams(location.hash.slice(1)).get("id_token")!;
const { address } = await tide("/v1/zklogin/salt", { jwt });
const proof = await tide("/v1/zklogin/proof", { jwt, extendedEphemeralPublicKey: getExtendedEphemeralPublicKey(ephemeral.getPublicKey()), maxEpoch, randomness });
// 3. Sign and send until maxEpoch
const signer = new ZkLoginSigner({ ephemeralSigner: ephemeral, maxEpoch, inputs: proof, legacyAddress: false, address });
const tx = new Transaction();
await sui.signAndExecuteTransaction({ transaction: tx, signer });A proof stays valid until maxEpoch (Sui allows up to 30 epochs ahead). Keep it with the ephemeral key in the tab, and ask for a new one after that. Sending the same request again returns the same proof from the cache, free, so a page reload costs nothing.
Coming soon: gas sponsorship, so new users can send their first transaction before they hold any SUI.
Move existing users
If your users already have zkLogin addresses, import their salts before you switch. An imported salt wins over tide's own, so every address stays exactly the same. Salts are encrypted at rest, and only your key can write them for your client id.
- In bulk: up to 1,000 users per call, each as
{ subHash, salt }wheresubHashis the hex SHA-256 of the user'ssub(or sendsubitself). - At sign-in: for a user you did not export, send
{ jwt, salt }once with the salt you used before. - A user who already has a different imported salt, or who already signed in with tide's own salt, is skipped unless you pass
replace: true, because their address would change.
curl -X POST "https://api.tide.ad/v1/zklogin/import" \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"clientId": "1234-abc.apps.googleusercontent.com",
"users": [
{ "subHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "salt": "129390038577185583942388216820280642146" }
]
}'Check one user afterwards: /v1/zklogin/salt returns source: "imported" and their old address.
Backups
/v1/zklogin/backup returns a recovery kit for the signed-in user: the issuer, your client id (aud), the user's id at Google (sub), the salt and the address, as JSON and as a short code. Show it to the user to save in a file or a password manager.
The salt is the only part of a zkLogin wallet that tide keeps. With the kit and their Google account, a user can rebuild the same wallet with any standard zkLogin prover, even if tide is gone: sign in through your app's Google client as usual, send the token and the kit's salt to the prover, and sign. The address is computed from iss, aud, sub and salt alone (computeZkLoginAddress in @mysten/sui).
Anyone can check a kit at tide.ad/docs/login/recover: it rebuilds the address in the browser, and can confirm the kit against a fresh Google sign-in.
A "Back up your wallet" button for your app, from a small script with no dependencies:
<div id="backup"></div>
<script type="module">
import { backupButton } from "https://tide.ad/sdk/zklogin.js";
backupButton(document.getElementById("backup"), {
apiKey: "YOUR_API_KEY", // a key with your site in its allowed origins
getJwt: () => sessionStorage.getItem("jwt"), // the user's current ID token
});
</script>It shows the code and the address and offers the kit as a file. If your server calls /v1/zklogin/backup instead, pass getKit (returning that answer) in place of apiKey and getJwt.
Limits and pricing
| Salt, backup, import, client ids | Free |
| Proofs | 50,000 new proofs a month free per app, then 1,000 credits each ($0.003). A repeated request is answered from the cache and is free. |
| Proof time | About 5 seconds |
| Rate | 30 new proofs a minute per IP, 60 a minute per app on the Free plan (600 on paid plans), 20 an hour per user. Sign-in calls: 120 a minute per IP. |
| Shared free use | Apps calling from one network share 75,000 free proofs a month. A Free account whose address has no on-chain history gets 100 free proofs a day. |
| Issuers |
Over a limit the answer is HTTP 429 with retry-after. Past the free proofs with no credits left it is HTTP 402 with the USDC offers (see Pay with USDC). /v1/zklogin/clients shows this month's proofs and what is left.
Methods
| Method | What it does | Credits |
|---|---|---|
| zklogin.saltPOST /v1/zklogin/salt | The salt and Sui address for a Google sign-in. | Free |
| zklogin.proofPOST /v1/zklogin/proof | A zkLogin proof for an ephemeral key, ready to sign Sui transactions. | varies |
| zklogin.backupPOST /v1/zklogin/backup | A recovery kit for the signed-in user: issuer, client id, user id, salt and address. | Free |
| zklogin.importPOST /v1/zklogin/import | Import the salts your users already have, so their addresses stay the same. | Free |
| zklogin.clientAddPOST /v1/zklogin/clients | Register an OAuth client id (aud) to your account. | Free |
| zklogin.clientsGET /v1/zklogin/clients | Your registered sign-in client ids, proof usage this month and the free allowance. | Free |
| zklogin.clientRemoveDELETE /v1/zklogin/clients/:clientId | Remove a client id from your account. | Free |
| zklogin.configGET /v1/zklogin/config | Sign-in settings: accepted issuers, free allowance, proof price and whether the prover is up. | Free |