diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Arbor Networks (c) 2018
+
+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 Alexey Raga 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+# antiope-contract
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/antiope-contract.cabal b/antiope-contract.cabal
new file mode 100644
--- /dev/null
+++ b/antiope-contract.cabal
@@ -0,0 +1,42 @@
+-- This file has been generated from package.yaml by hpack version 0.28.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 5b94c343c39d4a8821f3485ce8b4a174f7dc86d143250408ca9479da455828a3
+
+name:           antiope-contract
+version:        6.0.2
+description:    Please see the README on Github at <https://github.com/arbor/antiope#readme>
+category:       Services
+homepage:       https://github.com/packetloop/antiope-contract#readme
+author:         Arbor Networks
+maintainer:     mayhem@arbor.net
+copyright:      Arbor Networks
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+extra-source-files:
+    contract/file_change_message.avsc
+    contract/resource_changed.avsc
+    README.md
+
+library
+  exposed-modules:
+      Antiope.Contract.SQS.FileChangeMessage
+      Antiope.Contract.SQS.ResourceChanged
+  other-modules:
+      Paths_antiope_contract
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      aeson
+    , antiope-s3
+    , avro
+    , base >=4.7 && <5
+    , bytestring
+    , text
+  if os(osx)
+    cpp-options: -D__attribute__(A)= -D_Nullable= -D_Nonnull=
+  default-language: Haskell2010
diff --git a/contract/file_change_message.avsc b/contract/file_change_message.avsc
new file mode 100644
--- /dev/null
+++ b/contract/file_change_message.avsc
@@ -0,0 +1,13 @@
+{
+  "type": "record",
+  "name": "FileChangeMessage",
+  "namespace": "net.arbor.kafka",
+  "fields": [
+    { "name": "eventName",                          "type": "string" },
+    { "name": "eventTime",                          "type": "string" },
+    { "name": "bucketName",                         "type": "string" },
+    { "name": "objectKey",                          "type": "string" },
+    { "name": "objectSize",                         "type": "long" },
+    { "name": "objectTag",                          "type": "string" }
+  ]
+}
diff --git a/contract/resource_changed.avsc b/contract/resource_changed.avsc
new file mode 100644
--- /dev/null
+++ b/contract/resource_changed.avsc
@@ -0,0 +1,9 @@
+{
+  "type": "record",
+  "name": "ResourceChanged",
+  "namespace": "net.arbor",
+  "fields": [
+    { "name": "eventTime", "type": "string" },
+    { "name": "uri",       "type": "string" }
+  ]
+}
diff --git a/src/Antiope/Contract/SQS/FileChangeMessage.hs b/src/Antiope/Contract/SQS/FileChangeMessage.hs
new file mode 100644
--- /dev/null
+++ b/src/Antiope/Contract/SQS/FileChangeMessage.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE TemplateHaskell            #-}
+
+module Antiope.Contract.SQS.FileChangeMessage where
+
+import Antiope.S3
+import Data.Aeson
+import Data.Avro.Deriving
+import Data.Text.Encoding (encodeUtf8)
+
+import qualified Data.Aeson           as J
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Text            as T
+
+-- automagically creates data types and ToAvro/FromAvro instances
+-- for a given schema.
+flip deriveAvroWithOptions "contract/file_change_message.avsc" $ defaultDeriveOptions
+  { fieldNameBuilder = mkAsIsFieldName
+  }
+
+instance ToJSON FileChangeMessage where
+  toJSON r = object
+    [ "eventName"   .= eventName r
+    , "eventTime"   .= eventTime r
+    , "bucketName"  .= bucketName r
+    , "objectKey"   .= objectKey r
+    , "objectSize"  .= objectSize r
+    , "objectTag"   .= objectTag r
+    ]
+
+instance FromJSON FileChangeMessage where
+  parseJSON = withObject "FileChangeMessage" $ \o ->
+    FileChangeMessage <$> o .: "eventName"
+                      <*> o .: "eventTime"
+                      <*> o .: "bucketName"
+                      <*> o .: "objectKey"
+                      <*> o .: "objectSize"
+                      <*> o .: "objectTag"
+
+toFileChangeMessage :: T.Text -> Maybe FileChangeMessage
+toFileChangeMessage = J.decode . LBS.fromStrict . encodeUtf8
+
+fcmS3Uri :: T.Text -> Maybe S3Uri
+fcmS3Uri body = toFileChangeMessage body >>= \m ->
+  let b = BucketName (bucketName m)
+      k = ObjectKey (objectKey m)
+  in  pure $ S3Uri b k
diff --git a/src/Antiope/Contract/SQS/ResourceChanged.hs b/src/Antiope/Contract/SQS/ResourceChanged.hs
new file mode 100644
--- /dev/null
+++ b/src/Antiope/Contract/SQS/ResourceChanged.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE TemplateHaskell            #-}
+
+module Antiope.Contract.SQS.ResourceChanged where
+
+import Data.Aeson
+import Data.Avro.Deriving
+import Data.Text.Encoding (encodeUtf8)
+
+import qualified Data.Aeson           as J
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Text            as T
+
+-- automagically creates data types and ToAvro/FromAvro instances
+-- for a given schema.
+flip deriveAvroWithOptions "contract/resource_changed.avsc" $ defaultDeriveOptions
+  { fieldNameBuilder = mkAsIsFieldName
+  }
+
+instance ToJSON ResourceChanged where
+  toJSON r = object
+    [ "eventTime" .= eventTime r
+    , "uri"       .= uri r
+    ]
+
+instance FromJSON ResourceChanged where
+  parseJSON = withObject "ResourceChanged" $ \o ->
+    ResourceChanged <$> o .: "eventTime"
+                    <*> o .: "uri"
+
+toResourceChanged :: T.Text -> Maybe ResourceChanged
+toResourceChanged = J.decode . LBS.fromStrict . encodeUtf8
+
+rcmUri :: T.Text -> Maybe T.Text
+rcmUri body = toResourceChanged body >>= \m -> pure $ uri m
