A one-tap game store for the PS Vita. Browse PSP and PS Vita games with real cover art, download them over Wi-Fi, and install them straight to the console — no PC, no cables, no wall of plain-text links.
PEPEstore is a client. It hosts nothing. No games, no catalogs, no cover art live in this app or on this repo. Everything comes from third-party "stores" maintained by the community — game catalogs hosted on people's own GitHub repos. This project is the reader; the sources belong to whoever makes them. Only download games you legally own.
Built by Shalva Efremashvili — because PKGj gave me a wall of text with no covers, and half my games weren't even on it. So I built this. No ads, no paywalls, no BS.
Telegram: @PEPESTOREVITA — find stores, request games, report bugs.
PEPEstore was 100% vibe-coded. Every line (the Lua, the C module, the store system, this README) was written with AI, A to Z. No apologies, no asterisks.
If that fact makes your eye twitch: cool, the app still works, the covers still load, and your Vita doesn't care who typed the semicolons. 🐸
And here's the part that'll really annoy the right people: AI contributions are not just allowed, they're encouraged. Open a PR written by ChatGPT, Claude, Copilot, whatever, and I'll review it on whether it works, not on which meat-or-silicon wrote it. Humans welcome too, obviously. Ship good code; that's the only rule.
"But a real dev would..." a real dev did: they pointed the tools and shipped the thing you're reading about. Results > gatekeeping.
- Grab
PEPEstore.vpkfrom the Releases page. - Install it with VitaShell (or your bubble installer of choice).
- Launch it — it starts empty ("No stores yet"). You add a store to get games (below).
Requirements: a hacked Vita (HENkaku/enso). To launch downloaded PS Vita games you also need the NoNpDrm kernel plugin. PSP games run through Adrenaline.
The app ships with nothing. To get games you add a store, which is just a public GitHub repo:
- Press START → Stores → Add a store.
- Type the store's
username/reponame(e.g.shali1995/psp-webroms) and confirm. - It downloads the store, and its games appear in the browse list (filter by Store).
Update / remove a store from the same Stores screen. Updates only re-download what changed.
Where do you find stores? In the community — join the Telegram: @PEPESTOREVITA. Anyone can publish one (see below), so the list grows and broken sources get fixed by the people who use them.
When you add a store by username/reponame, the app fetches it from
raw.githubusercontent.com/<user>/<repo>/main/ and merges its games in. Browsing and covers are
fully offline — only the moment you press Download touches the network, through that store's
resolver. Nothing about any specific site is baked into the app.
A store is a public GitHub repo with five files at its root. Build them however you like
(any language) — you're just producing these files, then git push.
store.json manifest — what the app reads first
catalog.lua the game database
cov_index.lua where each cover sits inside covers.pak
covers.pak all the cover thumbnails in one file
resolver.lua turns a game into a real download URL (the only "live" code)
{
"id": "my-psp-store",
"name": "My PSP Store",
"author": "yourname",
"platform": "psp",
"repo": "yourname/my-psp-store",
"store_version": 1,
"files": [
{ "name": "catalog.lua", "version": 1, "size": 1234, "sha256": "..." },
{ "name": "cov_index.lua", "version": 1, "size": 1234, "sha256": "..." },
{ "name": "covers.pak", "version": 1, "size": 1234, "sha256": "..." },
{ "name": "resolver.lua", "version": 1, "size": 1234, "sha256": "..." }
]
}platform—"psp"or"psvita"(decides the install path).repo— youruser/repo, so in-app Update knows where to check.- Versioning: every file has a
version. To ship an update, change a file and bump itsversion(andstore_version). The app re-downloads only the files whose version changed — tweakresolver.lua, bump it, and users pull ~2 KB instead of the whole cover pack.
A Lua file returning an array of games. A game's position in the array is its cover index.
Only display_name, console, and a way to download are required; everything else just enriches
the detail page.
return {
{ id="final-fantasy", display_name="Final Fantasy", console="psp",
cover_url="https://…", -- online fallback if no packed cover
downloads=69200, size="1.04 G", rating=3.2, genre={"Role-Playing"},
publisher={"Square Enix"}, region="Japan", released="June 26, 2007",
description="A horrible cloak blankets the planet…",
variants={ -- the rows shown in the download picker
{ idx=1, filename="Final Fantasy (Japan).zip", size="1.04 G",
container="zip", region="Japan", title_id=nil, version=nil, languages={},
url="https://…" }, -- what the resolver receives as `variant`
-- …one per region / version
} },
-- …one table per game
}console—"psp"→ installed toux0:pspemu(ISO/EBOOT);"psvita"→ installed as a bubble.variants[]— each is a download option.containeris the file type (zip,7z, …);drm(e.g."NoNpDrm") shows as the chip in the picker;urlis whatever your resolver needs.- Game #1 in the array uses
cov_index[1], #2 usescov_index[2], etc.
covers.pak is every cover JPEG concatenated into one file (use 200×320 to match the app).
cov_index.lua maps each game's array position → {byteOffset, length} into that file:
return { [1]={0,31500}, [2]={31500,27961}, [3]={59461,18030}, … }Build it by appending each JPEG to the pak and recording where it landed. Missing covers just fall
back to cover_url (or a placeholder).
The heart of a store. The app calls it when the user hits Download:
-- ctx.get(url, referer) -> page body (or nil) · ctx.urlenc(s) -> url-encoded string
return {
name = "My PSP Store",
resolve = function(ctx, game, variant) -- variant = the picked row (or nil for single-file)
-- return url, referer (or nil, "reason")
end,
}Two common styles:
-- A) PRE-RESOLVED: the direct URL is already in the catalog, just hand it back (no network)
resolve = function(ctx, game, variant)
local u = (variant and variant.url) or game.download_url
if u and u ~= "" then return u, "https://the-site.example/" end
return nil, "no url"
end-- B) ON-DEVICE: the catalog stores a listing-page URL; fetch it and scrape the real link live
resolve = function(ctx, game, variant)
local html = ctx.get(variant.url, "https://the-site.example/")
if not html then return nil, "page fetch failed" end
local link = html:match('href="(https?://[^"]+%.zip)"')
if not link then return nil, "no link found" end
return ctx.urlenc(link), variant.url -- 2nd value = Referer for the download
endPush the five files to a public GitHub repo. Users add it as yourname/reponame. Bump versions
to ship updates.
The Vita runs plain HTTP + Lua string-matching only — no JavaScript engine, no Cloudflare
bypass. If a site builds its download link with JavaScript, or sits behind a Cloudflare "checking
your browser" / CAPTCHA wall, it cannot be resolved on-device. Pick sites where the link is in
the HTML (or one listing/redirect hop away), or pre-resolve the URLs into catalog.lua when you
build it. (A store author can front a tricky site with their own server-side resolver — that's on
them, not the client.)
Supports: PSP ISO / CSO / EBOOT.PBP (PSN & Minis) · PS Vita NoNpDrm .7z dumps ·
.7z (streamed, constant-RAM) / .zip / .rar · any source reachable by plain HTTP.
Doesn't: JavaScript-gated or Cloudflare-protected sites · Vitamin retail VPKs (retail-locked,
no license — the installer refuses them to avoid a crash) · PSN .pkg (encrypted, not implemented).
See BUILD.md. Engine is lpp-vita; PEPEstore is the Lua app + assets in
vpk/ (ready-to-package: index.lua, font, icons, loading screen, sounds, sce_sys) plus a
native module in module/ (libcurl downloads, libarchive extraction, scePromoterUtility
install). Build the engine+module, drop the compiled eboot.bin into vpk/, zip it → done. The app
bundles no catalog/covers — the store model provides those.
lpp-vita (Rinnegatamante) · libcurl · libarchive · liblzma · VitaShell (TheFloW) · pkgj · NOBORU (Creckeryop) · ONElua.
GPL-3.0 — copy, modify and redistribute freely, but derivatives must stay open source under the same license. See LICENSE.



