Integrations

Karatum × Lit Protocol: only certified agents may decrypt or act

Testnet pilotSource: docs/integrations/lit.md

Testnet integration. Karatum reads come from the KaratumReputationFeed on Base, Ethereum, OP and Arbitrum Sepolia. There is no mainnet deployment and no partnership with Lit Protocol. This integration uses Lit's public developer API.

A secret is encrypted so that only an agent wallet certified on Karatum with karats ≥ N, a fresh standing and no active recall can decrypt it. The same gate can also co-sign a payload for a certified agent: the "act" half of the integration.

PieceWhere
Lit Action (enforcement, runs on Lit)packages/lit/src/karatum-gate.action.js
TS API: encryptForCertifiedAgents, decryptAsAgent, attestAsAgent, verifyAttestation, gateSignerpackages/lit/src/gate.ts
Lit v3 REST clientpackages/lit/src/chipotle.ts
Local read-only twin of the rule (checkCertified)packages/lit/src/check.ts
Lit EVM contract conditions for the same rulepackages/lit/src/conditions.ts
Demoscripts/lit-demo.ts

Which Lit network, and why not access-control conditions

The brief asked for Lit's free dev network (datil-dev / naga-dev) with EVM contract conditions. That path no longer exists.

  • Naga (V1, including naga-dev) was sunset on 2026-04-01 in the move to Lit v3. Datil stayed up only for migrations. Both ran on the Chronicle Yellowstone chain, whose RPC (yellowstone-rpc.litprotocol.com) no longer resolves. createLitClient({ network: nagaDev }) from @lit-protocol/lit-client@8.3.1 fails at connect, when it reads the staking contract. This was checked on 2026-10-27.
  • Lit v3 ("Chipotle") is the live network, at https://api.chipotle.litprotocol.com/core/v1. Access-control conditions are gone: gating is plain JavaScript inside a Lit Action, and secrets are sealed with Lit.Actions.Encrypt/Decrypt under a vault PKP. Chipotle is paid. Running an action costs $0.01 per second, with a $5 minimum top-up. It needs an account, and the docs list no free dev network. Read-only calls are free, including resolving an action's CID.

So the enforcement point is a Lit Action on Chipotle. buildEvmContractConditions() still emits the rule in the Lit SDK's evmContractConditions format, and that output validates against EvmContractConditionsSchema from @lit-protocol/access-control-conditions-schemas@8.0.2. It is the declarative form of the policy, but no live Lit network evaluates it today.

The rule

On the chain named in the policy, the gate calls getStandingWithStatus(agent, maxAge) on the feed at 0x3e98094028cF9879A82B66f62f16edCaEfE7f16b. It then applies this order:

  1. recalled → RECALLED (the feed ORs in the live KaratumIncidentLog where one is wired)
  2. never seen by the feed (updatedAt == 0) → NOT_CERTIFIED
  3. stale (older than maxAge, where 0 means the feed heartbeat of 24 h; or the feed is paused) → STALE
  4. no certified class (classes == 0) → NOT_CERTIFIED
  5. karats < minKarats → BELOW_MIN_KARATS
  6. otherwise OK

evaluateStanding() in policy.ts is the same rule in TS. A unit test runs all 96 combinations through both copies and checks that they agree.

The EVM-conditions form has to differ. An ACC can't index into the feed's Standing tuple, so it reads isRecalled and isStale from the feed, plus status(agent, taskType) (s == VALID, karats >= N) from the Guard KaratumCertRegistry. That registry exists on Base Sepolia and Ethereum Sepolia only, and an agent passes only after its cert has been prove()d into the registry. The demo agent's registry status on Base Sepolia is NONE, even though its feed standing is 19K.

How the Lit Action is safe against its own callers

js_params are caller-controlled, so the action takes nothing security-relevant from them at decrypt time:

  • The policy is sealed inside the ciphertext. encrypt seals {v, policy, data}. decrypt uses the sealed policy, so a caller can't lower minKarats.
  • The feed address and the RPC for each chain are constants of the action source. They are part of its CID, so a caller can't point the gate at a fake feed or a lying RPC. The RPCs are public endpoints; Alchemy is not used.
  • The caller proves wallet ownership. The agent signs an EIP-191 request containing the version, op, agent, keccak256(ciphertext) and issuedAt. The action rejects a different signer, a request for a different ciphertext, a request older than 300 s, and one more than 60 s in the future.
  • The action fails closed. An RPC error, an unknown op, a malformed envelope or a foreign vault all return ok:false with no data.
  • Attestation. attest signs a statement (agent, chain, feed, karats, minKarats, payload hash, time) with getLitActionPrivateKey(). That key is derived from the action CID. Verifiers pin the gate's signer address, from gateSigner() or Lit's getLitActionWalletAddress(<CID>), and call verifyAttestation().

Residual risks:

  • The plaintext goes back to whoever makes the API call that carries the agent's signature. That signature is bound to one ciphertext and lives 5 minutes.
  • The vault PKP's group must permit only the gate CID. Any other action permitted on that PKP could decrypt the vault.

Current gate CID (computed by Lit for the committed source): QmZpczD4erLq7aVmuEzhtA6JXoTkciH1FvSz7RTPgASgF1. Any edit to the action file changes it.

Demo run (2026-10-27, server, Base Sepolia)

== 1. Lit network status
  Datil / Naga (incl. naga-dev): yellowstone-rpc.litprotocol.com DOES NOT RESOLVE (ENOTFOUND)
  Lit v3 (Chipotle) /version -> {"name":"lit-api-server","version":"0.1.0","commit_version":"v1.1.19"}
== 2. On-chain evidence
  certified test agent  0x220A2FCa33365C39AB18fa293c70e1286dA79434  block 50956307  classes=1 karats=19 recalled=false stale=false -> OK
  throwaway wallet      0x602A75d7850325A8fC0C036fb452b53865DC37DD  block 50956309  classes=0 karats=0 recalled=false stale=true  -> NOT_CERTIFIED
== 3. Lit Action CID (computed by Lit): QmZpczD4erLq7aVmuEzhtA6JXoTkciH1FvSz7RTPgASgF1
== 4. NOT RUN: LIT_API_KEY and LIT_VAULT_PKP_ID are not set   (exit 2)

Steps 1 to 3 are real. Step 4, the actual Lit encrypt, decrypt and attest calls, needs the owner setup below. The full flow is covered by unit tests that run the exact action bytes against a mocked Lit runtime: packages/lit/src/*.test.ts, 36 tests.

Owner setup

These steps need a person: creating the account and paying can't be automated here.

  1. Create a Chipotle account in the dashboard and add funds, minimum $5. A demo run uses about 5 action executions, each a few cents.
  2. Mint a vault PKP (dashboard or POST /create_wallet) and note its id.
  3. Create a group that contains the vault PKP and only the gate action CID above. Register the action from the source in packages/lit/src/karatum-gate.action.js; the CID must match.
  4. Create a usage API key that may run only that group.
  5. Run doppler run -p _factory -c stg_karatum -- npx tsx scripts/lit-demo.ts --json and expect exit 0:
    • the certified agent decrypts;
    • the throwaway wallet is refused with NOT_CERTIFIED;
    • the attestation verifies.

Usage

ts
import { ChipotleClient, encryptForCertifiedAgents, decryptAsAgent } from "@karatum/lit";

const ctx = { lit: new ChipotleClient({ apiKey: process.env.LIT_API_KEY! }), pkpId: process.env.LIT_VAULT_PKP_ID! };
const sealed = await encryptForCertifiedAgents("secret", { minKarats: 18, chain: "base-sepolia" }, ctx);
const { data, standing } = await decryptAsAgent(sealed, agentAccount /* viem LocalAccount */, ctx);
// throws LitGateError { code: "NOT_CERTIFIED" | "BELOW_MIN_KARATS" | "RECALLED" | "STALE" | ... }