diff --git a/Changelog.md b/Changelog.md
new file mode 100644
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+* Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2014, Oliver Charles
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Oliver Charles nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/network-carbon.cabal b/network-carbon.cabal
new file mode 100644
--- /dev/null
+++ b/network-carbon.cabal
@@ -0,0 +1,28 @@
+name: network-carbon
+version: 1.0.0
+synopsis: A Haskell implementation of the Carbon protocol (part of the Graphite monitoring tools)
+homepage: http://github.com/ocharles/network-carbon
+license: BSD3
+license-file: LICENSE
+author: Oliver Charles
+maintainer: ollie@ocharles.org.uk
+build-type: Simple
+cabal-version: >=1.10
+extra-source-files: Changelog.md
+
+library
+  exposed-modules:
+    Network.Carbon.Plaintext
+
+  build-depends:
+    base >=4.6 && <4.8,
+    bytestring >=0.10.2.0 && <0.11,
+    network >= 2.5.0.0 && < 2.6,
+    text >= 1.1.1.3 && < 1.2,
+    time >= 1.4.2 && < 1.5,
+    vector >= 0.10.11.0 && < 0.11
+
+  hs-source-dirs: src
+  default-language: Haskell2010
+
+  ghc-options: -Wall -O2 -fwarn-incomplete-uni-patterns
diff --git a/src/Network/Carbon/Plaintext.hs b/src/Network/Carbon/Plaintext.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Carbon/Plaintext.hs
@@ -0,0 +1,102 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Network.Carbon.Plaintext
+  ( -- * Interacting with Carbon
+    -- ** Connections
+    Connection(..)
+  , connect
+  , disconnect
+
+    -- ** Metrics
+  , sendMetrics
+  , sendMetric
+  , Metric(..)
+
+    -- * Protocol details
+  , encodeMetric
+  )
+  where
+
+import Control.Monad (unless)
+import Data.Monoid ((<>), mempty, mappend)
+import Data.Typeable (Typeable)
+
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.Time as Time
+import qualified Data.Time.Clock.POSIX as Time
+import qualified Data.Vector as V
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Network.Socket as Network
+import qualified Network.Socket.ByteString.Lazy as Network
+
+--------------------------------------------------------------------------------
+-- | Low-level representation of a Carbon connection. It's suggested that you
+-- construct this via 'connect'. It is henceforth assumed that
+-- 'Network.getPeerName' will return something useful, which usually means
+-- 'connectionSocket' should have been 'Network.connect'ed at least once.
+data Connection = Connection
+  { connectionSocket :: !Network.Socket
+    -- ^ The connection socket to Carbon.
+  }
+  deriving (Eq, Show, Typeable)
+
+
+--------------------------------------------------------------------------------
+-- | Connect to Carbon.
+connect :: Network.SockAddr -> IO Connection
+connect sockAddr = fmap Connection $ do
+  s <- Network.socket Network.AF_INET Network.Stream Network.defaultProtocol
+  Network.connect s sockAddr
+  return s
+
+
+--------------------------------------------------------------------------------
+-- | Disconnect from Carbon. Note that it's still valid to 'sendMetrics' to this
+-- 'Connection', and it will result in a reconnection.
+disconnect :: Connection -> IO ()
+disconnect (Connection s) = Network.close s
+
+
+--------------------------------------------------------------------------------
+reconnect :: Connection -> IO ()
+reconnect (Connection s) = do
+  peer <- Network.getPeerName s
+  Network.connect s peer
+
+
+--------------------------------------------------------------------------------
+-- | A single data point. A metric has a path that names it, a value, and the
+-- time the metric was sampled.
+data Metric = Metric
+  { metricPath :: !Text.Text
+  , metricValue :: !Double
+  , metricTimeStamp :: !Time.UTCTime
+  }
+  deriving (Eq, Show, Typeable)
+
+
+--------------------------------------------------------------------------------
+-- | Send a collection of metrics to Carbon.
+sendMetrics :: Connection -> V.Vector Metric -> IO ()
+sendMetrics c ms = do
+  let socket = connectionSocket c
+
+  do isWritable <- Network.isWritable socket
+     unless isWritable (reconnect c)
+
+  Network.sendAll socket (Builder.toLazyByteString (V.foldl' mappend mempty (V.map encodeMetric ms)))
+
+--------------------------------------------------------------------------------
+-- | Send a single metric.
+sendMetric :: Connection -> Text.Text -> Double -> Time.UTCTime -> IO ()
+sendMetric c k v t = sendMetrics c (V.singleton (Metric k v t))
+
+
+--------------------------------------------------------------------------------
+-- | Encode a 'Metric' for transmission over the plain text protocol.
+encodeMetric :: Metric -> Builder.Builder
+encodeMetric (Metric k v t) =
+  Builder.byteString (Text.encodeUtf8 k) <> " " <>
+  Builder.stringUtf8 (show v) <> " " <>
+  Builder.stringUtf8 (show (round (Time.utcTimeToPOSIXSeconds t) :: Int)) <> "\n"
