diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b01d9c4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,347 @@
+# Async
+
+Coroutines for Hytale's per-world ECS. Replaces the noisy
+`CompletableFuture.runAsync { world.execute { store.getComponent(…) } }`
+pattern with one suspending call.
+
+
+
+
+
+
+
+
+
+
+---
+
+## The pain it solves
+
+Each Hytale world runs on its own thread. Touch a component from anywhere
+else and you get `IllegalStateException: Assert not in thread!`. Touch the
+database from the world thread and you freeze every player connected to it.
+Plugin code lives wedged between those two failure modes, and the workaround
+is the same boilerplate everywhere:
+
+```java
+CompletableFuture.runAsync(() -> {
+ PlayerData data = database.load(uuid);
+ world.execute(() -> {
+ Ref ref = player.getRef();
+ Store store = ref.getStore();
+ PlayerStats stats = store.getComponent(ref, PlayerStats.getComponentType());
+ stats.setLevel(data.level);
+ });
+}).exceptionally(t -> { logger.error("load failed", t); return null; });
+```
+
+Async collapses both halves into one suspending function:
+
+```kotlin
+playerScope(player).launch {
+ val data = withContext(AsyncDispatchers.HytaleIO) { database.load(uuid) }
+ modify(player.handle()) { level = data.level }
+}
+```
+
+Same thread choreography. Cancellation on disconnect comes for free.
+
+---
+
+## Setup
+
+```kotlin
+class MyPlugin(init: JavaPluginInit) : JavaPlugin(init) {
+ override fun start() {
+ installAsync()
+ // Register each component type once, with whatever ComponentType
+ // your EntityStore.REGISTRY.register(...) call returned at SDK setup time.
+ ComponentRegistry.register(yourPlayerStatsComponentType)
+ }
+
+ override fun shutdown() {
+ Async.shutdown()
+ }
+}
+```
+
+Two lines in `start()`, one in `shutdown()`. From there, `playerScope(player)`,
+`worldScope(world)`, `pluginScope(this)` are all fair game from any thread.
+
+---
+
+## API by module
+
+### `:core` — dispatchers and scopes
+
+```kotlin
+AsyncDispatchers.World(world) // world's main thread
+AsyncDispatchers.HytaleIO // bounded pool for blocking I/O
+AsyncDispatchers.HytaleScheduled // backs delay() and withTimeout()
+
+PlayerScopes.of(uuid)
+PlayerScopes.cancel(uuid)
+PlayerScopes.cancelAll()
+
+WorldScopes.of(worldUuid) // same shape
+PluginScopes.of(plugin) // identity-keyed
+
+Async.shutdown() // cancels everything; idempotent
+```
+
+Sealed `AsyncException` hierarchy: `WorldClosedException`,
+`NoWorldInContextException`, `ComponentNotFoundException`,
+`ComponentTypeNotRegisteredException`.
+
+### `:ecs` — component DSL
+
+```kotlin
+// Register each component type once, with the ComponentType your EntityStore
+// registered at SDK setup time:
+ComponentRegistry.register(yourPlayerStatsComponentType)
+
+read(entity) { … } // throws if component missing
+readOrNull(entity) { … } // returns null instead
+
+modify(entity) { … } // Unit
+modify(entity) { … } // returns the block's value
+```
+
+Every entry switches to the entity's world dispatcher, runs your block on
+the world thread, returns to the caller's dispatcher. Mutations on the
+component persist in place — there's no `setComponent` and no rollback (see
+the threading section).
+
+### `:binding` — Hytale glue
+
+The only module that imports anything from the Hytale SDK.
+
+```kotlin
+World.asExecutor(): WorldExecutor
+
+Ref.toEntityHandle(world: World): EntityHandle
+PlayerRef.toEntityHandle(): EntityHandle
+Player.handle(): EntityHandle
+
+JavaPlugin.installAsync() // wires PlayerDisconnectEvent → PlayerScopes.cancel
+
+playerScope(player): CoroutineScope
+worldScope(world): CoroutineScope
+pluginScope(plugin): CoroutineScope
+```
+
+### `:dist` — what you ship
+
+No code. Bundles `:core + :ecs + :binding` into a single shaded JAR via
+`com.gradleup.shadow`. The only artifact a consumer ever drops in `mods/`.
+
+---
+
+## Three real patterns
+
+**Load on join.** `PlayerReadyEvent` → fetch off-thread → mutate on world thread.
+
+```kotlin
+eventRegistry.registerGlobal(PlayerReadyEvent::class.java) { event ->
+ val player = event.player
+ playerScope(player).launch {
+ val data = withContext(AsyncDispatchers.HytaleIO) { loadFromDisk(player.uuid) }
+ modify(player.handle()) {
+ level = data.level
+ clan = data.clan
+ }
+ }
+}
+```
+
+**Async chat moderation.** `IAsyncEvent` bridge via `pluginScope.future { … }`,
+HTTP off-thread, mutate-and-cancel inline.
+
+```kotlin
+eventRegistry.registerAsyncGlobal(PlayerChatEvent::class.java) { future ->
+ future.thenCompose { event ->
+ pluginScope(this).future {
+ val flagged = withContext(AsyncDispatchers.HytaleIO) {
+ moderationApi.check(event.content)
+ }
+ if (flagged) {
+ modify(event.sender.toEntityHandle()) { warnings += 1 }
+ event.isCancelled = true
+ }
+ event
+ }
+ }
+}
+```
+
+**Periodic leaderboard.** Plugin-scoped loop, parallel reads across players.
+
+```kotlin
+pluginScope(this).launch {
+ while (isActive) {
+ delay(60.seconds)
+ val top = onlinePlayers().map { p ->
+ async { p to read(p.handle()) { level } }
+ }.awaitAll().sortedByDescending { it.second }.take(5)
+ broadcast(top)
+ }
+}
+```
+
+Each `read` switches to its player's world dispatcher independently — reads
+on different worlds happen in parallel, reads on the same world serialize
+naturally. Fully runnable versions in [`examples/`](examples/).
+
+---
+
+## Modules
+
+| Module | Hytale SDK dep | Purpose |
+|---|---|---|
+| `core` | none | Dispatchers, scopes, exceptions. Testable without a server. |
+| `ecs` | none | The `read` / `readOrNull` / `modify` DSL. |
+| `binding` | `compileOnly` | Hytale-specific glue. The only module that imports `com.hypixel.*`. |
+| `dist` | aggregator | Single shaded JAR. |
+
+The split exists so `:core` and `:ecs` stay testable against in-memory stubs
+in milliseconds, and so a future Hytale API change only ripples through
+`:binding`. **For deployment, always ship `:dist`** — the per-module split
+is for code hygiene, not cherry-picking.
+
+---
+
+## Threading model, briefly
+
+Three dispatchers map to three concerns:
+
+| Dispatcher | Backed by | Use for |
+|---|---|---|
+| `AsyncDispatchers.World(world)` | `world.execute(Runnable)` | component reads, writes |
+| `AsyncDispatchers.HytaleIO` | bounded pool, `Runtime.availableProcessors() × 2` | blocking I/O — DB, HTTP, file |
+| `AsyncDispatchers.HytaleScheduled` | `ScheduledExecutorService` | backs `delay()` and `withTimeout()` |
+
+**Mutate in place.** Hytale's `Store` exposes no public
+`setComponent(ref, value)`. The component returned by `getComponent` is the
+live in-store instance, and mutating it is how you persist. There's no
+copy-on-read and no rollback if a `modify` block throws halfway through.
+Validate before mutating.
+
+**Cancellation.** Coroutines on a player/world/plugin scope are cancelled
+atomically when the scope is. `Dispatchers.World` honors cancellation
+*before* dispatch — a job cancelled while still in the queue is dropped.
+Once a runnable starts on the world thread, it runs to completion. The
+world is the single writer; ripping a thread mid-mutation would leave the
+store inconsistent.
+
+KDoc on every public symbol carries one of three plain-text tags:
+`@ThreadSafe`, `@WorldThreadOnly`, `@AnyThread`. Conventions, not
+annotations — zero runtime cost.
+
+---
+
+## Coexistence with Kytale
+
+[Kytale](https://github.com/briarss/Kytale) ships a full Kotlin framework
+for Hytale plugins (`KotlinPlugin` base class, Event / Command / Config DSLs).
+Async solves a narrower problem — thread-safe ECS access — and works
+alongside Kytale or standalone. If you're already on Kytale, drop Async in
+just for the component DSL and keep the rest of your stack.
+
+---
+
+## Installing
+
+```bash
+./gradlew :dist:shadowJar
+cp dist/build/libs/async-*.jar /Server/mods/
+# Restart the server
+```
+
+Requires JDK 25, Gradle 9.4+, Hytale Server `2026.03.26-89796e57b` or newer.
+
+---
+
+## Building from source
+
+```bash
+./gradlew build # all modules + tests
+./gradlew :dist:shadowJar # the shaded jar
+```
+
+Tests for `:core` and `:ecs` run against in-memory stubs and don't need a
+Hytale server.
+
+Layout for contributors:
+
+- New dispatcher → `core/dispatchers/AsyncDispatchers.kt`
+- New scope kind → mirror `PlayerScopes` in `core/scope/`
+- New DSL primitive → `ecs/ComponentDsl.kt`. Stay suspending and dispatcher-aware.
+- New SDK adapter → `binding/`. Don't import `com.hypixel.*` from `:core` or `:ecs`.
+
+---
+
+## How a call flows
+
+```
+ (any thread)
+ │
+ ▼
+ playerScope(player).launch
+ │
+ │ suspend
+ ▼
+ withContext(HytaleIO) ──── blocking I/O, off-thread
+ │
+ │ suspend
+ ▼
+ modify(player.handle()) ──── switches to world thread
+ │
+ ▼
+ live component mutation
+ │
+ ▼
+ (returns to caller's dispatcher)
+```
+
+---
+
+## Status and known limits
+
+v0.1 ships dispatchers, the three scope registries, the suspending DSL, the
+Hytale binding, the shaded JAR, and four example sketches.
+
+Things to know:
+
+- **No public `WorldUnloadEvent`** in the current SDK. Cancel
+ `WorldScopes.cancel(uuid)` manually from your world-management code until
+ it ships.
+- **`PlayerDisconnectEvent` fires twice** on world unload. `PlayerScopes.cancel`
+ is idempotent so this is benign — just be aware if you wire your own listener.
+- **World-death race**: a small window exists between `world.isAlive()` and
+ `world.execute()` where a task can be silently dropped. Wrap long
+ world-thread work in `withTimeout(...)` if it matters.
+
+---
+
+## Stack
+
+- Kotlin 2.2.20, target JVM 24, toolchain JDK 25 (will move to JVM 25 once
+ Kotlin 2.3 ships).
+- kotlinx-coroutines 1.8 (core + jdk8 bridge).
+- Gradle 9.4 with version catalog and Kotlin DSL.
+- JUnit 5 + Kotest assertions + MockK for tests.
+- `com.gradleup.shadow` 9.3 for the fat JAR.
+- Hytale Plugin API (`https://maven.hytale.com/release`), `compileOnly` only
+ in `:binding`.
+
+---
+
+## Credits
+
+By [Mythlane](https://mythlane.com). Module layout influenced by [Kytale](https://github.com/briarss/Kytale).
+
+---
+
+## License
+
+MIT — see [LICENSE](LICENSE). Free to fork, modify, and use commercially.