Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Build with Altana

Authorize across chains

What you need
Goal
I want a session granted on one chain to be verifiable on another without granting it again.
Who it's for
Developers who grant a session on one chain and need to verify or act on it from another.
What you'll use
grantSession on L1, ensureKeyCached to mirror, then verify from L2.
Prerequisites
A wallet with an active agent session, granted in Give an agent a wallet and a policy.

How it works

Sessions are granted on L1 (Ethereum, the Keystore source of truth). An L2 cache on Base can verify that same session via a storage proof against L1 state, without any bridge message or re-granting.

ensureKeyCached handles the proof:

  1. Reads the L2 cache. If the key is already there, returns immediately (cache-hit).
  2. Polls the L2's L1Block predeploy until it anchors past the relevant L1 block (waiting-for-anchor, typically 1–3 min)
  3. Fetches an eth_getProof from L1 and submits it to the L2 cache (submitting-proof)
  4. Returns once the cache confirms the key is live (done)

After step 4, any tool on Base can call isValidKey on the cache, for free, from any RPC.

The three steps below follow the flow above: grant on L1, mirror to the L2 cache, then verify on L2.

Step 1: Grant the session on Ethereum (L1)

Sessions are granted on L1, the Keystore source of truth. This is the same grantSession you have already used, pointed at Ethereum.

import {
  createClient,
  ensureKeyCached,
  ETHEREUM,
  BASE,
  signerFromPrivateKey,
} from "@altananetwork/sdk";
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet, base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
 
// 1. Grant the session on Ethereum (L1, source of truth).
const client = createClient({ chains: [ETHEREUM] });
const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = await client.createWallet({ signer });
 
const session = await client.grantSession({
  wallet,
  signer,
  permissions: {
    calls: [{ to: "0xSomeContract..." }],
    spend: [{ limit: 50_000_000_000_000_000n, period: "day" }],
  },
  expiry: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
});

Step 2: Mirror the session to the Base L2 cache

ensureKeyCached proves the L1 authorization into an L2 cache. It waits for Base to anchor past the L1 block, submits the storage proof, and returns once the key is live. Any funded account can pay the L2 gas.

// 2. Mirror the session key to the Base L2 cache.
const l1Client = createPublicClient({ chain: mainnet, transport: http(ETHEREUM.publicRpcUrl) });
const l2Client = createPublicClient({ chain: base, transport: http(BASE.publicRpcUrl) });
 
// The L2 wallet client pays L2 gas to submit the proof.
// Any funded account works — your own EOA, a backend relayer, etc.
const l2WalletClient = createWalletClient({
  account: privateKeyToAccount(process.env.RELAYER_KEY as `0x${string}`),
  chain: base,
  transport: http(BASE.publicRpcUrl),
});
 
await ensureKeyCached({
  l1Client,
  l2Client,
  l2WalletClient,
  l1KeyStore: ETHEREUM.keyStore,
  l2Cache: BASE.keyStoreCache,
  user: session.walletAddress,
  publicKey: session.publicKey,
  onStatus: (s) => console.log(s), // cache-hit | waiting-for-anchor | submitting-proof | done
});

Step 3: Verify the session from Base

Once cached, any tool on Base can confirm the session with a single free read, with no L1 round trip.

// 3. Verify the session from Base — free, from any RPC.
const { keccak256 } = await import("viem");
 
const CACHE_ABI = [{
  name: "isValidKey", type: "function", stateMutability: "view",
  inputs: [
    { name: "user", type: "address" },
    { name: "keyId", type: "bytes32" },
  ],
  outputs: [{ type: "bool" }],
}] as const;
 
const keyId = keccak256(session.publicKey);
const isValid = await l2Client.readContract({
  address: BASE.keyStoreCache,
  abi: CACHE_ABI,
  functionName: "isValidKey",
  args: [session.walletAddress, keyId],
});
 
console.log("Session valid on Base:", isValid); // true

Why it is different

Authorization crosses chains by storage proof, permissionlessly, with no bridge message and no per-chain re-granting. The first cross-chain action pays the proof cost once; after that, the L2 cache serves reads locally without L1 round-trips.

What's next

Browse all guides: see the full map of what you can build with Altana.