diff --git a/python_payload/apps/gr33nhouse/__init__.py b/python_payload/apps/gr33nhouse/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..0dfc5157fe77ead098f5ceb6bc61d9c3e3a1440f
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/__init__.py
@@ -0,0 +1,82 @@
+from st3m.application import Application, ApplicationContext
+from st3m.input import InputController, InputState
+from st3m.ui import colours
+from st3m.ui.view import ViewManager
+from ctx import Context
+from .applist import AppList
+from .background import Flow3rView
+from .record import RecordView
+from .manual import ManualInputView
+
+
+class Gr33nhouseApp(Application):
+    items = ["Browse apps", "Record App Seed", "Enter App Seed"]
+    selection = 0
+
+    input: InputController
+    background: Flow3rView
+
+    def __init__(self, app_ctx: ApplicationContext) -> None:
+        super().__init__(app_ctx=app_ctx)
+
+        self.input = InputController()
+        self.background = Flow3rView()
+
+    def on_enter(self, vm: ViewManager | None) -> None:
+        super().on_enter(vm)
+
+        if self.vm is None:
+            raise RuntimeError("vm is None")
+
+    def draw(self, ctx: Context) -> None:
+        self.background.draw(ctx)
+        ctx.save()
+
+        ctx.gray(1.0)
+        ctx.rectangle(
+            -120.0,
+            -15.0,
+            240.0,
+            30.0,
+        ).fill()
+
+        ctx.translate(0, -30 * self.selection)
+
+        offset = 0
+
+        ctx.font = "Camp Font 3"
+        ctx.font_size = 24
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+
+        for idx, item in enumerate(self.items):
+            if idx == self.selection:
+                ctx.gray(0.0)
+            else:
+                ctx.gray(1.0)
+
+            ctx.move_to(0, offset)
+            ctx.text(item)
+            offset += 30
+
+        ctx.restore()
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        self.background.think(ins, delta_ms)
+        self.input.think(ins, delta_ms)
+
+        if self.input.buttons.app.left.pressed:
+            if self.selection > 0:
+                self.selection -= 1
+
+        elif self.input.buttons.app.right.pressed:
+            if self.selection < len(self.items) - 1:
+                self.selection += 1
+
+        elif self.input.buttons.app.middle.pressed:
+            if self.selection == 0:
+                self.vm.push(AppList())
+            elif self.selection == 1:
+                self.vm.push(RecordView())
+            elif self.selection == 2:
+                self.vm.push(ManualInputView())
diff --git a/python_payload/apps/gr33nhouse/applist.py b/python_payload/apps/gr33nhouse/applist.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1514ef15215682f1b9422ddebf538ebba3a719d
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/applist.py
@@ -0,0 +1,169 @@
+from st3m.goose import Optional, Enum
+from st3m.input import InputController, InputState
+from st3m.ui import colours
+from st3m.ui.view import BaseView, ViewManager
+from ctx import Context
+import urequests
+import time
+from .background import Flow3rView
+from .confirmation import ConfirmationView
+
+
+class ViewState(Enum):
+    INITIAL = 1
+    LOADING = 2
+    ERROR = 3
+    LOADED = 4
+
+
+class AppList(BaseView):
+    initial_ticks: int = 0
+    _state: ViewState = ViewState.INITIAL
+
+    apps: list[any] = []
+    selection: int = 0
+
+    input: InputController
+    background: Flow3rView
+
+    def __init__(self) -> None:
+        self.input = InputController()
+        self.vm = None
+        self.background = Flow3rView()
+
+    def on_enter(self, vm: Optional[ViewManager]) -> None:
+        self.vm = vm
+        self.initial_ticks = time.ticks_ms()
+
+    def draw(self, ctx: Context) -> None:
+        ctx.move_to(0, 0)
+
+        if self._state == ViewState.INITIAL or self._state == ViewState.LOADING:
+            ctx.rgb(*colours.BLACK)
+            ctx.rectangle(
+                -120.0,
+                -120.0,
+                240.0,
+                240.0,
+            ).fill()
+
+            ctx.save()
+            ctx.rgb(*colours.WHITE)
+            ctx.font = "Camp Font 3"
+            ctx.font_size = 24
+            ctx.text_align = ctx.CENTER
+            ctx.text_baseline = ctx.MIDDLE
+            ctx.text("Collecting seeds...")
+            ctx.restore()
+            return
+
+        elif self._state == ViewState.ERROR:
+            ctx.rgb(*colours.BLACK)
+            ctx.rectangle(
+                -120.0,
+                -120.0,
+                240.0,
+                240.0,
+            ).fill()
+
+            ctx.save()
+            ctx.rgb(*colours.WHITE)
+            ctx.gray(1.0)
+            ctx.font = "Camp Font 3"
+            ctx.font_size = 24
+            ctx.text_align = ctx.CENTER
+            ctx.text_baseline = ctx.MIDDLE
+            ctx.text("Something went wrong")
+            ctx.restore()
+            return
+
+        elif self._state == ViewState.LOADED:
+            self.background.draw(ctx)
+
+            ctx.save()
+            ctx.gray(1.0)
+            ctx.rectangle(
+                -120.0,
+                -15.0,
+                240.0,
+                30.0,
+            ).fill()
+
+            ctx.translate(0, -30 * self.selection)
+
+            offset = 0
+
+            ctx.font = "Camp Font 3"
+            ctx.font_size = 24
+            ctx.text_align = ctx.CENTER
+            ctx.text_baseline = ctx.MIDDLE
+
+            ctx.move_to(0, 0)
+            for idx, app in enumerate(self.apps):
+                if idx == self.selection:
+                    ctx.gray(0.0)
+                else:
+                    ctx.gray(1.0)
+
+                ctx.move_to(0, offset)
+                ctx.text(app["name"])
+                offset += 30
+
+            ctx.restore()
+        else:
+            raise RuntimeError(f"Invalid view state {self._state}")
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        if self.initial_ticks == 0 or time.ticks_ms() < self.initial_ticks + 300:
+            return
+
+        self.input.think(ins, delta_ms)
+
+        if self._state == ViewState.INITIAL:
+            try:
+                self._state = ViewState.LOADING
+                print("Loading app list...")
+                res = urequests.get("https://flow3r.garden/api/apps.json")
+                self.apps = res.json()["apps"]
+
+                if self.apps == None:
+                    print(f"Invalid JSON or no apps: {res.json()}")
+                    self._state = ViewState.ERROR
+                    return
+
+                self._state = ViewState.LOADED
+                print("App list loaded")
+            except Exception as e:
+                print(f"Load failed: {e}")
+                self._state = ViewState.ERROR
+            return
+        elif self._state == ViewState.LOADING:
+            raise RuntimeError(f"Invalid view state {self._state}")
+        elif self._state == ViewState.ERROR:
+            return
+
+        self.background.think(ins, delta_ms)
+
+        if self.input.buttons.app.left.pressed:
+            if self.selection > 0:
+                self.selection -= 1
+
+        elif self.input.buttons.app.right.pressed:
+            if self.selection < len(self.apps) - 1:
+                self.selection += 1
+
+        elif self.input.buttons.app.middle.pressed:
+            print(f"state {self._state}")
+            print(f">> {self.apps[self.selection]}")
+            app = self.apps[self.selection]
+            url = app["tarDownloadUrl"]
+            name = app["name"]
+            author = app["author"]
+            # self.vm.push(DownloadView(url))
+            self.vm.push(
+                ConfirmationView(
+                    url=url,
+                    name=name,
+                    author=author,
+                )
+            )
diff --git a/python_payload/apps/gr33nhouse/background.py b/python_payload/apps/gr33nhouse/background.py
new file mode 100644
index 0000000000000000000000000000000000000000..516f33be7d21c4547e3f92424c5f328a4ff13707
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/background.py
@@ -0,0 +1,109 @@
+import random
+from st3m.input import InputController, InputState
+
+from st3m.ui.view import BaseView
+from ctx import Context
+
+
+class Flow3rView(BaseView):
+    input: InputController
+
+    def __init__(self) -> None:
+        self.vm = None
+        self.input = InputController()
+
+        self.flowers = []
+        for i in range(8):
+            self.flowers.append(
+                Flower(
+                    ((random.getrandbits(16) - 32767) / 32767.0) * 200,
+                    ((random.getrandbits(16)) / 65535.0) * 240 - 120,
+                    ((random.getrandbits(16)) / 65535.0) * 400 + 25,
+                )
+            )
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        super().think(ins, delta_ms)
+        for c in self.flowers:
+            c.y += (10 * delta_ms / 1000.0) * 200 / c.z
+            if c.y > 300:
+                c.y = -300
+            c.rot += delta_ms * c.rot_speed
+        self.flowers = sorted(self.flowers, key=lambda c: -c.z)
+
+    def draw(self, ctx: Context) -> None:
+        ctx.save()
+        ctx.rectangle(-120, -120, 240, 240)
+        ctx.rgb(0.1, 0.4, 0.3)
+        ctx.fill()
+
+        for f in self.flowers:
+            f.draw(ctx)
+        ctx.restore()
+
+
+class Flower:
+    def __init__(self, x: float, y: float, z: float) -> None:
+        self.x = x
+        self.y = y
+        self.z = z
+        self.rot = 0
+        self.rot_speed = (((random.getrandbits(16) - 32767) / 32767.0) - 0.5) / 800
+
+    def draw(self, ctx: Context):
+        ctx.save()
+        ctx.translate(-78 + self.x, -70 + self.y)
+        ctx.translate(50, 40)
+
+        ctx.rotate(self.rot)
+        ctx.translate(-50, -40)
+        ctx.scale(100 / self.z, 100.0 / self.z)
+        ctx.move_to(76.221727, 3.9788409).curve_to(
+            94.027758, 31.627675, 91.038918, 37.561293, 94.653428, 48.340473
+        ).rel_curve_to(
+            25.783102, -3.90214, 30.783332, -1.52811, 47.230192, 4.252451
+        ).rel_curve_to(
+            -11.30184, 19.609496, -21.35729, 20.701768, -35.31018, 32.087063
+        ).rel_curve_to(
+            5.56219, 12.080061, 12.91196, 25.953973, 9.98735, 45.917643
+        ).rel_curve_to(
+            -19.768963, -4.59388, -22.879866, -10.12216, -40.896842, -23.93099
+        ).rel_curve_to(
+            -11.463256, 10.23025, -17.377386, 18.2378, -41.515124, 25.03533
+        ).rel_curve_to(
+            0.05756, -29.49286, 4.71903, -31.931936, 10.342734, -46.700913
+        ).curve_to(
+            33.174997, 77.048676, 19.482194, 71.413009, 8.8631648, 52.420793
+        ).curve_to(
+            27.471602, 45.126773, 38.877997, 45.9184, 56.349456, 48.518302
+        ).curve_to(
+            59.03275, 31.351935, 64.893201, 16.103886, 76.221727, 3.9788409
+        ).close_path().rgba(
+            1.0, 0.6, 0.4, 0.4
+        ).fill()
+        ctx.restore()
+        return
+        ctx.move_to(116.89842, 17.221179).rel_curve_to(
+            6.77406, 15.003357, 9.99904, 35.088466, 0.27033, 47.639569
+        ).curve_to(
+            108.38621, 76.191194, 87.783414, 86.487988, 75.460015, 75.348373
+        ).curve_to(
+            64.051094, 64.686361, 61.318767, 54.582827, 67.499384, 36.894251
+        ).curve_to(
+            79.03955, 16.606134, 103.60918, 15.612261, 116.89842, 17.221179
+        ).close_path().rgb(
+            0.5, 0.3, 0.4
+        ).fill()
+
+        ctx.move_to(75.608612, 4.2453713).curve_to(
+            85.516707, 17.987709, 93.630911, 33.119248, 94.486497, 49.201225
+        ).curve_to(
+            95.068862, 60.147617, 85.880014, 75.820834, 74.919761, 75.632395
+        ).curve_to(
+            63.886159, 75.442695, 57.545631, 61.257211, 57.434286, 50.22254
+        ).curve_to(
+            57.257291, 32.681814, 65.992688, 16.610811, 75.608612, 4.2453713
+        ).close_path().rgb(
+            0.2, 0.5, 0.8
+        ).fill()
+        ctx.restore()
diff --git a/python_payload/apps/gr33nhouse/confirmation.py b/python_payload/apps/gr33nhouse/confirmation.py
new file mode 100644
index 0000000000000000000000000000000000000000..7974cb35748f78b569dbbf2d8fefdda86a819bee
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/confirmation.py
@@ -0,0 +1,70 @@
+from st3m.input import InputController, InputState
+from st3m.ui import colours
+from st3m.ui.view import BaseView, ViewManager
+from ctx import Context
+from .background import Flow3rView
+
+
+class ConfirmationView(BaseView):
+    background: Flow3rView
+    input: InputController
+
+    url: str
+    name: str
+    author: str
+
+    def __init__(self, url: str, name: str, author: str) -> None:
+        self.input = InputController()
+        self.vm = None
+        self.background = Flow3rView()
+
+        self.url = url
+        self.name = name
+        self.author = author
+
+    def on_enter(self, vm: ViewManager | None) -> None:
+        super().on_enter(vm)
+
+        if self.vm is None:
+            raise RuntimeError("vm is None")
+
+    def draw(self, ctx: Context) -> None:
+        ctx.move_to(0, 0)
+
+        self.background.draw(ctx)
+
+        ctx.save()
+        ctx.rgb(*colours.WHITE)
+        ctx.rectangle(
+            -120.0,
+            -80.0,
+            240.0,
+            160.0,
+        ).fill()
+
+        ctx.rgb(*colours.BLACK)
+        ctx.font = "Camp Font 3"
+        ctx.font_size = 24
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+
+        ctx.move_to(0, -60)
+        ctx.text("Install")
+
+        ctx.move_to(0, -30)
+        ctx.text(self.name)
+
+        ctx.move_to(0, 0)
+        ctx.text("by")
+
+        ctx.move_to(0, 30)
+        ctx.text(self.author)
+
+        ctx.move_to(0, 60)
+        ctx.text("?")
+
+        ctx.restore()
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        self.input.think(ins, delta_ms)
+        self.background.think(ins, delta_ms)
diff --git a/python_payload/apps/gr33nhouse/download.py b/python_payload/apps/gr33nhouse/download.py
new file mode 100644
index 0000000000000000000000000000000000000000..84dd89f368adcfa72293c5a9ed415cd33115ca63
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/download.py
@@ -0,0 +1,72 @@
+import network
+from st3m.input import InputState
+import urequests
+import gzip
+import utarfile
+import io
+import os
+from st3m.ui.view import BaseView
+from ctx import Context
+
+
+class DownloadView(BaseView):
+    def __init__(self, url: str) -> None:
+        super().__init__()
+        self._state = 1
+        self._try = 1
+        self._url = url
+
+    def draw(self, ctx: Context) -> None:
+        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
+
+        if self._state == 1 or self._state == 2:
+            # Fetching
+            ctx.rgb(255, 0, 0).rectangle(-20, -20, 40, 40).fill()
+            self._state = 2
+        elif self._state == 3 or self._state == 4:
+            # Extracting
+            ctx.rgb(0, 0, 255).rectangle(-20, -20, 40, 40).fill()
+            self._state = 4
+        elif self._state == 5:
+            # Done
+            ctx.rgb(0, 255, 0).rectangle(-20, -20, 40, 40).fill()
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        super().think(ins, delta_ms)  # Let BaseView do its thing
+
+        if self._state == 2:
+            try:
+                print("Getting it")
+                self.response = urequests.get(self._url)
+                if self.response.content is not None:
+                    print("Got something")
+                    self._state = 3
+                    return
+                print("no content...")
+            except:
+                print("Exception")
+            print("Next try")
+            self._try += 1
+
+        elif self._state == 4:
+            tar = gzip.decompress(self.response.content)
+            self.response = None
+            t = utarfile.TarFile(fileobj=io.BytesIO(tar))
+            for i in t:
+                print(i.name)
+                if i.type == utarfile.DIRTYPE:
+                    print("dirtype")
+                    dirname = "/flash/sys/apps/" + i.name
+                    if not os.path.exists(dirname):
+                        print("making", dirname)
+                        os.mkdir(dirname)
+                    else:
+                        print("dir", dirname, "exists")
+                else:
+                    filename = "/flash/sys/apps/" + i.name
+                    print("writing to", filename)
+                    f = t.extractfile(i)
+                    with open(filename, "wb") as of:
+                        of.write(f.read())
+            self._state = 5
+            self.vm.pop()
diff --git a/python_payload/apps/gr33nhouse/flow3r.toml b/python_payload/apps/gr33nhouse/flow3r.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2beaa7baf8ba26461e6b87f7db17fec7c5249ea3
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/flow3r.toml
@@ -0,0 +1,11 @@
+[app]
+name = "Get Apps"
+menu = "Apps"
+
+[entry]
+class = "Gr33nhouseApp"
+
+[metadata]
+author = "Flow3r Badge Authors"
+license = "LGPL-3.0-only"
+url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
diff --git a/python_payload/apps/gr33nhouse/manual.py b/python_payload/apps/gr33nhouse/manual.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e93bb69ef5cde3940e8fcc4cc93173b964bd0f0
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/manual.py
@@ -0,0 +1,42 @@
+from st3m.input import InputController, InputState
+from st3m.ui import colours
+from st3m.ui.view import BaseView, ViewManager
+from ctx import Context
+from .background import Flow3rView
+
+
+class ManualInputView(BaseView):
+    input: InputController
+
+    def __init__(self) -> None:
+        self.input = InputController()
+        self.vm = None
+        self.background = Flow3rView()
+
+    def on_enter(self, vm: ViewManager | None) -> None:
+        super().on_enter(vm)
+
+        if self.vm is None:
+            raise RuntimeError("vm is None")
+
+    def draw(self, ctx: Context) -> None:
+        ctx.move_to(0, 0)
+        ctx.save()
+        ctx.rgb(*colours.BLACK)
+        ctx.rectangle(
+            -120.0,
+            -120.0,
+            240.0,
+            240.0,
+        ).fill()
+
+        ctx.rgb(*colours.WHITE)
+        ctx.font = "Camp Font 3"
+        ctx.font_size = 24
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+        ctx.text("Coming soon")
+        ctx.restore()
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        self.input.think(ins, delta_ms)
diff --git a/python_payload/apps/gr33nhouse/record.py b/python_payload/apps/gr33nhouse/record.py
new file mode 100644
index 0000000000000000000000000000000000000000..be2e969e33e2f5af7e16b2f7c3494c2ea91557ae
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/record.py
@@ -0,0 +1,42 @@
+from st3m.input import InputController, InputState
+from st3m.ui import colours
+from st3m.ui.view import BaseView, ViewManager
+from ctx import Context
+from .background import Flow3rView
+
+
+class RecordView(BaseView):
+    input: InputController
+
+    def __init__(self) -> None:
+        self.input = InputController()
+        self.vm = None
+        self.background = Flow3rView()
+
+    def on_enter(self, vm: ViewManager | None) -> None:
+        super().on_enter(vm)
+
+        if self.vm is None:
+            raise RuntimeError("vm is None")
+
+    def draw(self, ctx: Context) -> None:
+        ctx.move_to(0, 0)
+        ctx.save()
+        ctx.rgb(*colours.BLACK)
+        ctx.rectangle(
+            -120.0,
+            -120.0,
+            240.0,
+            240.0,
+        ).fill()
+
+        ctx.rgb(*colours.WHITE)
+        ctx.font = "Camp Font 3"
+        ctx.font_size = 24
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+        ctx.text("Coming soon")
+        ctx.restore()
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        self.input.think(ins, delta_ms)