diff --git a/nixos/pkgs/zkapissuer/default.nix b/nixos/pkgs/zkapissuer/default.nix
index b4f90d3582cd686fbdf62a6267cb1070c05e9c57..efa55ff108e72fb7d78d95c6db46bddcdca1116f 100644
--- a/nixos/pkgs/zkapissuer/default.nix
+++ b/nixos/pkgs/zkapissuer/default.nix
@@ -1,6 +1,7 @@
-{ callPackage }:
+{ callPackage, fetchFromGitHub, lib }:
 let
-  repo = callPackage ./repo.nix { };
+  repo-data = lib.importJSON ./repo.json;
+  repo = fetchFromGitHub (builtins.removeAttrs repo-data [ "branch" ]);
   PaymentServer = (import "${repo}/nix").PaymentServer;
 in
   PaymentServer.components.exes."PaymentServer-exe"
diff --git a/nixos/pkgs/zkapissuer/repo.json b/nixos/pkgs/zkapissuer/repo.json
new file mode 100644
index 0000000000000000000000000000000000000000..0a003dc61620fd92b1a618e9845763e276c9693a
--- /dev/null
+++ b/nixos/pkgs/zkapissuer/repo.json
@@ -0,0 +1,8 @@
+{
+  "owner": "PrivateStorageio",
+  "repo": "PaymentServer",
+  "rev": "e080beb14ec58ffe8e55c35e6dddd46c5082887f",
+  "branch": "main",
+  "outputHashAlgo": "sha256",
+  "outputHash": "1zck9kawbs2lkr3qjipira9gawa4gxlqijqqjrmlvvyp9mr0fgxm"
+}
diff --git a/nixos/pkgs/zkapissuer/repo.nix b/nixos/pkgs/zkapissuer/repo.nix
deleted file mode 100644
index 6646a2e32eb8e5a747e4491ce43f706fee65724c..0000000000000000000000000000000000000000
--- a/nixos/pkgs/zkapissuer/repo.nix
+++ /dev/null
@@ -1,7 +0,0 @@
-{ fetchFromGitHub }:
-fetchFromGitHub {
-  owner = "PrivateStorageio";
-  repo = "PaymentServer";
-  rev = "ff30e85c231a3b5ad76426bbf8801f8f76884367";
-  sha256 = "1spz19f5z96shmfpazj0rv6877xvchf3gl49a4xahjbbsz39x34x";
-}
diff --git a/tools/default.nix b/tools/default.nix
index fb44c660e7d4a4a62cec6bb58a008a1bf00429dc..b10bb5f209c44c3ccba5cf509655e6d25fbb88da 100644
--- a/tools/default.nix
+++ b/tools/default.nix
@@ -16,6 +16,7 @@ let
   python-commands = [
     ./update-nixpkgs
     ./update-gitlab-repo
+    ./update-github-repo
   ];
 in
   # This derivation creates a package that wraps our tools to setup an environment
diff --git a/tools/update-github-repo b/tools/update-github-repo
new file mode 100644
index 0000000000000000000000000000000000000000..0e7e1511fc017c360660dc9fb752ff03f315f9bb
--- /dev/null
+++ b/tools/update-github-repo
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+"""
+Update a pinned github repository.
+
+Pass this path to a JSON file and it will update it to the latest
+version of the branch it specifies. You can also pass a different
+branch or repository owner, which will update the file to point at
+the new branch/repository, and update to the latest version.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import httpx
+from ps_tools import get_url_hash
+
+HASH_TYPE = "sha512"
+
+ARCHIVE_TEMPLATE = "https://api.github.com/repos/{owner}/{repo}/tarball/{rev}"
+BRANCH_TEMPLATE = (
+    "https://api.github.com/repos/{owner}/{repo}/commits/{branch}"
+)
+
+
+def get_github_commit(config):
+    response = httpx.get(BRANCH_TEMPLATE.format(**config))
+    response.raise_for_status()
+    return response.json()["sha"]
+
+
+def get_github_archive_url(config):
+    return ARCHIVE_TEMPLATE.format(**config)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "repo_file",
+        metavar="repo-file",
+        type=Path,
+        help="JSON file with pinned configuration.",
+    )
+    parser.add_argument(
+        "--branch",
+        type=str,
+        help="Branch to update to.",
+    )
+    parser.add_argument(
+        "--owner",
+        type=str,
+        help="Repository owner to update to.",
+    )
+    parser.add_argument(
+        "--rev",
+        type=str,
+        help="Revision to pin.",
+    )
+    parser.add_argument(
+        "--dry-run",
+        action="store_true",
+    )
+    args = parser.parse_args()
+
+    repo_file = args.repo_file
+    config = json.loads(repo_file.read_text())
+
+    for key in ["owner", "branch"]:
+        if getattr(args, key) is not None:
+            config[key] = getattr(args, key)
+
+    if args.rev is not None:
+        config["rev"] = args.rev
+    else:
+        config["rev"] = get_github_commit(config)
+
+    archive_url = get_github_archive_url(config)
+    config.update(get_url_hash(HASH_TYPE, "source", archive_url))
+
+    output = json.dumps(config, indent=2)
+    if args.dry_run:
+        print(output)
+    else:
+        repo_file.write_text(output)
+
+
+if __name__ == "__main__":
+    main()