Kasada's KPSDK: the 128-bit token, the VM, and the obfuscated bytecode
Load a page behind Kasada and watch the network panel. Before the real request goes out, the browser fetches a script with a name like /ips.js, runs it, and only then sends your request carrying two headers you did not write: x-kpsdk-ct and x-kpsdk-cd. Strip them and you get a 429. Forge them with a plausible-looking string and you still get a 429. Replay a captured pair a few seconds later and, depending on the deployment, that fails too. The headers are not a session cookie in the usual sense. They are the receipt for a computation the browser was forced to perform, and the server keeps enough state to know whether that computation was real.
So what is the computation, and why is the code that produces it so hard to read? The short version is that Kasada ships its client logic not as ordinary JavaScript but as bytecode for a custom virtual machine, bundled together with the interpreter that runs it. You can open the script. You can even pretty-print it. What you get back is a dispatcher loop and a table of tiny handler functions, with the actual fingerprinting and proof-of-work logic encoded as data that only means something once the VM executes it. The exact internal field layout is not documented by Kasada, so where this post gets specific about bytecode formats or token contents it is drawing on independent reverse-engineering write-ups and observed traffic, and it flags which is which.
The sections below go in the order the browser experiences them. First, what Kasada is and where the KPSDK sits. Then the request flow: the /fp probe, the /ips.js script, the /tl POST, and the headers that come back. Then the two tokens, CT and CD, and what is known and not known about their contents. Then the heart of it, the VM and its bytecode, with reference to a real devirtualization of Kasada’s interpreter. Then the proof-of-work and why timing matters. Then per-tenant rotation and what it costs an attacker. A closing section on where the design is genuinely strong and where it leaks.
What Kasada is, and what KPSDK refers to
Kasada is an anti-bot vendor founded in Sydney in 2015 by Sam Crowther, who had worked on bot defence at a large Australian bank and interned at the Australian Signals Directorate before starting the company at nineteen. The product sits in front of web and mobile traffic and decides, on the first request, whether a client is a real browser or automation. It does this without showing a CAPTCHA in the normal case. When it is confident a request is automated it returns a block, usually a 429 or a 403, rather than a puzzle.
The company shipped its V2 platform in March 2021, and the announcement is worth reading for what it admits about the design. V2 increased the number of client interrogation sensors by roughly fifteen times, aimed at detecting Puppeteer, Playwright, stealth plugins, headless browsers, and mobile emulators. It leaned harder on a proprietary, patented obfuscation built around a custom interpreter, the explicit goal being to slow down anyone trying to reverse or retool against the client. Crowther’s line at launch was that the right posture is to force automated clients to “prove they’re from a source controlled by a human,” and to make that proof continuously expensive rather than checking it once.
KPSDK is the name that surfaces in the wire protocol. Every header Kasada adds is prefixed x-kpsdk-, and practitioners refer to the whole client component as the KPSDK or the Kasada SDK. It is the bundle of script, VM, bytecode, and the small protocol of endpoints and headers that together produce the tokens. This post is about that client component specifically. Kasada also runs server-side scoring, TLS and HTTP/2 fingerprinting, and IP reputation, and those feed the same verdict, but the SDK is the part that lives in your browser and the part this piece traces.
A note on the title. The two tokens are widely described as cryptographic values of fixed width, and the CT token in particular reads as a compact fixed-length string in captures, which is where the “128-bit” shorthand comes from. Kasada does not publish the token’s internal structure, so treat the bit-width as a characterisation of how the value behaves, a short fixed-size cryptographic token, rather than a documented spec. The mechanism matters more than the exact width, and the mechanism is knowable.
The request flow: /fp, /ips.js, /tl
The KPSDK handshake has a recognisable shape, and you can map it from the endpoint paths and the headers alone. When a client first hits a protected route without valid tokens, Kasada serves a challenge response. In the documented flows this comes back as a 429 whose HTML embeds the path to the challenge script, and the script path carries a per-tenant prefix and a version query. A real example from a public write-up looks like a pair of UUIDs followed by fp?x-kpsdk-v=j-1.1.0, and the script itself is referenced as ips.js? with parameters. The x-kpsdk-v value, something like j-1.1.0, is the version pin for the SDK build the tenant is currently serving.
The script, once fetched, is the KPSDK proper. It is the obfuscated bundle that performs fingerprinting and the proof-of-work, and it produces a binary payload. That payload is POSTed to a token endpoint, documented in third-party SDK integrations as /tl, with Content-Type: application/octet-stream. The body is binary, not form-encoded and not JSON. The /tl response carries the session token in x-kpsdk-ct and a server timestamp in x-kpsdk-st. Some deployments add a configuration fetch (an /mfc endpoint appears in one integration’s flow, returning feature-config and a signature header used in later proof-of-work), but the load-bearing three are the /fp probe, the /ips.js script, and the /tl POST.
After that, every protected request carries the tokens. The CT value rides along unchanged for the life of the session; the CD value is recomputed per request. The full header family seen across documented integrations includes x-kpsdk-ct (the session token), x-kpsdk-cd (the per-request proof-of-work data), x-kpsdk-st (the server timestamp the client must echo and build on), x-kpsdk-v (the version pin), and a signature-style header (x-kpsdk-h) whose job is to bind CT and CD together so they cannot be mixed across sessions. Treat the exact set as deployment-dependent; not every tenant emits every header, and the names above come from independent integrations rather than Kasada documentation.
The two tokens: CT and CD
The cleanest way to understand the token pair is by cost. One is expensive to make and reusable; the other is cheap to make and single-use. That asymmetry is the whole design.
x-kpsdk-ct is the expensive one. It is minted once, from the full fingerprint and proof-of-work the VM computes on first load, and it is returned by the /tl endpoint. In observed behaviour it is reusable across requests for the life of a session, with practitioner notes putting the practical session window around thirty minutes on typical deployments, shorter on hardened ones. Think of CT as the certificate that says “this browser did the hard work once and here is the signed proof.” The server can re-check it cheaply on every subsequent request without making the client redo the whole interrogation.
x-kpsdk-cd is the cheap one, and it is where the proof-of-work answer lives on each request. Unlike CT, it is meant to be regenerated for every single request, and it is single-use: once the server has seen a given CD value it will not accept it again. Independent write-ups that have looked at decoded CD payloads describe it as a small JSON structure carrying fields named along the lines of workTime, id, answers, duration, and st. Read those names with care, because they come from one researcher’s deobfuscation of a particular SDK build, not from Kasada, and Kasada rotates the build. The shape is the point: a per-request token that reports how long the work took, what the answer was, and which server timestamp it was computed against.
The timestamp matters more than it looks. x-kpsdk-st is a server-supplied clock value, returned at token issuance, and the per-request CD work is computed relative to it. That ties each proof to a server-anchored time rather than the client’s local clock, which closes the obvious replay trick: you cannot precompute a stockpile of CD answers in advance because you do not know the server timestamp you will be asked to build on, and you cannot reuse an old one because CD is single-use and the freshness check has a tight window. Practitioner notes describe the acceptable gap between the timestamp and submission as small, on the order of a few seconds. The signature header then binds CT and CD so the pair has to come from the same lineage. None of this requires the server to store much; it stores a little session state for CT and checks CD against the timestamp and a seen-set.
The VM and the bytecode
Here is the part that makes Kasada different from most of its peers. Akamai’s sensor script, for all its obfuscation, is still JavaScript: you can deobfuscate it, rename the variables, and eventually read the logic as code. Kasada compiles its logic into bytecode for a virtual machine it ships alongside, so what you read in the script is the interpreter, not the program. The fingerprinting checks, the proof-of-work, the anti-tamper logic, all of it is encoded as data that only the VM understands.
The general technique is well documented in the reverse-engineering literature. A virtualizing obfuscator turns the protected code into an array of bytes that encode a custom instruction set, then ships a VM that runs a fetch-decode-execute loop over those bytes. A central dispatcher reads the next opcode, looks up the matching handler, runs it, and loops. State lives in a virtual instruction pointer and a virtual stack or register file rather than in real machine registers. The semantics are smeared across dozens of tiny handler functions, so no single place in the code tells you what the program does. Recovering the original logic, called devirtualization, means reconstructing the instruction set, the state model, and the control flow all at once, and that is deliberately laborious.
*The shape of a virtualizing obfuscator, with labels matching one published devirtualization of Kasada's interpreter. The handler table and register file are real code; the program lives in the bytecode they consume.*The labels in that diagram are not generic. A detailed public devirtualization of the Kasada VM deployed on Nike’s site walks through exactly this structure. In that build the bytecode arrived as a base62 string over the alphabet a-zA-Z0-9 with a decoding constant, run through a decodeBytecode() function into a numeric array, with a slice carved off to hold the strings section. The interpreter was a state-machine function the author labelled S(t), running an argumentless loop. An array, g, held roughly sixty opcode handlers as short atomic functions: arithmetic, unary ops, stack and register moves. The instruction pointer was t.g[0], indexing into the bytecode and incrementing after each step, with helper functions to read operands and write results back into the register file. Strings were stored separately and decoded with a bit-masking expression rather than stored in clear. Those identifiers (S, g, t.g) are artefacts of one obfuscated build and will not match the next one; the structure they describe is stable, the names are noise that rotates.
What runs inside that VM is the interesting question, and it is the question Kasada most wants to keep closed. From the outside, the bytecode program does two things. It collects a fingerprint, the canvas and WebGL outputs, hardware concurrency, the available browser APIs, timing behaviour, and the tells that betray automation. And it computes the proof-of-work. Both results get folded into the payload that becomes CT, and the per-request part becomes CD. Because the logic is bytecode, Kasada can change what it collects, in what order, with what weighting, by shipping new bytecode against the same interpreter, or by shipping a new interpreter too. The anti-instrumentation checks that look for CDP, Playwright, and patched runtimes ride inside the same VM, which is a topic in its own right; see Kasada’s anti-instrumentation for how those specific detections work. The VM is the delivery vehicle; the detections are the cargo.
This is the structural reason Kasada resists the tooling that works against script-based competitors. Against Akamai’s sensor_data payload you can, with effort, deobfuscate the script and reimplement the encoder. Against Kasada you first have to lift a whole instruction set out of an obfuscated interpreter, then keep doing it every time the build rotates. The work does not transfer cleanly between builds, which is exactly what V2 was advertised to do.
The proof-of-work, and why it is timed
The proof-of-work is the part that gives the tokens their cost. The VM runs a computation that takes real CPU time, the kind of repeated-hashing search that has no shortcut, and the result is the answer that goes into CD. A genuine browser pays that cost once per token without the user noticing; reported timings for the work sit in the tens to low hundreds of milliseconds depending on difficulty, with some practitioner notes citing a couple of milliseconds for the cheapest case. The server checks the answer in microseconds. That asymmetry, expensive to produce and cheap to verify, is the classic proof-of-work bargain, and it is what makes large-scale automation pay a per-request tax.
Kasada’s patent posture and public statements lean on this directly. The V2 platform was described as escalating the mathematical challenge to demand more resources from an attacker until continuing the attack stops being economic, and as requiring the work to be re-solved throughout a session rather than just at entry. The single solve is not the defence. The defence is that the solve recurs, the difficulty can climb for a session that looks automated, and the cost lands on the attacker’s hardware rather than Kasada’s. A solver that has cracked the algorithm still has to run it for real, at difficulty Kasada chooses, on every request.
The timing cuts the other way too, and this is the subtle part. The work is calibrated against how long a real browser takes. Solve it too fast, the way a native reimplementation running outside the browser’s overhead would, and the timing itself becomes a tell. The CD payload’s reported workTime and duration fields are not decoration; a proof-of-work that came back implausibly quick, or with timing that does not match the device it claims to be, is a signal in its own right. So the constraint is two-sided. The answer has to be correct, and the wall-clock cost of producing it has to look like a browser produced it. A correct answer delivered too cheaply is still suspicious.
Per-tenant rotation and what it costs
Two things rotate, and conflating them muddies the picture. The bytecode rotates, and the deployment is per-tenant. Both raise the cost of keeping a solver alive.
The script is polymorphic. Kasada does not serve the same challenge bytecode twice across deployment cycles, so a solver pinned to the identifiers, the opcode numbering, or the string layout of one build breaks when the build changes. The version pin in x-kpsdk-v (j-1.1.0 and the like) is the visible marker that you are looking at a particular SDK release. Because the logic is bytecode rather than JavaScript, a rotation can be a genuinely new instruction encoding, not just renamed variables, which is more disruptive to an attacker than the cosmetic churn a script-only obfuscator can manage.
The deployment is also tenant-scoped. The challenge path carries per-tenant prefixes, the SDK build a given site serves is its own, and the proof-of-work parameters and feature configuration arrive per deployment (the /mfc config fetch in some flows). So a solver tuned against one Kasada customer does not automatically carry to another. Each protected site is, in effect, a slightly different target, and the rotation clock runs independently. The practical consequence is a treadmill: anyone maintaining a bypass is not solving Kasada once, they are re-solving a moving build on a recurring basis, per tenant, indefinitely. That is the economic design working as intended, and it is the same logic, raise the attacker’s recurring cost, that the proof-of-work applies at the per-request level applied at the tooling level.
It is worth being honest about where this sits among peers. Kasada is one of several systems converging on the same playbook: an obfuscated client agent, a stateful token, server-side fingerprinting, and frequent rotation. The reese84 sensor from Imperva, covered in Imperva’s reese84 sensor, collects a comparable spread of signals through obfuscated JavaScript, and the DataDome JS tag does similar work behind its own token. What sets Kasada apart in this group is the commitment to virtualization as the obfuscation primitive and to proof-of-work as the cost lever, rather than relying on script obfuscation and server scoring alone.
What the design gets right, and where it leaks
The strong claim Kasada can make is that it raised the floor. The combination of a virtualized client, a timed proof-of-work, single-use per-request tokens bound to a server timestamp, and per-tenant rotation means the cheap automation that defeats a static script does not get off the ground. You cannot replay tokens, you cannot stockpile proofs, you cannot lift the logic out as readable JavaScript, and a solver you build today decays as the build rotates underneath it. Each of those is individually defeatable by a well-resourced attacker; together they push the cost of automation from trivial to ongoing-engineering-project, which for most adversaries is the whole game. The economic argument the company has made since 2015, that the way to beat bots is to make them uneconomic rather than to perfectly classify them, is honest about what it is doing.
The leaks are the usual ones for a client-side defence, and they are structural rather than incidental. The whole computation happens on hardware the attacker controls, so given enough effort the VM can be lifted, the proof-of-work reimplemented, and the timing forged to look browser-shaped. That the headers and endpoints are stable enough to be documented by third-party integrations at all tells you that the protocol surface, the /fp, /ips.js, and /tl shape and the x-kpsdk- header family, is observable and reusable even when the bytecode under it is not. The defence does not rest on secrecy of the protocol. It rests on the recurring cost of keeping a solver current against a rotating, virtualized, timed target. And the most concrete thing to say about that cost is that it is paid in calendar time: not the difficulty of one break, but the obligation to keep breaking, per tenant, every time the version pin ticks.
Sources & further reading
- nullpt.rs (2023), Devirtualizing Nike.com’s Bot Protection (Part 1) — a step-by-step devirtualization of a Kasada VM, naming the base62 bytecode decode, the
S(t)dispatcher, the ~60-handler array, and the register-file model. - OPCODES (2021), Reverse engineering Kasada javascript VM obfuscation, part 1 — early write-up describing Kasada’s processor array of opcode handlers and the renaming process used to recover meaning.
- Tim Blazytko / synthesis.to (2021), Writing Disassemblers for VM-based Obfuscators — the general theory of VM obfuscation: bytecode, dispatcher, fetch-decode-execute, virtual instruction pointer, and why devirtualization is hard.
- Hyper Solutions (2026), Kasada flow: the fingerprint endpoint — an integration’s view of the
/fp,/ips.js,/tl, and/mfcendpoints and thex-kpsdk-ct,-cd,-st,-v,-hheader family. - ChrisYP (2024), Kasada notes — reverse-engineering notes describing the CD JSON fields (
workTime,id,answers,duration,st) and an example per-tenant script path withx-kpsdk-v=j-1.1.0. - Help Net Security (2021), Kasada V2 platform provides defense against advanced bot attacks — the V2 launch: 15x more interrogation sensors, the custom-interpreter obfuscation, escalating cryptographic challenges, and Sam Crowther’s stated rationale.
- Kasada (2026), About us — company background: founded 2015 in Sydney by Sam Crowther, and the current product line including Bot Defense and AI Agent Trust.
- Scrapfly (2026), How to bypass Kasada anti-scraping when web scraping in 2026 — overview of Kasada’s detection layers (TLS/JA3, IP, HTTP, JavaScript fingerprinting) and the
X-Kpsdk-Ctheader as a block indicator. - Evomi (2025), Kasada, Shape, and the next generation of anti-bot — describes the proof-of-work-in-a-JS-payload model, per-deployment payload rotation, and the timing-based detection of solvers that run too fast.
- USPTO (2018), Automated agent detection utilizing non-CAPTCHA methods — patent in the non-CAPTCHA automated-agent-detection space relevant to client interrogation approaches.
Frequently asked questions
What is the difference between the x-kpsdk-ct and x-kpsdk-cd headers?
CT is the expensive, reusable session token minted once from the full fingerprint and proof-of-work the VM computes on first load, returned by the /tl endpoint and valid for the life of a session. CD is the cheap, single-use token regenerated for every request, carrying the per-request proof-of-work answer. Once the server has seen a given CD value it will not accept it again, so the asymmetry of one reusable certificate and one disposable proof is the core of the design.
Why is Kasada's challenge script so much harder to reverse than an ordinary obfuscated JavaScript payload?
Kasada compiles its client logic into bytecode for a custom virtual machine and ships the interpreter alongside it, so what you read in the script is the dispatcher loop and a table of small handlers, not the program itself. The fingerprinting and proof-of-work live as data the VM executes. Recovering the logic, called devirtualization, means reconstructing the instruction set, state model, and control flow at once, which is deliberately laborious and does not transfer cleanly when the build rotates.
How does the x-kpsdk-st server timestamp prevent replaying or precomputing Kasada tokens?
The st value is a server-supplied clock returned at token issuance, and each per-request CD computation is anchored to it rather than to the client's local clock. You cannot precompute a stockpile of CD answers because you do not know which server timestamp you will be asked to build on, and you cannot reuse an old one because CD is single-use and the freshness check has a tight window, described in practitioner notes as a few seconds. A signature header binds CT and CD so the pair must share one lineage.
Why can solving Kasada's proof-of-work too quickly get a request flagged?
The work is calibrated against how long a real browser takes, so producing the answer faster than a browser would, as a native reimplementation running outside browser overhead might, makes the timing itself a tell. The CD payload reportedly carries workTime and duration fields, and a proof that comes back implausibly quick or with timing that does not match the claimed device is a signal in its own right. The constraint is two-sided: the answer must be correct, and its wall-clock cost must look browser-shaped.
What makes maintaining a Kasada bypass an ongoing cost rather than a one-time effort?
Two things rotate. The challenge bytecode is polymorphic, so Kasada does not serve the same logic twice across deployment cycles, and because it is bytecode a rotation can be a genuinely new instruction encoding rather than renamed variables. The deployment is also tenant-scoped, with per-tenant script prefixes and per-deployment proof-of-work parameters, so a solver tuned to one customer does not carry to another. The result is a treadmill of re-solving a moving build, per tenant, every time the x-kpsdk-v version pin ticks.
Further reading
Inside the DataDome JS tag: what ddjskey and the client payload carry
A reference on DataDome's client-side JavaScript tag: the ddjskey site identifier, the signals the browser collector gathers and posts to api-js.datadome.co, and how the challenge and interstitial flow is wired.
·21 min readInside the Cloudflare challenge platform: anatomy of the cf-chl orchestration
A primary-source walk through the Cloudflare interstitial: the window._cf_chl_opt object, the /cdn-cgi/challenge-platform/h/ orchestration endpoints, the obfuscated client script, and how a cf_clearance pass is returned.
·18 min readDeobfuscating anti-bot JavaScript: the workflow for VM-based protections
How to read obfuscated anti-bot JavaScript without running it blind: beautify, scope and string-array recovery, Babel AST transforms, runtime hooking, and where the workflow hits a wall against bytecode VMs.
·20 min read