Module M.03

API Key Leakage

The Transparent Secret

GenLayer contracts are executed by multiple validators who can inspect the full source code and state. Hardcoding an API key means every validator sees it. A malicious validator could extract and abuse your key, leading to cost draining or account suspension.

Side-by-side · Vulnerable vs. Patched

two contracts · proven by paired transactions
vulnerablecontracts/vulnerable/VulnerableAPI.py
Failed TX
> consensus failed · validators diverged
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *45# Module 3 (Vulnerable) -- API key leakage.6# Anti-pattern: a third-party API key embedded in contract source AND7# state. Validators don't have to "read" anything special -- the contract8# bytecode and storage are public on-chain, so anyone reading the explorer9# sees the secret. Also, web.render to a real third-party API is wasteful10# when GenLayer validators already have first-class LLM access.1112# WARNING: this is a deliberate anti-pattern. The value below is NOT a real13# key -- it's a clearly-fake placeholder shaped to avoid matching any14# provider's secret-scanning regex. The lesson is structural, not literal.15LEAKED_API_KEY = "FAKE-demo-key-do-not-use-visible-on-chain-0000"16OPENAI_URL = "https://api.openai.com/v1/chat/completions"171819class VulnerableAPI(gl.Contract):20    api_key: str21    last_summary: str2223    def __init__(self):24        # Storing a secret in state is the bug.25        self.api_key = LEAKED_API_KEY26        self.last_summary = ""2728    @gl.public.write29    def summarize(self, text: str) -> None:30        # web.request with an external auth token is doubly bad:31        #   - the secret is visible to anyone reading the deployed code32        #   - the call will return 401 because the key is fake,33        #     producing a clean FINISHED_WITH_ERROR on Bradbury.34        bounded = text[:512]3536        def _call() -> str:37            url = OPENAI_URL + f"?demo_key={self.api_key}&q={bounded}"38            resp = gl.nondet.web.request(url, method='GET')39            if resp.status_code >= 400:40                raise Exception(f"api returned {resp.status_code}")41            return resp.body.decode("utf-8", errors="replace")[:200]4243        self.last_summary = gl.eq_principle.strict_eq(_call)4445    @gl.public.view46    def get_api_key(self) -> str:47        # Even worse: a public getter for the secret.48        return self.api_key
patchedcontracts/patched/SafeAPI.py
Success TX
> consensus reached · all validators agree
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *45# Module 3 (Patched) -- Safe summarisation.6# No third-party API, no secret. Validators run the LLM themselves via7# gl.nondet.exec_prompt and reach consensus through prompt_comparative.8# If you genuinely need a private API, the right pattern is an off-chain9# proxy that signs/authenticates separately and is referenced by URL only.101112class SafeAPI(gl.Contract):13    last_summary: str14    config_proof: str1516    def __init__(self):17        self.last_summary = ""18        self.config_proof = ""1920    @gl.public.write21    def summarize(self, text: str) -> None:22        # Bound input to keep prompts predictable across validators.23        bounded = text.strip().replace("```", "ʼʼʼ")[:1000]2425        def _llm() -> str:26            prompt = (27                "Summarise the following text in one sentence (max 25 words). "28                "Output the summary only, no preface.\n"29                f"```\n{bounded}\n```"30            )31            return gl.nondet.exec_prompt(prompt).strip()3233        self.last_summary = gl.eq_principle.prompt_comparative(34            _llm,35            principle="Summaries must convey the same key information.",36        )3738    @gl.public.write39    def demonstrate_fix(self) -> None:40        """Deterministic proof that no third-party API key lives in this41        contract -- the set of storage fields does not include one."""42        self.config_proof = "NO_API_KEY_IN_STATE; validators run LLM directly"4344    @gl.public.view45    def get_last_summary(self) -> str:46        return self.last_summary4748    @gl.public.view49    def get_config_proof(self) -> str:50        return self.config_proof
Call invoked
summarize("GenLayer runs Intelligent Contracts w...")

calls api.openai.com with fake key -> 401 / external error

Call invoked
demonstrate_fix()

no api key in contract state -- structural fix proven by config_proof getter

On-chain receipts

Knowledge check · M.03

01 / 02

Two questions on this incident. Pick the best answer; the question locks once committed.

Question 01 / 02
Why can't you hide API keys in GenLayer contracts?