diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,116 @@
+mason: alacritous builder library
+====
+
+[![Build Status](https://travis-ci.com/fumieval/mason.svg?branch=master)](https://travis-ci.com/fumieval/mason)
+[![Hackage](https://img.shields.io/hackage/v/mason)](https://hackage.haskell.org/package/mason)
+
+mason is a builder & IO library.
+
+* __Fast__: much faster than bytestring's Builder.
+* __Extensible__: Builders can be consumed in a user-defined way.
+* __Hackable__: Low-level APIs are exposed. It's easy to plug in even pointer-level operations.
+
+`Mason.Builder` has API mostly compatible with `Data.ByteString.Builder` but there are some additions to the original API:
+
+* `toStrictByteString` produces a strict `ByteString` directly.
+* `hPutBuilderLen` writes a builder to a handle and returns the number of bytes.
+* `sendBuilder` sends the content of `Builder` over a socket.
+
+Usage
+----
+
+Replace `Data.ByteString.Builder` with `Mason.Builder`. Note that if you have `Builder` in the type signature, you'll need `RankNTypes` extensions because of the design explained below.
+
+Performance
+----
+
+As long as the code is optimised, mason's builder can be very fast (twice or more as bytestring). Make sure that functions returning `Builder`s are well inlined.
+
+Serialisation of JSON-like structure:
+
+```
+mason/hPutBuilder                        mean 274.7 μs  ( +- 49.40 μs  )
+fast-builder/hPutBuilder                 mean 399.9 μs  ( +- 76.05 μs  )
+bytestring/hPutBuilder                   mean 335.1 μs  ( +- 86.96 μs  )
+mason/toStrictByteString                 mean 106.6 μs  ( +- 6.680 μs  )
+fast-builder/toStrictByteString          mean 254.8 μs  ( +- 31.64 μs  )
+bytestring/toLazyByteString              mean 283.3 μs  ( +- 24.26 μs  )
+mason/toLazyByteString                   mean 127.2 μs  ( +- 25.86 μs  )
+fast-builder/toLazyByteString            mean 249.0 μs  ( +- 25.60 μs  )
+bytestring/toLazyByteString              mean 263.4 μs  ( +- 9.401 μs  )
+```
+
+In the same benchmark application, the allocation footprint of mason is feathery.
+
+```
+toStrictByteString
+mason           291,112    0
+fast-builder    991,016    0
+bytestring    1,158,584    0 (toStrict . toLazyByteString)
+
+toLazyByteString
+Case          Allocated  GCs
+mason           228,936    0
+fast-builder    903,752    0
+bytestring    1,101,448    0
+```
+
+`doubleDec` employs Grisu3 which grants ~20x speedup over `show`-based implementation.
+
+```
+mason/double                             mean 116.2 ns  ( +- 6.654 ns  )
+fast-builder/double                      mean 2.183 μs  ( +- 85.80 ns  )
+bytestring/double                        mean 2.312 μs  ( +- 118.8 ns  )
+```
+
+TBD: more benchmarks
+
+Architecture
+----
+
+Mason's builder is a function that takes a purpose-dependent environment and a buffer. There is little intermediate structure involved; almost everything runs in one pass. This design is inspired by [fast-builder](http://hackage.haskell.org/package/fast-builder).
+
+```haskell
+type Builder = forall s. Buildable s => BuilderFor s
+
+newtype BuilderFor s = Builder { unBuilder :: s -> Buffer -> IO Buffer }
+
+data Buffer = Buffer
+  { bEnd :: {-# UNPACK #-} !(Ptr Word8) -- ^ end of the buffer (next to the last byte)
+  , bCur :: {-# UNPACK #-} !(Ptr Word8) -- ^ current position
+  }
+
+class Buildable s where
+  byteString :: B.ByteString -> BuilderFor s
+  flush :: BuilderFor s
+  allocate :: Int -> BuilderFor s
+```
+
+Instances of the `Buildable` class implement purpose-specific behaviour (e.g. exponentially allocate a buffer, flush to disk). This generic interface also allows creative uses of Builders such as on-the-fly compression.
+
+`Builder` has a smart constructor called `ensure`:
+
+```haskell
+ensure :: Int -> (Buffer -> IO Buffer) -> Builder
+```
+
+`ensure n f` secures at least `n` bytes in the buffer and passes the pointer to `f`. This gives rise to monoid homorphism; namely, `ensure m f <> ensure n g` will fuse into `ensure (m + n) (f >=> g)` so don't worry about the overhead of bound checking.
+
+Creating your own primitives
+----
+
+The easiest way to create a new primitive is `withPtr`, a simplified version of `ensure`. This is quite convenient for calling foreign functions or anything low-level.
+
+```haskell
+-- | Construct a 'Builder' from a "poke" function.
+withPtr :: Int -- ^ number of bytes to allocate (if needed)
+  -> (Ptr Word8 -> IO (Ptr Word8)) -- ^ return a next pointer after writing
+  -> Builder
+
+grisu v = withPtr 24 $ \ptr -> do
+  n <- dtoa_grisu3 v ptr
+  return $ plusPtr ptr (fromIntegral n)
+
+foreign import ccall unsafe "static dtoa_grisu3"
+  dtoa_grisu3 :: Double -> Ptr Word8 -> IO CInt
+```
diff --git a/mason.cabal b/mason.cabal
--- a/mason.cabal
+++ b/mason.cabal
@@ -1,7 +1,7 @@
 cabal-version:       2.4
 
 name:                mason
-version:             0
+version:             0.1
 synopsis:            Fast and extensible bytestring builder
 description:         See README.md
 bug-reports:         https://github.com/fumieval/mason/issues
@@ -11,7 +11,8 @@
 maintainer:          fumiexcel@gmail.com
 copyright:           2019 Fumiaki Kinoshita, Don Stewart 2005-2009, Duncan Coutts 2006-2015, David Roundy 2003-2005, Jasper Van der Jeugt 2010, Simon Meier 2010-2013, Ben Gamari 2017
 category:            Data
-extra-source-files:  CHANGELOG.md
+extra-source-files:  CHANGELOG.md, README.md
+tested-with:         GHC == 8.6.5, GHC==8.8.1
 
 library
   exposed-modules:
diff --git a/src/Mason/Builder.hs b/src/Mason/Builder.hs
--- a/src/Mason/Builder.hs
+++ b/src/Mason/Builder.hs
@@ -1,6 +1,15 @@
 {-# LANGUAGE MagicHash, CPP, UnboxedTuples #-}
 {-# LANGUAGE ForeignFunctionInterface #-}
 {-# LANGUAGE RankNTypes #-}
+----------------------------------------------------------------------------
+-- |
+-- Module      :  Mason.Builders
+-- Copyright   :  (c) Fumiaki Kinoshita 2019
+-- License     :  BSD3
+--
+-- Maintainer  :  Fumiaki Kinoshita <fumiexcel@gmail.com>
+--
+----------------------------------------------------------------------------
 module Mason.Builder
   ( Builder
   , BuilderFor
@@ -11,13 +20,14 @@
   , hPutBuilderLen
   , hPutBuilder
   , sendBuilder
-  -- * Raw
+  -- * Primitives
   , flush
   , encodeUtf8Builder
   , encodeUtf8BuilderEscaped
   , byteString
   , lazyByteString
   , shortByteString
+  , storable
   , int8
   , word8
   , int16LE
@@ -73,6 +83,8 @@
   , byteStringHex
   , lazyByteStringHex
   -- * Advanced
+  , padded
+  , zeroPadded
   , primFixed
   , primBounded
   , lengthPrefixedWithin
@@ -99,6 +111,7 @@
 hPutBuilder h b = void $ hPutBuilderLen h b
 {-# INLINE hPutBuilder #-}
 
+-- | Combine chunks of a lazy 'BL.ByteString'
 lazyByteString :: BL.ByteString -> Builder
 lazyByteString = foldMap byteString . BL.toChunks
 {-# INLINE lazyByteString #-}
@@ -299,9 +312,9 @@
   | x == 0 = string7 "0.0"
   | otherwise = grisu x
   where
-    grisu v = B.ensure 24 $ \(B.Buffer end ptr) -> do
+    grisu v = withPtr 24 $ \ptr -> do
       n <- dtoa_grisu3 v ptr
-      return $ B.Buffer end $ plusPtr ptr (fromIntegral n)
+      return $ plusPtr ptr (fromIntegral n)
 
 foreign import ccall unsafe "static dtoa_grisu3"
   dtoa_grisu3 :: Double -> Ptr Word8 -> IO CInt
diff --git a/src/Mason/Builder/Internal.hs b/src/Mason/Builder/Internal.hs
--- a/src/Mason/Builder/Internal.hs
+++ b/src/Mason/Builder/Internal.hs
@@ -28,6 +28,10 @@
   , SocketEnv(..)
   , cstring
   , cstringUtf8
+  , withPtr
+  , storable
+  , padded
+  , zeroPadded
   -- * Internal
   , ensure
   , allocateConstant
@@ -56,7 +60,7 @@
 import Data.IORef
 import Data.Word (Word8)
 import Data.String
-import qualified Foreign.Storable as S
+import Foreign.Storable as S
 import System.IO.Unsafe
 import qualified Data.Text.Array as A
 import qualified Data.Text.Internal as T
@@ -75,9 +79,12 @@
 -- | Builder specialised for a backend
 newtype BuilderFor s = Builder { unBuilder :: s -> Buffer -> IO Buffer }
 
+-- | This class is used to provide backend-specific operations for running a 'Builder'.
 class Buildable s where
   -- | Put a 'B.ByteString'.
   byteString :: B.ByteString -> BuilderFor s
+  byteString = byteStringCopy
+  {-# INLINE byteString #-}
   -- | Flush the content of the internal buffer.
   flush :: BuilderFor s
   -- | Allocate a buffer with at least the given length.
@@ -91,19 +98,31 @@
 
 -- | Copy a 'B.ByteString' to a buffer.
 byteStringCopy :: Buildable s => B.ByteString -> BuilderFor s
-byteStringCopy = \(B.PS fsrc ofs len) -> ensure len $ \(Buffer end ptr) -> do
+byteStringCopy = \(B.PS fsrc ofs len) -> withPtr len $ \ptr -> do
   withForeignPtr fsrc $ \src -> B.memcpy ptr (src `plusPtr` ofs) len
-  return $ Buffer end (ptr `plusPtr` len)
+  return $ ptr `plusPtr` len
 {-# INLINE byteStringCopy #-}
 
 -- | Copy a 'SB.ShortByteString' to a buffer.
 shortByteString :: SB.ShortByteString -> Builder
-shortByteString = \src -> let len = SB.length src in ensure len $ \(Buffer end ptr) ->
-  Buffer end (ptr `plusPtr` len)
-  <$ SB.copyToPtr src 0 ptr len
+shortByteString = \src -> let len = SB.length src in withPtr len $ \ptr ->
+  plusPtr ptr len <$ SB.copyToPtr src 0 ptr len
 {-# INLINE shortByteString #-}
 
--- | Ensure that the given number of bytes is available in the buffer. Subject to semigroup fusions
+-- | Construct a 'Builder' from a "poke" function.
+withPtr :: Buildable s
+  => Int -- ^ number of bytes to allocate (if needed)
+  -> (Ptr Word8 -> IO (Ptr Word8)) -- ^ return a next pointer after writing
+  -> BuilderFor s
+withPtr n f = ensure n $ \(Buffer e p) -> Buffer e <$> f p
+{-# INLINE withPtr #-}
+
+-- | Turn a 'Storable' value into a 'Builder'
+storable :: Storable a => a -> Builder
+storable a = withPtr (sizeOf a) $ \p -> plusPtr p (sizeOf a) <$ poke (castPtr p) a
+{-# INLINE storable #-}
+
+-- | Ensure that the given number of bytes is available in the buffer. Subject to semigroup fusion
 ensure :: Int -> (Buffer -> IO Buffer) -> Builder
 ensure mlen cont = Builder $ \env buf@(Buffer end ptr) ->
   if ptr `plusPtr` mlen >= end
@@ -199,14 +218,14 @@
   fromString = stringUtf8
   {-# INLINE fromString #-}
 
+-- | Use 'B.BoundedPrim'
 primBounded :: B.BoundedPrim a -> a -> Builder
-primBounded bp a = ensure (B.sizeBound bp)
-  $ \(Buffer end ptr) -> Buffer end <$> B.runB bp a ptr
+primBounded bp = withPtr (B.sizeBound bp) . B.runB bp
 {-# INLINE primBounded #-}
 
+-- | Use 'B.FixedPrim'
 primFixed :: B.FixedPrim a -> a -> Builder
-primFixed fp a = ensure (B.size fp)
-  $ \(Buffer end ptr) -> Buffer end (ptr `plusPtr` B.size fp) <$ B.runF fp a ptr
+primFixed fp a = withPtr (B.size fp) $ \ptr -> (ptr `plusPtr` B.size fp) <$ B.runF fp a ptr
 {-# INLINE primFixed #-}
 
 primMapListFixed :: B.FixedPrim a -> [a] -> Builder
@@ -225,6 +244,23 @@
 primMapLazyByteStringFixed fp = BL.foldr (mappend . primFixed fp) mempty
 {-# INLINE primMapLazyByteStringFixed #-}
 
+padded :: Word8
+  -> Int -- ^ pad if shorter than this
+  -> B.BoundedPrim a
+  -> a
+  -> Builder
+padded ch size bp a = ensure (B.sizeBound bp) $ \(Buffer end ptr) -> do
+  ptr' <- B.runB bp a ptr
+  let len = ptr' `minusPtr` ptr
+  let pad = size - len
+  when (pad > 0) $ do
+    c_memmove (ptr `plusPtr` pad) ptr len
+    void $ B.memset ptr ch (fromIntegral pad)
+  return $ Buffer end $ ptr' `plusPtr` max pad 0
+
+zeroPadded :: Int -> B.BoundedPrim a -> a -> Builder
+zeroPadded = padded 48
+
 newtype GrowingBuffer = GrowingBuffer (IORef (ForeignPtr Word8))
 
 instance Buildable GrowingBuffer where
@@ -244,6 +280,7 @@
     return $ Buffer (dst' `plusPtr` size') (dst' `plusPtr` pos)
   {-# INLINE allocate #-}
 
+-- | Create a strict 'B.ByteString'
 toStrictByteString :: BuilderFor GrowingBuffer -> B.ByteString
 toStrictByteString b = unsafePerformIO $ do
   fptr0 <- mallocForeignPtrBytes initialSize
@@ -267,7 +304,7 @@
 
 instance Buildable Channel where
   byteString bs
-    | B.length bs < 4096 = byteStringCopy bs
+    | B.length bs < 3272 = byteStringCopy bs
     | otherwise = flush <> Builder (\(Channel v _) b -> b <$ putMVar v bs)
   {-# INLINE byteString #-}
   flush = Builder $ \(Channel v ref) (Buffer end ptr) -> do
@@ -279,19 +316,19 @@
   allocate = allocateConstant chBuffer
   {-# INLINE allocate #-}
 
+-- | Create a lazy 'BL.ByteString'. Threaded runtime is required.
 toLazyByteString :: BuilderFor Channel -> BL.ByteString
 toLazyByteString body = unsafePerformIO $ do
   resp <- newEmptyMVar
 
-  let initialSize = 4096
-  fptr <- mallocForeignPtrBytes initialSize
+  fptr <- mallocForeignPtrBytes defaultBufferSize
   ref <- newIORef fptr
   let ptr = unsafeForeignPtrToPtr fptr
 
   let final (Left e) = throw e
       final (Right _) = putMVar resp B.empty
   _ <- flip forkFinally final $ unBuilder (body <> flush) (Channel resp ref)
-    $ Buffer (ptr `plusPtr` initialSize) ptr
+    $ Buffer (ptr `plusPtr` defaultBufferSize) ptr
 
   let go _ = unsafePerformIO $ do
         bs <- takeMVar resp
@@ -301,6 +338,7 @@
   return $ go ()
 {-# INLINE toLazyByteString #-}
 
+-- | Environemnt for handle output
 data PutBuilderEnv = PBE
   { pbHandle :: !Handle
   , pbBuffer :: !(IORef (ForeignPtr Word8))
@@ -318,7 +356,7 @@
 
 instance Buildable PutBuilderEnv where
   byteString bs
-    | len > 4096 = mappend flush $ Builder $ \(PBE h _ _) buf -> do
+    | len > defaultBufferSize = mappend flush $ Builder $ \(PBE h _ _) buf -> do
       B.hPut h bs
       return buf
     | otherwise = byteStringCopy bs
@@ -337,20 +375,23 @@
   allocate = allocateConstant pbBuffer
   {-# INLINE allocate #-}
 
+defaultBufferSize :: Int
+defaultBufferSize = 2048
+
 -- | Write a 'Builder' into a handle and obtain the number of bytes written.
 -- 'flush' does not imply actual disk operations. Set 'NoBuffering' if you want
 -- it to write the content immediately.
 hPutBuilderLen :: Handle -> BuilderFor PutBuilderEnv -> IO Int
 hPutBuilderLen h b = do
-  let initialSize = 4096
-  fptr <- mallocForeignPtrBytes initialSize
+  fptr <- mallocForeignPtrBytes defaultBufferSize
   ref <- newIORef fptr
   let ptr = unsafeForeignPtrToPtr fptr
   counter <- newIORef 0
-  _ <- unBuilder (b <> flush) (PBE h ref counter) (Buffer (ptr `plusPtr` initialSize) ptr)
+  _ <- unBuilder (b <> flush) (PBE h ref counter) (Buffer (ptr `plusPtr` defaultBufferSize) ptr)
   readIORef counter
 {-# INLINE hPutBuilderLen #-}
 
+-- | Environemnt for socket output
 data SocketEnv = SE
   { seSocket :: !S.Socket
   , seBuffer :: !(IORef (ForeignPtr Word8))
@@ -368,7 +409,7 @@
 
 instance Buildable SocketEnv where
   byteString bs
-    | len > 4096 = mappend flush $ Builder $ \(SE sock _ _) (Buffer end ptr) -> do
+    | len > defaultBufferSize = mappend flush $ Builder $ \(SE sock _ _) (Buffer end ptr) -> do
       S.sendAll sock bs
       return $! Buffer end ptr
     | otherwise = byteStringCopy bs
@@ -388,12 +429,11 @@
 -- | Write a 'Builder' into a handle and obtain the number of bytes written.
 sendBuilder :: S.Socket -> BuilderFor SocketEnv -> IO Int
 sendBuilder sock b = do
-  let initialSize = 4096
-  fptr <- mallocForeignPtrBytes initialSize
+  fptr <- mallocForeignPtrBytes defaultBufferSize
   ref <- newIORef fptr
   let ptr = unsafeForeignPtrToPtr fptr
   counter <- newIORef 0
-  _ <- unBuilder (b <> flush) (SE sock ref counter) (Buffer (ptr `plusPtr` initialSize) ptr)
+  _ <- unBuilder (b <> flush) (SE sock ref counter) (Buffer (ptr `plusPtr` defaultBufferSize) ptr)
   readIORef counter
 {-# INLINE sendBuilder #-}
 
