diff --git a/projects/damm/BUILD b/projects/damm/BUILD
new file mode 100644
index 0000000..653a463
--- /dev/null
+++ b/projects/damm/BUILD
@@ -0,0 +1,20 @@
+py_library(
+    name = "damm",
+    srcs = [
+        "damm.py",
+    ],
+    imports = [
+        "."
+    ],
+    visibility = [
+        "//visibility:public",
+    ]
+)
+
+py_pytest(
+    name = "test_damm",
+    srcs = glob(["test_*.py"]),
+    deps = [
+        ":damm",
+    ]
+)
diff --git a/projects/damm/README.md b/projects/damm/README.md
new file mode 100644
index 0000000..ca46a0c
--- /dev/null
+++ b/projects/damm/README.md
@@ -0,0 +1,23 @@
+# Damm
+
+An implementation of a Damm check digits.
+
+doi:10.2174/9781608058822114010013
+
+To get into the group theory here a bit, Damm showed that it was possible to construct an anti-symmetric quasigroup for most orders (group sizes).
+A quasigroup is a non-associative group for which division is always well defined.
+Damm's check digits are based on this non-associative property which allows for the full detection of transposition errors.
+
+We can use a quasigroup to implement a check digit scheme.
+Checking a number consists of "multiplying" the digits of the number together through the group's multiplication table.
+
+The neat bit of Damm's quasigroups is the property `x ยท x = 0`.
+This is visible in the multiplication matrix below.
+What this means is that if the Damm code (product of all digits within the Damm group!)
+of a numeric string is x, the product of THAT product with itself is 0.
+Which means we can compute the Damm coding of any number simply by APPENDING the product of its digits.
+
+Damm checking is then defined by computing the product of the digits within a Damm group and affirming that the product is 0 which is only possible if the input string is '0' OR it is a valid Damm checked number.
+
+Another nice trick with the Damm quasigroup is that it's safe to PREFIX zeros to any Damm coded number without changing the Damm correction code.
+Unfortunately this also means that SUFFIXING zeros won't impact the Damm code either.
diff --git a/projects/damm/damm.py b/projects/damm/damm.py
new file mode 100644
index 0000000..55460df
--- /dev/null
+++ b/projects/damm/damm.py
@@ -0,0 +1,78 @@
+"""
+An implementation of Damm check digits.
+"""
+
+import re
+from typing import NamedTuple
+
+
+class DammException(Exception):
+    """Base class for all exceptions in the module."""
+
+
+class CheckDigitException(DammException):
+    """Thrown when a string did not check when decoding a supposedly checked string."""
+
+
+class NoMatchException(DammException):
+    """Thrown when a string did not match."""
+
+
+class Damm(NamedTuple):
+    """A Damm coded number."""
+
+    MATRIX = (
+        (0, 3, 1, 7, 5, 9, 8, 6, 4, 2),
+        (7, 0, 9, 2, 1, 5, 4, 8, 6, 3),
+        (4, 2, 0, 6, 8, 7, 1, 3, 5, 9),
+        (1, 7, 5, 0, 9, 8, 3, 4, 2, 6),
+        (6, 1, 2, 3, 0, 4, 5, 9, 7, 8),
+        (3, 6, 7, 4, 2, 0, 9, 5, 8, 1),
+        (5, 8, 6, 9, 7, 2, 0, 1, 3, 4),
+        (8, 9, 4, 5, 3, 6, 2, 0, 1, 7),
+        (9, 4, 3, 8, 6, 1, 7, 2, 0, 5),
+        (2, 5, 8, 1, 4, 3, 6, 7, 9, 0),
+    )
+
+    DAMM_PATTERN = re.compile(r"(?P<value>\d+)-?(?P<check>\d)")
+
+    value: int
+
+    def __str__(self):
+        return f"{self.value}-{self.encode(self.value)}"
+
+    def __repr__(self):
+        return f"{self.__class__.__name__}({self.value}, {self.encode(self.value)})"
+
+    @classmethod
+    def encode(cls, value):
+        state = 0
+        for digit in str(value):
+            state = cls.MATRIX[state][int(digit)]
+        return state
+
+    @classmethod
+    def verify(cls, value: str) -> bool:
+        """Verify a Damm encoded number, returning its value if valid.
+
+        The core Damm encoding property is that the LAST digit is ALWAYS the check. When the rest of
+        the string is correctly Damm encoded, the Damm of the entire string will be 0.
+
+        """
+        return cls.encode(int(value.replace("-", ""))) == 0
+
+    @classmethod
+    def from_str(cls, value: str) -> "Damm":
+        """Verify a Damm coded string, and return its decoding."""
+        if match := re.fullmatch(cls.DAMM_PATTERN, value):
+            given_value = match.group("value")
+            computed_code = cls.encode(given_value)
+            given_code = int(match.group("check"))
+            if computed_code == given_code:
+                return Damm(int(given_value))
+            else:
+                raise CheckDigitException(
+                    f"Value {value!r} had check digit {given_code!r}, but computed check was {computed_code!r}"
+                )
+        else:
+            raise NoMatchException(f"Input {value!r} was not a valid decimal number")
diff --git a/projects/damm/test_damm.py b/projects/damm/test_damm.py
new file mode 100644
index 0000000..0314556
--- /dev/null
+++ b/projects/damm/test_damm.py
@@ -0,0 +1,5 @@
+"""
+
+"""
+
+from damm import Damm