diff --git a/src/en/arvenscans/build.gradle b/src/en/arvenscans/build.gradle
index 98d02e4d0..a9b69b34e 100644
--- a/src/en/arvenscans/build.gradle
+++ b/src/en/arvenscans/build.gradle
@@ -1,9 +1,7 @@
 ext {
-    extName = 'Arven Scans'
-    extClass = '.ArvenScans'
-    themePkg = 'mangathemesia'
-    baseUrl = 'https://arvenscans.com'
-    overrideVersionCode = 0
+    extName = 'Vortex Scans'
+    extClass = '.VortexScans'
+    extVersionCode = 31
 }
 
 apply from: "$rootDir/common.gradle"
diff --git a/src/en/arvenscans/res/mipmap-hdpi/ic_launcher.png b/src/en/arvenscans/res/mipmap-hdpi/ic_launcher.png
index ba568b04b..ec16cf9ee 100644
Binary files a/src/en/arvenscans/res/mipmap-hdpi/ic_launcher.png and b/src/en/arvenscans/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/arvenscans/res/mipmap-mdpi/ic_launcher.png b/src/en/arvenscans/res/mipmap-mdpi/ic_launcher.png
index 25832490a..de4160824 100644
Binary files a/src/en/arvenscans/res/mipmap-mdpi/ic_launcher.png and b/src/en/arvenscans/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/arvenscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/arvenscans/res/mipmap-xhdpi/ic_launcher.png
index 74335d56c..b72de6414 100644
Binary files a/src/en/arvenscans/res/mipmap-xhdpi/ic_launcher.png and b/src/en/arvenscans/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/arvenscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/arvenscans/res/mipmap-xxhdpi/ic_launcher.png
index c6f496dee..5c054b81e 100644
Binary files a/src/en/arvenscans/res/mipmap-xxhdpi/ic_launcher.png and b/src/en/arvenscans/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/arvenscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/arvenscans/res/mipmap-xxxhdpi/ic_launcher.png
index a7c865dc8..6423ae29a 100644
Binary files a/src/en/arvenscans/res/mipmap-xxxhdpi/ic_launcher.png and b/src/en/arvenscans/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/ArvenScans.kt b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/ArvenScans.kt
deleted file mode 100644
index 3c3c5bcb5..000000000
--- a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/ArvenScans.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package eu.kanade.tachiyomi.extension.en.arvenscans
-
-import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
-import eu.kanade.tachiyomi.network.interceptor.rateLimit
-import okhttp3.OkHttpClient
-import java.util.concurrent.TimeUnit
-
-class ArvenScans : MangaThemesia("Arven Scans", "https://arvenscans.com", "en", "/series") {
-
-    override val client: OkHttpClient = super.client.newBuilder()
-        .rateLimit(20, 5, TimeUnit.SECONDS)
-        .build()
-}
diff --git a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/Dto.kt b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/Dto.kt
new file mode 100644
index 000000000..e08fc1192
--- /dev/null
+++ b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/Dto.kt
@@ -0,0 +1,113 @@
+package eu.kanade.tachiyomi.extension.en.arvenscans
+
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonPrimitive
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.jsoup.Jsoup
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@Serializable
+class SearchResponse(
+    val posts: List<Manga>,
+    val totalCount: Int,
+)
+
+@Serializable
+class Manga(
+    val id: Int,
+    val slug: String,
+    private val postTitle: String,
+    private val postContent: String? = null,
+    val isNovel: Boolean,
+    private val featuredImage: String? = null,
+    private val alternativeTitles: String? = null,
+    private val author: String? = null,
+    private val artist: String? = null,
+    private val seriesType: String? = null,
+    private val seriesStatus: String? = null,
+    private val genres: List<Name>? = emptyList(),
+) {
+    fun toSManga(baseUrl: String) = SManga.create().apply {
+        url = "$slug#$id"
+        title = postTitle
+        thumbnail_url = "$baseUrl/_next/image".toHttpUrl().newBuilder().apply {
+            addQueryParameter("url", featuredImage)
+            addQueryParameter("w", "828")
+            addQueryParameter("q", "75")
+        }.toString()
+        author = this@Manga.author?.takeUnless { it.isEmpty() }
+        artist = this@Manga.artist?.takeUnless { it.isEmpty() }
+        description = buildString {
+            postContent?.takeUnless { it.isEmpty() }?.let { desc ->
+                val tmpDesc = desc.replace("\n", "<br>")
+
+                append(Jsoup.parse(tmpDesc).text())
+            }
+            alternativeTitles?.takeUnless { it.isEmpty() }?.let { altName ->
+                append("\n\n")
+                append("Alternative Names: ")
+                append(altName)
+            }
+        }.trim()
+        genre = getGenres()
+        status = when (seriesStatus) {
+            "ONGOING", "COMING_SOON" -> SManga.ONGOING
+            "COMPLETED" -> SManga.COMPLETED
+            "CANCELLED", "DROPPED" -> SManga.CANCELLED
+            else -> SManga.UNKNOWN
+        }
+        initialized = true
+    }
+
+    fun getGenres() = buildList {
+        when (seriesType) {
+            "MANGA" -> add("Manga")
+            "MANHUA" -> add("Manhua")
+            "MANHWA" -> add("Manhwa")
+            else -> {}
+        }
+        genres?.forEach { add(it.name) }
+    }.distinct().joinToString()
+}
+
+@Serializable
+class Name(val name: String)
+
+@Serializable
+class Post<T>(val post: T)
+
+@Serializable
+class ChapterListResponse(
+    val isNovel: Boolean,
+    val slug: String,
+    val chapters: List<Chapter>,
+)
+
+@Serializable
+class Chapter(
+    private val id: Int,
+    private val slug: String,
+    private val number: JsonPrimitive,
+    private val createdBy: Name,
+    private val createdAt: String,
+    private val chapterStatus: String,
+) {
+    fun isPublic() = chapterStatus == "PUBLIC"
+
+    fun toSChapter(mangaSlug: String) = SChapter.create().apply {
+        url = "/series/$mangaSlug/$slug#$id"
+        name = "Chapter $number"
+        scanlator = createdBy.name
+        date_upload = try {
+            dateFormat.parse(createdAt)!!.time
+        } catch (_: ParseException) {
+            0L
+        }
+    }
+}
+
+private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
diff --git a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/Filters.kt b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/Filters.kt
new file mode 100644
index 000000000..e9eda2739
--- /dev/null
+++ b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/Filters.kt
@@ -0,0 +1,101 @@
+package eu.kanade.tachiyomi.extension.en.arvenscans
+
+import eu.kanade.tachiyomi.source.model.Filter
+import okhttp3.HttpUrl
+
+interface UrlPartFilter {
+    fun addUrlParameter(url: HttpUrl.Builder)
+}
+
+abstract class SelectFilter(
+    name: String,
+    private val urlParameter: String,
+    private val options: List<Pair<String, String>>,
+) : UrlPartFilter, Filter.Select<String>(
+    name,
+    options.map { it.first }.toTypedArray(),
+) {
+    override fun addUrlParameter(url: HttpUrl.Builder) {
+        url.addQueryParameter(urlParameter, options[state].second)
+    }
+}
+
+class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
+
+abstract class CheckBoxGroup(
+    name: String,
+    private val urlParameter: String,
+    options: List<Pair<String, String>>,
+) : UrlPartFilter, Filter.Group<CheckBoxFilter>(
+    name,
+    options.map { CheckBoxFilter(it.first, it.second) },
+) {
+    override fun addUrlParameter(url: HttpUrl.Builder) {
+        val checked = state.filter { it.state }.map { it.value }
+
+        if (checked.isNotEmpty()) {
+            url.addQueryParameter(urlParameter, checked.joinToString(","))
+        }
+    }
+}
+
+class StatusFilter : SelectFilter(
+    "Status",
+    "seriesStatus",
+    listOf(
+        Pair("", ""),
+        Pair("Ongoing", "ONGOING"),
+        Pair("Completed", "COMPLETED"),
+        Pair("Cancelled", "CANCELLED"),
+        Pair("Dropped", "DROPPED"),
+        Pair("Mass Released", "MASS_RELEASED"),
+        Pair("Coming Soon", "COMING_SOON"),
+    ),
+)
+
+class TypeFilter : SelectFilter(
+    "Type",
+    "seriesType",
+    listOf(
+        Pair("", ""),
+        Pair("Manga", "MANGA"),
+        Pair("Manhua", "MANHUA"),
+        Pair("Manhwa", "MANHWA"),
+        Pair("Russian", "RUSSIAN"),
+    ),
+)
+
+class GenreFilter : CheckBoxGroup(
+    "Genres",
+    "genreIds",
+    listOf(
+        Pair("Action", "1"),
+        Pair("Adventure", "13"),
+        Pair("Comedy", "7"),
+        Pair("Drama", "2"),
+        Pair("elf", "25"),
+        Pair("Fantas", "28"),
+        Pair("Fantasy", "8"),
+        Pair("Historical", "19"),
+        Pair("Horror", "9"),
+        Pair("Josei", "21"),
+        Pair("Manhwa", "5"),
+        Pair("Martial Arts", "6"),
+        Pair("Mature", "12"),
+        Pair("Monsters", "14"),
+        Pair("Reincarnation", "16"),
+        Pair("Revenge", "17"),
+        Pair("Romance", "20"),
+        Pair("School Life", "23"),
+        Pair("Seinen", "10"),
+        Pair("shojo", "26"),
+        Pair("Shoujo", "22"),
+        Pair("Shounen", "3"),
+        Pair("Slice Of Life", "18"),
+        Pair("Sports", "4"),
+        Pair("Supernatural", "11"),
+        Pair("System", "15"),
+        Pair("terror", "24"),
+        Pair("Video Games", "27"),
+    ),
+)
diff --git a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt
new file mode 100644
index 000000000..5ca02669c
--- /dev/null
+++ b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt
@@ -0,0 +1,145 @@
+package eu.kanade.tachiyomi.extension.en.arvenscans
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.injectLazy
+
+class VortexScans : HttpSource() {
+
+    override val name = "Vortex Scans"
+
+    override val baseUrl = "https://vortexscans.com"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    override val client = network.cloudflareClient
+
+    private val json by injectLazy<Json>()
+
+    override fun headersBuilder() = super.headersBuilder()
+        .set("Referer", "$baseUrl/")
+
+    private val titleCache by lazy {
+        val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
+        val data = response.parseAs<SearchResponse>()
+
+        data.posts
+            .filterNot { it.isNovel }
+            .associateBy { it.slug }
+    }
+
+    override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
+
+    override fun popularMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+        val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
+            .map { it.absUrl("href").substringAfterLast("/series/") }
+
+        val entries = slugs.mapNotNull {
+            titleCache[it]?.toSManga(baseUrl)
+        }
+
+        return MangasPage(entries, false)
+    }
+
+    override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
+    override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
+            addQueryParameter("page", page.toString())
+            addQueryParameter("perPage", perPage.toString())
+            addQueryParameter("searchTerm", query.trim())
+            filters.filterIsInstance<UrlPartFilter>().forEach {
+                it.addUrlParameter(this)
+            }
+        }.build()
+
+        return GET(url, headers)
+    }
+
+    override fun searchMangaParse(response: Response): MangasPage {
+        val data = response.parseAs<SearchResponse>()
+        val page = response.request.url.queryParameter("page")!!.toInt()
+
+        val entries = data.posts
+            .filterNot { it.isNovel }
+            .map { it.toSManga(baseUrl) }
+
+        val hasNextPage = data.totalCount > (page * perPage)
+
+        return MangasPage(entries, hasNextPage)
+    }
+
+    override fun getFilterList() = FilterList(
+        StatusFilter(),
+        TypeFilter(),
+        GenreFilter(),
+    )
+
+    override fun mangaDetailsRequest(manga: SManga): Request {
+        val id = manga.url.substringAfterLast("#")
+        val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
+
+        return GET(url, headers)
+    }
+
+    override fun getMangaUrl(manga: SManga): String {
+        val slug = manga.url.substringBeforeLast("#")
+
+        return "$baseUrl/series/$slug"
+    }
+
+    override fun mangaDetailsParse(response: Response): SManga {
+        val data = response.parseAs<Post<Manga>>()
+
+        assert(!data.post.isNovel) { "Novels are unsupported" }
+
+        // genres are only returned in search call
+        // and not when fetching details
+        return data.post.toSManga(baseUrl).apply {
+            genre = titleCache[data.post.slug]?.getGenres()
+        }
+    }
+
+    override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
+
+    override fun chapterListParse(response: Response): List<SChapter> {
+        val data = response.parseAs<Post<ChapterListResponse>>()
+
+        assert(!data.post.isNovel) { "Novels are unsupported" }
+
+        return data.post.chapters
+            .filter { it.isPublic() }
+            .map { it.toSChapter(data.post.slug) }
+    }
+
+    override fun pageListParse(response: Response): List<Page> {
+        val document = response.asJsoup()
+
+        return document.select("main > section > img").mapIndexed { idx, img ->
+            Page(idx, imageUrl = img.absUrl("src"))
+        }
+    }
+
+    override fun imageUrlParse(response: Response) =
+        throw UnsupportedOperationException()
+
+    private inline fun <reified T> Response.parseAs(): T =
+        json.decodeFromString(body.string())
+}
+
+private const val perPage = 18