diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a59eb527..96cfa777a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,25 +19,37 @@ on:
 
 jobs:
   lint:
-    name: Lint & Mypy
+    name: Static Checks
     runs-on: ubuntu-latest
     steps:
+      - uses: extractions/setup-just@v2
       - uses: actions/checkout@v3
       - name: Set up Python 3
         uses: actions/setup-python@v4
         with:
           python-version: "3.10"
-      - name: mypy
-        run: make mypy
-      - name: lint
-        run: make lint
-      - name: fmtcheck
-        run: make fmtcheck
+      - name: check examples w/ mypy (against python@3.10)
+        run: just typecheck-examples
+      # skip deps on all these since mypy installed everything
+      - name: check linting
+        run: just --no-deps lint
+      - name: check formatting
+        run: just --no-deps format-check
+      # pyright depends on node, which it handles and installs for itself as needed
+      # we _could_ run setup-node to make it available for it if we're having reliability problems
+      - name: check types (all Python versions)
+        run: |
+          set -eox
+
+          for minor in {6..12}; do
+            just --no-deps typecheck $minor
+          done
 
   build:
     name: Build
     runs-on: ubuntu-latest
     steps:
+      - uses: extractions/setup-just@v2
       - uses: actions/checkout@v3
 
       - name: Set up Python 3
@@ -45,15 +57,9 @@ jobs:
         with:
           python-version: "3.10"
 
-      - name: Install tools
-        run: make venv
-
       - name: Build and check package
         run: |
-          set -x
-          source venv/bin/activate
-          python setup.py clean --all sdist bdist_wheel --universal
-          python -m twine check dist/*
+          just build
 
       - name: "Upload Artifact"
         uses: actions/upload-artifact@v3
@@ -69,45 +75,30 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python:
-          - { version: "3.6" , env: "py36" }
-          - { version: "3.7" , env: "py37" }
-          - { version: "3.8" , env: "py38" }
-          - { version: "3.9" , env: "py39" }
-          - { version: "3.10" , env: "py310" }
-          - { version: "3.11" , env: "py311" }
-          - { version: "3.12" , env: "py312" }
-          - { version: "pypy-3.7" , env: "py3.7" }
-          - { version: "pypy-3.8" , env: "py3.8" }
-          - { version: "pypy-3.9" , env: "py3.9" }
-          - { version: "pypy-3.10" , env: "py3.10" }
-    name: Test (${{ matrix.python.version }})
+        python_version:
+          - "3.6"
+          - "3.7"
+          - "3.8"
+          - "3.9"
+          - "3.10"
+          - "3.11"
+          - "3.12"
+          - "pypy-3.7"
+          - "pypy-3.8"
+          - "pypy-3.9"
+          - "pypy-3.10"
+    name: Test (${{ matrix.python_version }})
     steps:
+      - uses: extractions/setup-just@v2
       - uses: actions/checkout@v3
-
-      - name: Set up Python ${{ matrix.python.version }} and 3.10
+      - name: Set up Python ${{ matrix.python_version }}
         uses: actions/setup-python@v4
         with:
-          python-version: |
-            ${{ matrix.python.version }}
-            3.10
-
+          python-version: ${{ matrix.python_version }}
       - uses: stripe/openapi/actions/stripe-mock@master
 
-      - name: Typecheck with pyright
-        run: PYRIGHT_ARGS="-- --pythonversion ${{ matrix.python.version }}" make pyright
-        # Skip typecheking in pypy legs
-        if: ${{ !contains(matrix.python.version, 'pypy') }}
-
-      - name: Test with pytest
-        run: TOX_ARGS="-e ${{ matrix.python.env }}" make ci-test
-
-      - name: Calculate and publish coverage
-        run: make coveralls
-        if: env.COVERALLS_REPO_TOKEN && matrix.python.version == '3.10'
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+      - name: "run tests"
+        run: just test
 
   publish:
     name: Publish
@@ -137,11 +128,12 @@ jobs:
           GPG_SIGNING_PRIVKEY: ${{ secrets.GPG_SIGNING_PRIVKEY }}
           GPG_SIGNING_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }}
       - name: Install tools
-        run: make venv
-      - name: Publish packages to PyPy
+        run: just install-build-deps
+      - name: Publish packages to PyPI
+        # could probably move this into a just recipe too?
         run: |
           set -ex
-          source venv/bin/activate
+          source .venv/bin/activate
           export VERSION=$(cat VERSION)
           gpg --detach-sign --local-user $GPG_SIGNING_KEYID  --pinentry-mode loopback --passphrase $GPG_SIGNING_PASSPHRASE -a dist/stripe-$VERSION.tar.gz
           gpg --detach-sign --local-user $GPG_SIGNING_KEYID  --pinentry-mode loopback --passphrase $GPG_SIGNING_PASSPHRASE -a dist/stripe-$VERSION-py2.py3-none-any.whl
diff --git a/MANIFEST.in b/MANIFEST.in
index ba34b83e6..ff12f246b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,5 @@
-include .coveragerc .flake8 CHANGELOG.md LICENSE LONG_DESCRIPTION.rst README.md VERSION pytest.ini tox.ini
+# this file specifies what's included in a source distribution (https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist)
+# see also: https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html
+include .flake8 CHANGELOG.md LICENSE LONG_DESCRIPTION.rst README.md VERSION pytest.ini justfile
 recursive-include tests *.py
 recursive-include examples *.txt *.py
diff --git a/Makefile b/Makefile
deleted file mode 100644
index a85caa62a..000000000
--- a/Makefile
+++ /dev/null
@@ -1,49 +0,0 @@
-VENV_NAME?=venv
-PIP?=pip
-PYTHON?=python3.10
-DEFAULT_TEST_ENV?=py310
-
-venv: $(VENV_NAME)/bin/activate
-
-$(VENV_NAME)/bin/activate: setup.py requirements.txt
-	@test -d $(VENV_NAME) || $(PYTHON) -m venv --clear $(VENV_NAME)
-	${VENV_NAME}/bin/python -m pip install -r requirements.txt
-	@touch $(VENV_NAME)/bin/activate
-
-test: venv pyright lint mypy
-	@${VENV_NAME}/bin/tox -p auto -e $(DEFAULT_TEST_ENV) $(TOX_ARGS)
-
-test-nomock: venv
-	@${VENV_NAME}/bin/tox -p auto -- --nomock $(TOX_ARGS)
-
-ci-test: venv
-	@${VENV_NAME}/bin/tox $(TOX_ARGS)
-
-coveralls: venv
-	@${VENV_NAME}/bin/tox -e coveralls
-
-pyright: venv
-	@${VENV_NAME}/bin/tox -e pyright $(PYRIGHT_ARGS)
-
-mypy: venv
-	@${VENV_NAME}/bin/tox -e mypy $(MYPY_ARGS)
-
-fmt: venv
-	@${VENV_NAME}/bin/tox -e fmt
-
-fmtcheck: venv
-	@${VENV_NAME}/bin/tox -e fmt -- --check --verbose
-
-lint: venv
-	@${VENV_NAME}/bin/tox -e lint
-
-clean:
-	@rm -rf $(VENV_NAME) .coverage .coverage.* build/ dist/ htmlcov/
-
-update-version:
-	@echo "$(VERSION)" > VERSION
-	@perl -pi -e 's|VERSION = "[.\d\w]+"|VERSION = "$(VERSION)"|' stripe/_version.py
-
-codegen-format: fmt
-
-.PHONY: ci-test clean codegen-format coveralls fmt fmtcheck lint test test-nomock test-travis update-version venv pyright mypy
diff --git a/README.md b/README.md
index 62f399fd0..35cfa9912 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
 
 [![pypi](https://img.shields.io/pypi/v/stripe.svg)](https://pypi.python.org/pypi/stripe)
 [![Build Status](https://github.com/stripe/stripe-python/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stripe/stripe-python/actions?query=branch%3Amaster)
-[![Coverage Status](https://coveralls.io/repos/github/stripe/stripe-python/badge.svg?branch=master)](https://coveralls.io/github/stripe/stripe-python?branch=master)
 
 The Stripe Python library provides convenient access to the Stripe API from
 applications written in the Python language. It includes a pre-defined set of
@@ -341,46 +340,48 @@ go install github.com/stripe/stripe-mock@latest
 stripe-mock
 ```
 
-Run the following command to set up the development virtualenv:
+We use [just](https://github.com/casey/just) for conveniently running development tasks. You can use them directly, or copy the commands out of the `justfile`. To our help docs, run `just`. By default, all commands will use an virtualenv created by your default python version (whatever comes out of `python --version`). We recommend using [mise](https://mise.jdx.dev/lang/python.html) or [pyenv](https://github.com/pyenv/pyenv) to control that version.
 
-```sh
-make
-```
-
-Run all tests on all supported Python versions:
+Run the following command to set up the development virtualenv:
 
 ```sh
-make test
+just venv
+# or: python -m venv venv  && venv/bin/python -I -m pip install -e .
 ```
 
-Run all tests for a specific Python version (modify `-e` according to your Python target):
+Run all tests:
 
 ```sh
-TOX_ARGS="-e py37" make test
+just test
+# or: venv/bin/pytest
 ```
 
 Run all tests in a single file:
 
 ```sh
-TOX_ARGS="-e py37 -- tests/api_resources/abstract/test_updateable_api_resource.py" make test
+just test tests/api_resources/abstract/test_updateable_api_resource.py
+# or: venv/bin/pytest tests/api_resources/abstract/test_updateable_api_resource.py
 ```
 
 Run a single test suite:
 
 ```sh
-TOX_ARGS="-e py37 -- tests/api_resources/abstract/test_updateable_api_resource.py::TestUpdateableAPIResource" make test
+just test tests/api_resources/abstract/test_updateable_api_resource.py::TestUpdateableAPIResource
+# or: venv/bin/pytest tests/api_resources/abstract/test_updateable_api_resource.py::TestUpdateableAPIResource
 ```
 
 Run a single test:
 
 ```sh
-TOX_ARGS="-e py37 -- tests/api_resources/abstract/test_updateable_api_resource.py::TestUpdateableAPIResource::test_save" make test
+just test tests/api_resources/abstract/test_updateable_api_resource.py::TestUpdateableAPIResource::test_save
+# or: venv/bin/pytest tests/api_resources/abstract/test_updateable_api_resource.py::TestUpdateableAPIResource::test_save
 ```
 
 Run the linter with:
 
 ```sh
-make lint
+just lint
+# or: venv/bin/python -m flake8 --show-source stripe tests setup.py
 ```
 
 The library uses [Ruff][ruff] for code formatting. Code must be formatted
@@ -388,7 +389,8 @@ with Black before PRs are submitted, otherwise CI will fail. Run the formatter
 with:
 
 ```sh
-make fmt
+just format
+# or: venv/bin/ruff format . --quiet
 ```
 
 [api-keys]: https://dashboard.stripe.com/account/apikeys
diff --git a/deps/build-requirements.txt b/deps/build-requirements.txt
new file mode 100644
index 000000000..ae45d54c1
--- /dev/null
+++ b/deps/build-requirements.txt
@@ -0,0 +1,4 @@
+# packages needed to package & release
+
+twine
+setuptools
diff --git a/deps/dev-requirements.txt b/deps/dev-requirements.txt
new file mode 100644
index 000000000..cedd76e2b
--- /dev/null
+++ b/deps/dev-requirements.txt
@@ -0,0 +1,11 @@
+# packages needed to run static analysis (lints, types, etc)
+# version requirements: any modern python version (currently 3.10)
+
+# typechecking for all versions
+pyright == 1.1.336
+# general typechecking
+mypy == 1.7.0
+# formatting
+ruff == 0.4.4
+# linting
+flake8
diff --git a/test-requirements.txt b/deps/test-requirements.txt
similarity index 72%
rename from test-requirements.txt
rename to deps/test-requirements.txt
index f2e9a43ba..a04785b92 100644
--- a/test-requirements.txt
+++ b/deps/test-requirements.txt
@@ -1,4 +1,5 @@
-# These requirements must be installable on all our supported versions
+# packages needed to run unit tests (including extra supported http libraries)
+# version requirements: all supported versions (currently 3.6-3.12)
 
 # This is the last version of httpx compatible with Python 3.6
 httpx == 0.22.0; python_version == "3.6"
@@ -8,10 +9,7 @@ aiohttp == 3.8.6; python_version <= "3.7"
 aiohttp == 3.9.4; python_version > "3.7"
 anyio[trio] == 3.6.2
 
-pytest-cov >= 2.8.1, < 2.11.0
 pytest-mock >= 2.0.0
 mock >= 4.0; python_version < "3.8"
 pytest-xdist >= 1.31.0
 pytest >= 6.0.0
-coverage >= 4.5.3, < 5
-coveralls
diff --git a/justfile b/justfile
new file mode 100644
index 000000000..4e8c0fec8
--- /dev/null
+++ b/justfile
@@ -0,0 +1,81 @@
+set quiet
+
+import? '../sdk-codegen/justfile'
+
+VENV_NAME := ".venv"
+
+export PATH := `pwd` / VENV_NAME / "bin:" + env('PATH')
+
+_default:
+    just --list --unsorted
+
+# ⭐ run all unit tests
+test *args: install-test-deps
+    # configured in pyproject.toml
+    pytest {{ args }}
+
+# ⭐ check for potential mistakes
+lint: install-dev-deps
+    python -m flake8 --show-source stripe tests setup.py
+
+# verify types. optional argument to test as of a specific minor python version (e.g. `8` to test `python 3.8`); otherwise uses current version
+typecheck minor_py_version="": install-test-deps install-dev-deps
+    # suppress version update warnings
+    PYRIGHT_PYTHON_IGNORE_WARNINGS=1 pyright {{ if minor_py_version == "" { "" } else { "--pythonversion 3." + minor_py_version } }}
+
+# ⭐ format all code
+format: install-dev-deps
+    ruff format . --quiet
+
+# verify formatting, but don't modify files
+format-check: install-dev-deps
+    ruff format . --check  --quiet
+
+# remove venv
+clean:
+    # clear old files too
+    rm -rf {{ VENV_NAME }} venv .tox
+
+# blow away and reinstall virtual env
+reset: clean && venv
+
+# build the package for upload
+build: install-build-deps
+    # --universal is deprecated, so we'll probably need to look at this eventually
+    # given that we don't care about universal 2 and 3 packages, we probably don't need it?
+    python -I setup.py clean --all sdist bdist_wheel --universal
+    python -m twine check dist/*
+
+# typecheck some examples w/ mypy
+typecheck-examples: _install-all
+    # configured in pyproject.toml
+    mypy
+
+# install the tools for local development & static checks
+install-dev-deps: (install "dev")
+
+# install everything for unit tests
+install-test-deps: (install "test")
+
+# install dependencies to build the package
+install-build-deps: (install "build")
+
+_install-all: install-dev-deps install-test-deps install-build-deps
+
+# installs files out of a {group}-requirements.txt into the local venv; mostly used by other recipes
+install group: venv
+    python -I -m pip install -r deps/{{ group }}-requirements.txt --disable-pip-version-check {{ if is_dependency() == "true" {"--quiet"} else {""} }}
+
+# create a virtualenv if it doesn't exist; always installs the local package
+[private]
+venv:
+    [ -d {{ VENV_NAME }} ] || ( \
+        python -m venv {{ VENV_NAME }} && \
+        {{ VENV_NAME }}/bin/python -I -m pip install -e . --quiet --disable-pip-version-check \
+    )
+
+# called by tooling
+[private]
+update-version version:
+    @echo "{{ version }}" > VERSION
+    @perl -pi -e 's|VERSION = "[.\d\w]+"|VERSION = "{{ version }}"|' stripe/_version.py
diff --git a/pyproject.toml b/pyproject.toml
index 7ed6ba63c..dae143954 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,4 +33,15 @@ warn_unused_ignores = true
 no_implicit_reexport = true
 
 [tool.pytest.ini_options]
-filterwarnings = "ignore::DeprecationWarning"
+# use as many threads as available for tests
+addopts = "-n auto"
+# already the default, but will speed up searching a little, since this is the only place tests live
+testpaths = "tests"
+# these are warnings we show to users; we don't need them in our test logs
+filterwarnings = [
+  # single quotes so we don't have to re-backslack our backslashes
+  'ignore:[\S\s]*stripe-python:DeprecationWarning',
+  'ignore:[\S\s]*stripe\..* package:DeprecationWarning',
+  'ignore:[\S\s]*RecipientTransfer:DeprecationWarning',
+  'ignore:[\S\s]*`save` method is deprecated:DeprecationWarning',
+]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index cfe397d29..000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# These requirements must be installable only on py3.10 +
-twine
-# 4.5.0 is the last version that works with virtualenv<20.22.0
-tox == 4.5.0
-#Virtualenv 20.22.0 dropped support for all Python versions smaller or equal to Python 3.6.
-virtualenv<20.22.0
-pyright == 1.1.336
-ruff == 0.4.4
-flake8
-mypy == 1.7.0
-
--r test-requirements.txt
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 23ee4816e..000000000
--- a/tox.ini
+++ /dev/null
@@ -1,56 +0,0 @@
-# Tox (http://tox.testrun.org/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
-
-[tox]
-envlist =
-    fmt
-    lint
-    pyright
-    mypy
-    py{312,311,310,39,38,37,36,py3}
-ignore_base_python_conflict = false
-
-[tool:pytest]
-testpaths = tests
-addopts =
-    --cov-report=term-missing
-
-[testenv]
-description = run the unit tests under {basepython}
-setenv =
-    COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
-deps =
-  -r test-requirements.txt
-
-# ignore stripe directory as all tests are inside ./tests
-commands = pytest --cov {posargs:-n auto} --ignore stripe
-# compilation flags can be useful when prebuilt wheels cannot be used, e.g.
-# PyPy 2 needs to compile the `cryptography` module. On macOS this can be done
-# by passing the following flags:
-# LDFLAGS="-L$(brew --prefix openssl@1.1)/lib"
-# CFLAGS="-I$(brew --prefix openssl@1.1)/include"
-passenv = LDFLAGS,CFLAGS
-
-[testenv:{lint,fmt,pyright,mypy}]
-basepython = python3.10
-skip_install = true
-commands =
-  pyright: pyright {posargs}
-  lint: python -m flake8  --show-source stripe tests setup.py
-  fmt: ruff format . {posargs}
-  mypy: mypy {posargs}
-deps =
-  -r requirements.txt
-
-[testenv:coveralls]
-description = upload coverage to coveralls.io
-skip_install = true
-setenv =
-    COVERAGE_FILE = {toxworkdir}/.coverage
-passenv = GITHUB_*
-commands =
-    coverage combine
-    coveralls --service=github
-depends = py{312,311,310,39,38,37,36,py3}