diff --git a/lib/Data/Serialize/Extended.hs b/lib/Data/Serialize/Extended.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Serialize/Extended.hs
@@ -0,0 +1,22 @@
+
+module Data.Serialize.Extended (
+    roll
+  , unroll
+  ) where
+
+import Data.Bits
+import qualified Data.ByteString as BS
+import Data.List (unfoldr)
+import Numeric.Natural (Natural)
+
+-- | Simple little-endian ByteString encoding for Naturals.
+unroll :: Natural -> BS.ByteString
+unroll = BS.pack . unfoldr step where
+  step 0 = Nothing
+  step i = Just (fromIntegral i, i `shiftR` 8)
+
+-- | Simple little-endian ByteString decoding for Naturals.
+roll :: BS.ByteString -> Natural
+roll = foldr unstep 0 . BS.unpack where
+  unstep b a = a `shiftL` 8 .|. fromIntegral b
+
diff --git a/lib/Urbit/Ob/Co.hs b/lib/Urbit/Ob/Co.hs
--- a/lib/Urbit/Ob/Co.hs
+++ b/lib/Urbit/Ob/Co.hs
@@ -1,70 +1,132 @@
-{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE OverloadedStrings #-}
 
+-- |
+-- Module: Urbit.Ob.Co
+-- Copyright: (c) 2019 Jared Tobin
+-- License: MIT
+--
+-- Maintainer: Jared Tobin <jared@jtobin.io>
+-- Stability: unstable
+-- Portability: ghc
+--
+-- General functions for atom printing.
+--
+-- Roughly analogous to the +co arm in hoon.hoon.
+
 module Urbit.Ob.Co (
     Patp
-  , render
 
   , patp
   , fromPatp
+
+  , render
+  , parse
   ) where
 
 import qualified Data.ByteString as BS
-import qualified Data.Vector as V
-import qualified Data.Serialize as C
+import Data.Char (isAsciiLower)
+import Data.Foldable (foldrM)
+import qualified Data.Serialize.Extended as C
 import qualified Data.Text as T
-import Data.Word (Word8, Word16, Word)
-import Urbit.Ob.Ob (fein, fynd)
+import qualified Data.Vector as V
+import Data.Word (Word8)
+import Numeric.Natural (Natural)
+import Prelude hiding (log)
+import qualified Urbit.Ob.Ob as Ob (fein, fynd)
 
+-- | Hoon's \@p encoding.
+--
+--   This encoding is an /obfuscated/ representation of some underlying number,
+--   but a pronounceable, memorable, and unique one.
+--
+--   The representation exists for any natural number, but it's typically used
+--   only for naming Azimuth points, and thus normal 32-bit Urbit ships.
+--
+--   (It's also used for naming comets, i.e. self-signed 128-bit Urbit ships.)
+--
 newtype Patp = Patp BS.ByteString
-  deriving (Eq, Show)
+  deriving Eq
 
--- | Convert a nonnegative Int to a Patp value.
-patp :: Int -> Patp
-patp n
-    | n >= 0    = _patp
-    | otherwise = error "urbit-hob (patp): input out of range"
-  where
-    sxz  = fein n
-    sxz8 = fromIntegral sxz :: Word8
+instance Show Patp where
+  show = T.unpack . render
 
-    _patp
-      | met 3 sxz <= 1 = Patp (BS.cons 0 (BS.singleton sxz8))
-      | otherwise      = Patp (C.encode (fromIntegral sxz :: Word))
+unPatp :: Patp -> BS.ByteString
+unPatp (Patp p) = p
 
--- | Convert a Patp value to an Int.
-fromPatp :: Patp -> Int
-fromPatp (Patp p) = decoded where
-  decoded = case BS.length p of
-    2 -> case C.decode p :: Either String Word16 of
-      Left _  -> internalErr "fromPatp"
-      Right x -> fynd (fromIntegral x)
-    _ -> case C.decode p :: Either String Word of
-      Left _  -> internalErr "fromPatp"
-      Right x -> fynd (fromIntegral x)
+-- | Convert a 'Natural' to \@p.
+--
+--   >>> patp 0
+--   ~zod
+--   >>> patp 256
+--   ~marzod
+--   >>> patp 65536
+--   ~dapnep-ronmyl
+--   >>> patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+--   ~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes
+--
+patp :: Natural -> Patp
+patp = Patp . BS.reverse . C.unroll . Ob.fein
 
--- | Render a Patp value as Text.
+-- | Convert a \@p value to its corresponding 'Natural'.
+--
+--   >>> let zod = patp 0
+--   >>> fromPatp zod
+--   0
+--
+fromPatp :: Patp -> Natural
+fromPatp = Ob.fynd . C.roll . BS.reverse . unPatp
+
+-- | Render a \@p value as 'T.Text'.
+--
+--   >>> render (patp 0)
+--   "~zod"
+--   >>> render (patp 15663360)
+--   "~nidsut-tomdun"
 render :: Patp -> T.Text
-render (Patp p) = prefixed where
-  prefix = V.unsafeIndex prefixes . fromIntegral
-  suffix = V.unsafeIndex suffixes . fromIntegral
+render (Patp bs) = render' bs
 
-  prefixed = case T.uncons encoded of
-    Just ('-', pp) -> T.cons '~' pp
-    Just _         -> T.cons '~' encoded
-    _              -> internalErr "render"
+render' :: BS.ByteString -> T.Text
+render' bs =
+      T.cons '~'
+    . snd
+    . BS.foldr alg (0 :: Int, mempty)
+    $ padded
+  where
+    alg val (idx, acc) =
+      let syl = if even idx then suffix val else prefix val
+          glue
+            | idx `mod` 8 == 0 = if idx == 0 then mempty else "--"
+            | even idx         = "-"
+            | otherwise        = mempty
+      in  (succ idx, syl <> glue <> acc)
 
-  encoded = foldr alg mempty pruned where
-    alg (idx, x) acc
-      | odd idx   =        suffix x <> acc
-      | otherwise = "-" <> prefix x <> acc
+    padded =
+      let len = BS.length bs
+      in  if   (odd len && len > 2) || len == 0
+          then BS.cons 0 bs
+          else bs
 
-  pruned =
-    let len = BS.length p
-        indexed = zip [len, pred len..] (BS.unpack p)
-        padding (idx, val) = idx /= 1 && val == 0
-    in  dropWhile padding indexed
+-- | Parse a \@p value existing as 'T.Text'.
+--
+--   >>> parse "~nidsut-tomdun"
+--   Right ~nidsut-tomdun
+--   > parse "~fipfes-fipfes-fipfes-doznec"
+--   Right ~fipfes-fipfes-fipfes-doznec
+--
+parse :: T.Text -> Either T.Text Patp
+parse p =
+      fmap (Patp . snd)
+    $ foldrM alg (0 :: Int, mempty) syls
+  where
+    alg syl (idx, acc) = do
+      word <- if even idx then fromSuffix syl else fromPrefix syl
+      return (succ idx, BS.cons word acc)
 
+    syls =
+        T.chunksOf 3
+      . T.filter isAsciiLower
+      $ p
+
 prefixes :: V.Vector T.Text
 prefixes = V.fromList
   ["doz","mar","bin","wan","sam","lit","sig","hid","fid","lis","sog","dir"
@@ -90,6 +152,16 @@
   ,"lap","tal","pit","nam","bon","ros","ton","fod","pon","sov","noc","sor"
   ,"lav","mat","mip","fip"]
 
+prefix :: Integral a => a -> T.Text
+prefix = V.unsafeIndex prefixes . fromIntegral
+
+fromPrefix :: T.Text -> Either T.Text Word8
+fromPrefix syl = case V.findIndex (== syl) prefixes of
+    Nothing -> Left (msg syl)
+    Just x  -> Right (fromIntegral x :: Word8)
+  where
+    msg s = "urbit-hob (fromPrefix): invalid prefix \"" <> s <> "\""
+
 suffixes :: V.Vector T.Text
 suffixes = V.fromList
   ["zod","nec","bud","wes","sev","per","sut","let","ful","pen","syt","dur"
@@ -115,19 +187,13 @@
   ,"lyr","tes","mud","nyt","byr","sen","weg","fyr","mur","tel","rep","teg"
   ,"pec","nel","nev","fes"]
 
-bex :: Integral a => a -> a
-bex = (^) 2
-
-rsh :: Integral a => a -> a -> a -> a
-rsh a b c = c `div` bex (bex a * b)
-
-met :: Integral a => a -> a -> a
-met = loop 0 where
-  loop !acc a b
-    | b == 0    = acc
-    | otherwise = loop (succ acc) a (rsh a 1 b)
+suffix :: Integral a => a -> T.Text
+suffix = V.unsafeIndex suffixes . fromIntegral
 
-internalErr :: String -> a
-internalErr fn = error $
-  "urbit-hob (" <> fn <> "): internal error -- please report this as a bug!"
+fromSuffix :: T.Text -> Either T.Text Word8
+fromSuffix syl = case V.findIndex (== syl) suffixes of
+    Nothing -> Left (msg syl)
+    Just x  -> Right (fromIntegral x :: Word8)
+  where
+    msg s = "urbit-hob (fromSuffix): invalid suffix \"" <> s <> "\""
 
diff --git a/lib/Urbit/Ob/Muk.hs b/lib/Urbit/Ob/Muk.hs
--- a/lib/Urbit/Ob/Muk.hs
+++ b/lib/Urbit/Ob/Muk.hs
@@ -1,4 +1,17 @@
 
+-- |
+-- Module: Urbit.Ob.Muk
+-- Copyright: (c) 2019 Jared Tobin
+-- License: MIT
+--
+-- Maintainer: Jared Tobin <jared@jtobin.io>
+-- Stability: unstable
+-- Portability: ghc
+--
+-- A specific murmur3 variant.
+--
+-- Analogous to +muk in hoon.hoon.
+
 module Urbit.Ob.Muk (
     muk
   ) where
diff --git a/lib/Urbit/Ob/Ob.hs b/lib/Urbit/Ob/Ob.hs
--- a/lib/Urbit/Ob/Ob.hs
+++ b/lib/Urbit/Ob/Ob.hs
@@ -1,5 +1,18 @@
 {-# LANGUAGE BangPatterns #-}
 
+-- |
+-- Module: Urbit.Ob.Ob
+-- Copyright: (c) 2019 Jared Tobin
+-- License: MIT
+--
+-- Maintainer: Jared Tobin <jared@jtobin.io>
+-- Stability: unstable
+-- Portability: ghc
+--
+-- Integer obfuscation functions.
+--
+-- Analogous to the +ob arm in hoon.hoon.
+
 module Urbit.Ob.Ob (
     fein
   , fynd
@@ -14,8 +27,8 @@
 
 import Data.Bits
 import Data.Word (Word32)
-import Urbit.Ob.Muk (muk)
 import Prelude hiding (tail)
+import Urbit.Ob.Muk (muk)
 
 -- | Conceal structure v3.
 fein :: (Integral a, Bits a) => a -> a
@@ -43,7 +56,7 @@
              then hi .|. loop lo
              else cry
 
--- | Generalised Feistel cipher
+-- | Generalised Feistel cipher.
 --
 --   See: Black and Rogaway (2002), "Ciphers with arbitrary finite domains."
 --
diff --git a/test/Co.hs b/test/Co.hs
--- a/test/Co.hs
+++ b/test/Co.hs
@@ -1,14 +1,12 @@
 module Main where
 
+import qualified Co.Tests.Property as P
+import qualified Co.Tests.Unit as U
 import Test.Hspec
-import Test.Hspec.Core.QuickCheck (modifyMaxSuccess)
-import Test.QuickCheck
-import qualified Urbit.Ob.Co as Co
 
 main :: IO ()
-main = hspec $
-  describe "fromPatp" $
-    modifyMaxSuccess (const 1000) $
-      it "inverts patp" $
-        property $ \(NonNegative x) -> Co.fromPatp (Co.patp x) == x
+main =
+  hspec $ do
+    context "property tests" P.tests
+    context "unit tests" U.tests
 
diff --git a/test/Co/Tests/Property.hs b/test/Co/Tests/Property.hs
new file mode 100644
--- /dev/null
+++ b/test/Co/Tests/Property.hs
@@ -0,0 +1,42 @@
+
+module Co.Tests.Property (
+  tests
+  ) where
+
+import qualified Data.Text as T
+import Data.Word (Word32)
+import Numeric.Natural (Natural)
+import Test.Hspec
+import Test.Hspec.Core.QuickCheck (modifyMaxSuccess)
+import Test.QuickCheck
+import qualified Urbit.Ob.Co as Co
+
+nats :: Gen Natural
+nats = fmap fromIntegral (arbitrary :: Gen Word32)
+
+patps :: Gen Co.Patp
+patps = fmap Co.patp nats
+
+patpStrings :: Gen T.Text
+patpStrings = fmap Co.render patps
+
+tests :: Spec
+tests = do
+  describe "fromPatp" $
+    modifyMaxSuccess (const 1000) $
+      it "inverts patp" $
+        forAll nats $ \x -> Co.fromPatp (Co.patp x) == x
+
+  describe "patp" $
+    modifyMaxSuccess (const 1000) $
+      it "inverts fromPatp" $
+        forAll patps $ \x -> Co.patp (Co.fromPatp x) == x
+
+  describe "render" $
+    modifyMaxSuccess (const 1000) $
+      it "inverts parse" $
+        forAll patpStrings $ \x ->
+          case Co.parse x of
+            Left _  -> False
+            Right p -> Co.render p == x
+
diff --git a/test/Co/Tests/Unit.hs b/test/Co/Tests/Unit.hs
new file mode 100644
--- /dev/null
+++ b/test/Co/Tests/Unit.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Co.Tests.Unit (
+  tests
+  ) where
+
+import Data.Word (Word32)
+import Test.Hspec
+import qualified Urbit.Ob.Co as Co
+
+tests :: Spec
+tests =
+  describe "render" $ do
+    it "matches 32-bit reference values" $ do
+      Co.render (Co.patp 0) `shouldBe` "~zod"
+      Co.render (Co.patp 255) `shouldBe` "~fes"
+      Co.render (Co.patp 256) `shouldBe` "~marzod"
+      Co.render (Co.patp 65535) `shouldBe` "~fipfes"
+      Co.render (Co.patp 65536) `shouldBe` "~dapnep-ronmyl"
+      Co.render (Co.patp 15663360) `shouldBe` "~nidsut-tomdun"
+      Co.render (Co.patp 0xFFFFFFFF) `shouldBe` "~dostec-risfen"
+
+    it "matches 64-bit reference values" $ do
+      let big_64_01 = 0x0000000100000000
+      Co.render (Co.patp big_64_01) `shouldBe` "~doznec-dozzod-dozzod"
+
+      let big_64_02 = 0xFFFFFFFFFFFFFFFF
+      Co.render (Co.patp big_64_02) `shouldBe` "~fipfes-fipfes-dostec-risfen"
+
+    it "matches 128-bit reference values" $ do
+      let big_128_01  = 0x00000000000000010000000000000000
+          patp_128_01 = "~doznec--dozzod-dozzod-dozzod-dozzod"
+      Co.render (Co.patp big_128_01) `shouldBe` patp_128_01
+
+      let big_128_02  = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+          patp_128_02 =
+            "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes"
+
+      Co.render (Co.patp big_128_02) `shouldBe` patp_128_02
+
diff --git a/test/Ob.hs b/test/Ob.hs
--- a/test/Ob.hs
+++ b/test/Ob.hs
@@ -11,5 +11,5 @@
   hspec $ do
     context "small input space" S.tests
     context "medium input space" M.tests
-    context "32-bit input space" P.tests
+    context "propert tests" P.tests
     context "unit tests" U.tests
diff --git a/test/Ob/Tests/Property.hs b/test/Ob/Tests/Property.hs
--- a/test/Ob/Tests/Property.hs
+++ b/test/Ob/Tests/Property.hs
@@ -3,20 +3,48 @@
   tests
   ) where
 
+import Data.Word (Word32, Word64)
+import Numeric.Natural (Natural)
 import Test.Hspec
 import Test.Hspec.Core.QuickCheck (modifyMaxSuccess)
 import Test.QuickCheck
 import qualified Urbit.Ob.Ob as Ob
 
+planets :: Gen Word32
+planets = arbitrary `suchThat` (> 0xFFFF)
+
+word64 :: Gen Word64
+word64 = arbitrary
+
+nat :: Gen Natural
+nat = do
+  a <- fmap fromIntegral word64
+  b <- fmap fromIntegral word64
+  return (a * b)
+
 tests :: Spec
 tests = do
+  describe "fynd" $
+    modifyMaxSuccess (const 1000) $
+      it "inverts fein" $
+        forAll nat $ \x ->
+          Ob.fynd (Ob.fein x) == x
+
+  describe "fein" $
+    modifyMaxSuccess (const 1000) $
+      it "inverts fynd" $
+        forAll nat $ \x ->
+          Ob.fein (Ob.fynd x) == x
+
   describe "feis" $
     modifyMaxSuccess (const 1000) $
       it "inverts tail" $
-        property $ \x -> Ob.feis (Ob.tail x) == x
+        forAll planets $ \planet -> property $
+          Ob.feis (Ob.tail planet) == planet
 
   describe "tail" $
     modifyMaxSuccess (const 1000) $
       it "inverts feis" $
-        property $ \x -> Ob.tail (Ob.feis x) == x
+        forAll planets $ \planet -> property $
+          Ob.tail (Ob.feis planet) == planet
 
diff --git a/test/Ob/Tests/Unit.hs b/test/Ob/Tests/Unit.hs
--- a/test/Ob/Tests/Unit.hs
+++ b/test/Ob/Tests/Unit.hs
@@ -3,13 +3,31 @@
   tests
   ) where
 
+import Data.Word (Word32)
 import Test.Hspec
 import qualified Urbit.Ob.Ob as Ob
 
 tests :: Spec
-tests =
-  describe "tail . feis" $
-    context "when applied to 2052065766" $
-      it "should be the identity function" $
-        Ob.tail (Ob.feis 2052065766) `shouldBe` 2052065766
+tests = do
+  describe "fein" $
+    it "matches reference values" $ do
+      Ob.fein 123456789 `shouldBe` (1897766331 :: Int)
+      Ob.fein 15663360 `shouldBe` (1208402137 :: Int)
+      Ob.fein 0x10000000000000000 `shouldBe` (0x10000000000000000 :: Integer)
+
+  describe "fynd" $
+    it "matches reference values" $ do
+      Ob.fynd 1208402137 `shouldBe` (15663360 :: Int)
+      Ob.fynd 1897766331 `shouldBe` (123456789 :: Int)
+      Ob.fynd 0x10000000000000000 `shouldBe` (0x10000000000000000 :: Integer)
+
+  describe "feis" $
+    it "matches reference values" $ do
+      Ob.feis 123456789 `shouldBe` 2060458291
+      Ob.feis 15663360 `shouldBe` 1195593620
+
+  describe "tail" $
+    it "matches reference values" $ do
+      Ob.tail 123456789 `shouldBe` 1107963580
+      Ob.tail 1195593620 `shouldBe` 15663360
 
diff --git a/urbit-hob.cabal b/urbit-hob.cabal
--- a/urbit-hob.cabal
+++ b/urbit-hob.cabal
@@ -1,5 +1,5 @@
 name:           urbit-hob
-version:        0.1.1
+version:        0.2.0
 synopsis:       Hoon-style atom manipulation and printing functions
 homepage:       https://github.com/urbit/urbit-hob
 bug-reports:    https://github.com/urbit/urbit-hob/issues
@@ -16,20 +16,25 @@
   base used by Urbit.  The \@p encoding is used for naming ships; it uniquely
   represents a 32-bit number in a memorable and pronounceable fashion.
   .
-  The \@p encoding is an obfuscated representation of an underlying 32-bit
+  The \@p encoding is an /obfuscated/ representation of an underlying 32-bit
   number, in particular, hence the \"ob\" in the library's name.
   .
   The @Urbit.Ob@ module exposes two functions, 'patp' and 'fromPatp', for
-  converting between representations.  You can render a 'Patp' value via the
-  'render' function.
+  converting between representations.  You can also render a 'Patp' value via
+  the 'render' function, or parse one via 'parse'.
   .
-  Here's a quick example:
+  Some quick examples:
   .
-  > import qualified Urbit.Ob as Ob
-  >
-  > let nidsut = Ob.patp 15663360
-  > Ob.render nidsut   --  "~nidsut-tomdun"
-  > Ob.fromPatp nidsut --  15663360
+  >>> :set -XOverloadedStrings
+  >>> import qualified Urbit.Ob as Ob
+  >>>
+  >>> let nidsut = Ob.patp 15663360
+  >>> Ob.render nidsut
+  "~nidsut-tomdun"
+  >>> Ob.fromPatp nidsut
+  15663360
+  >>> Ob.parse "~nidsut-tomdun"
+  Right ~nidsut-tomdun
 
 source-repository head
   type: git
@@ -54,10 +59,13 @@
     , Urbit.Ob.Co
     , Urbit.Ob.Muk
     , Urbit.Ob.Ob
+
+  other-modules:
+      Data.Serialize.Extended
+
   build-depends:
       base        >= 4.7  && < 6
     , bytestring  >= 0.10 && < 1
-    , cereal      >= 0.5  && < 1
     , murmur3     >= 1.0  && < 2
     , text        >= 1.2  && < 2
     , vector      >= 0.12 && < 1
@@ -85,6 +93,9 @@
   type:                exitcode-stdio-1.0
   hs-source-dirs:      test
   main-is:             Co.hs
+  other-modules:
+    Co.Tests.Property
+    Co.Tests.Unit
   default-language:    Haskell2010
   ghc-options:
     -rtsopts
@@ -93,5 +104,6 @@
     , hspec
     , hspec-core
     , QuickCheck
+    , text
     , urbit-hob
 
