diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## 0.4.0 (2017-11-22)
+
+* Change `createSessionKey` inputs to be `inbound`, `outbound` rather than
+  `side A`, `side B`. If you were passing as `side A`, `side B` before, it
+  should continue to work, unless you were deliberately triggering an error
+  condition.
+* Add `spake2Exchange`, for much more convenient exchanges.
+
 ## 0.3.0 (2017-11-11)
 
 * Depend on protolude 0.2 minimum
diff --git a/cmd/interop-entrypoint/Main.hs b/cmd/interop-entrypoint/Main.hs
--- a/cmd/interop-entrypoint/Main.hs
+++ b/cmd/interop-entrypoint/Main.hs
@@ -29,14 +29,8 @@
   , SideID(..)
   , makeSymmetricProtocol
   , makeAsymmetricProtocol
-  , createSessionKey
   , makePassword
-  , computeOutboundMessage
-  , generateKeyMaterial
-  , extractElement
-  , startSpake2
-  , elementToMessage
-  , formatError
+  , spake2Exchange
   )
 import Crypto.Spake2.Group (AbelianGroup, Group(..))
 import Crypto.Spake2.Groups (Ed25519(..))
@@ -76,31 +70,22 @@
   -> Handle
   -> IO ()
 runInteropTest protocol password inH outH = do
-  spake2 <- startSpake2 protocol password
-  let outElement = computeOutboundMessage spake2
-  output (elementToMessage protocol outElement)
-  line <- hGetLine inH
-  let inMsg = parseHex (toS line :: ByteString)
-  case inMsg of
-    Left err -> abort (toS err)
-    Right inMsgBytes ->
-      case extractElement protocol inMsgBytes of
-        Left err -> abort $ "Could not handle incoming message (line = " <> show line <> ", msgBytes = " <>  show inMsgBytes <> "): " <> formatError err
-        Right inElement -> do
-          -- TODO: This is wrong, because it doesn't handle A/B properly.
-          let key = generateKeyMaterial spake2 inElement
-          let sessionKey = createSessionKey protocol inElement outElement key password
-          output sessionKey
-
+  sessionKey' <- spake2Exchange protocol password output input
+  case sessionKey' of
+    Left err -> abort $ show err
+    Right sessionKey -> output sessionKey
   where
+    output :: ByteString -> IO ()
     output message = do
       hPutStrLn outH (convertToBase Base16 message :: ByteString)
       hFlush outH
 
-    parseHex line =
-      case convertFromBase Base16 line of
-        Left err -> Left ("Could not decode line (reason: " <> err <> "): " <> show line)
-        Right bytes -> Right bytes
+    input :: IO (Either Text ByteString)
+    input = do
+      line <- hGetLine inH
+      case convertFromBase Base16 (toS line :: ByteString) of
+        Left err -> pure . Left . toS $ "Could not decode line (reason: " <> err <> "): " <> show line
+        Right bytes -> pure (Right bytes)
 
 
 makeProtocolFromSide :: Side -> Protocol Ed25519 SHA256
@@ -114,7 +99,7 @@
     group = Ed25519
     m = arbitraryElement group ("M" :: ByteString)
     n = arbitraryElement group ("N" :: ByteString)
-    s = arbitraryElement group ("S" :: ByteString)
+    s = arbitraryElement group ("symmetric" :: ByteString)
     idA = SideID ""
     idB = SideID ""
     idSymmetric = SideID ""
diff --git a/spake2.cabal b/spake2.cabal
--- a/spake2.cabal
+++ b/spake2.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           spake2
-version:        0.3.0
+version:        0.4.0
 synopsis:       Implementation of the SPAKE2 Password-Authenticated Key Exchange algorithm
 description:    This library implements the SPAKE2 password-authenticated key exchange
                 ("PAKE") algorithm. This allows two parties, who share a weak password, to
@@ -21,6 +21,9 @@
 extra-source-files:
     CHANGELOG.md
 
+data-files:
+    tests/python/spake2_exchange.py
+
 source-repository head
   type: git
   location: https://github.com/jml/haskell-spake2
@@ -71,12 +74,17 @@
   build-depends:
       base >= 4.9 && < 5
     , protolude >= 0.2
+    , aeson
+    , bytestring
     , cryptonite
+    , memory
+    , process
     , QuickCheck
     , spake2
     , tasty
     , tasty-hspec
   other-modules:
       Groups
+      Integration
       Spake2
   default-language: Haskell2010
diff --git a/src/Crypto/Spake2.hs b/src/Crypto/Spake2.hs
--- a/src/Crypto/Spake2.hs
+++ b/src/Crypto/Spake2.hs
@@ -90,6 +90,7 @@
   , Protocol
   , makeAsymmetricProtocol
   , makeSymmetricProtocol
+  , spake2Exchange
   , startSpake2
   , Math.computeOutboundMessage
   , Math.generateKeyMaterial
@@ -153,7 +154,7 @@
         Asymmetric{us=SideB} -> "B"
 
 -- | An error that occurs when interpreting messages from the other side of the exchange.
-data MessageError
+data MessageError e
   = EmptyMessage -- ^ We received an empty bytestring.
   | UnexpectedPrefix Word8 Word8
     -- ^ The bytestring had an unexpected prefix.
@@ -165,13 +166,17 @@
     -- ^ Message could not be decoded to an element of the group.
     -- This can indicate either an error in serialization logic,
     -- or in mathematics.
+  | UnknownError e
+    -- ^ An error arising from the "receive" action in 'spake2Exchange'.
+    -- Since 0.4.0
   deriving (Eq, Show)
 
 -- | Turn a 'MessageError' into human-readable text.
-formatError :: MessageError -> Text
+formatError :: Show e => MessageError e -> Text
 formatError EmptyMessage = "Other side sent us an empty message"
 formatError (UnexpectedPrefix got expected) = "Other side claims to be " <> show (chr (fromIntegral got)) <> ", expected " <> show (chr (fromIntegral expected))
 formatError (BadCrypto err message) = "Could not decode message (" <> show message <> ") to element: " <> show err
+formatError (UnknownError err) = "Error receiving message from other side: " <> show err
 
 -- | Extract an element on the group from an incoming message.
 --
@@ -179,7 +184,7 @@
 -- or the other side does not appear to be the expected other side.
 --
 -- TODO: Need to protect against reflection attack at some point.
-extractElement :: Group group => Protocol group hashAlgorithm -> ByteString -> Either MessageError (Element group)
+extractElement :: Group group => Protocol group hashAlgorithm -> ByteString -> Either (MessageError error) (Element group)
 extractElement protocol message =
   case ByteString.uncons message of
     Nothing -> throwError EmptyMessage
@@ -275,6 +280,59 @@
       , Math.theirBlind = blind theirs
       }
 
+-- | Perform an entire SPAKE2 exchange.
+--
+-- Given a SPAKE2 protocol that has all of the parameters for this exchange,
+-- generate a one-off message from this side and receive a one off message
+-- from the other.
+--
+-- Once we are done, return a key shared between both sides for a single
+-- session.
+--
+-- Note: as per the SPAKE2 definition, the session key is not guaranteed
+-- to actually /work/. If the other side has failed to authenticate, you will
+-- still get a session key. Therefore, you must exchange some other message
+-- that has been encrypted using this key in order to confirm that the session
+-- key is indeed shared.
+--
+-- Note: the "send" and "receive" actions are performed 'concurrently'. If you
+-- have ordering requirements, consider using a 'TVar' or 'MVar' to coordinate,
+-- or implementing your own equivalent of 'spake2Exchange'.
+--
+-- If the message received from the other side cannot be parsed, return a
+-- 'MessageError'.
+--
+-- Since 0.4.0.
+spake2Exchange
+  :: (AbelianGroup group, HashAlgorithm hashAlgorithm)
+  => Protocol group hashAlgorithm
+  -- ^ A 'Protocol' with all the parameters for the exchange. These parameters
+  -- must be shared by both sides. Construct with 'makeAsymmetricProtocol' or
+  -- 'makeSymmetricProtocol'.
+  -> Password
+  -- ^ The password shared between both sides. Construct with 'makePassword'.
+  -> (ByteString -> IO ())
+  -- ^ An action to send a message. The 'ByteString' parameter is this side's
+  -- SPAKE2 element, encoded using the group encoding, prefixed according to
+  -- the parameters in the 'Protocol'.
+  -> IO (Either error ByteString)
+  -- ^ An action to receive a message. The 'ByteString' generated ought to be
+  -- the protocol-prefixed, group-encoded version of the other side's SPAKE2
+  -- element.
+  -> IO (Either (MessageError error) ByteString)
+  -- ^ Either the shared session key or an error indicating we couldn't parse
+  -- the other side's message.
+spake2Exchange protocol password send receive = do
+  exchange <- startSpake2 protocol password
+  let outboundElement = Math.computeOutboundMessage exchange
+  let outboundMessage = elementToMessage protocol outboundElement
+  (_, inboundMessage) <- concurrently (send outboundMessage) receive
+  pure $ do
+    inboundMessage' <- first UnknownError inboundMessage
+    inboundElement <- extractElement protocol inboundMessage'
+    let keyMaterial = Math.generateKeyMaterial exchange inboundElement
+    pure (createSessionKey protocol inboundElement outboundElement keyMaterial password)
+
 -- | Commence a SPAKE2 exchange.
 startSpake2
   :: (MonadRandom randomly, AbelianGroup group)
@@ -291,15 +349,22 @@
 -- \[SK \leftarrow H(A, B, X^{\star}, Y^{\star}, K, pw)\]
 --
 -- Including \(pw\) in the session key is what makes this SPAKE2, not SPAKE1.
+--
+-- __Note__: In spake2 0.3 and earlier, The \(X^{\star}\) and \(Y^{\star}\)
+-- were expected to be from side A and side B respectively. Since spake2 0.4,
+-- they are the outbound and inbound elements respectively. This fixes an
+-- interoperability concern with the Python library, and reduces the burden on
+-- the caller. Apologies for the possibly breaking change to any users of
+-- older versions of spake2.
 createSessionKey
   :: (Group group, HashAlgorithm hashAlgorithm)
   => Protocol group hashAlgorithm  -- ^ The protocol used for this exchange
-  -> Element group  -- ^ The message from side A, \(X^{\star}\), or either side if symmetric
-  -> Element group  -- ^ The message from side B, \(Y^{\star}\), or either side if symmetric
+  -> Element group  -- ^ The outbound message, generated by this, \(X^{\star}\), or either side if symmetric
+  -> Element group  -- ^ The inbound message, generated by the other side, \(Y^{\star}\), or either side if symmetric
   -> Element group  -- ^ The calculated key material, \(K\)
   -> Password  -- ^ The shared secret password
   -> ByteString  -- ^ A session key to use for further communication
-createSessionKey Protocol{group, hashAlgorithm, relation} x y k (Password password) =
+createSessionKey Protocol{group, hashAlgorithm, relation} outbound inbound k (Password password) =
   hashDigest transcript
 
   where
@@ -311,19 +376,24 @@
 
     transcript =
       case relation of
-        Asymmetric{sideA, sideB} -> mconcat [ hashDigest password
-                                            , hashDigest (unSideID (sideID sideA))
-                                            , hashDigest (unSideID (sideID sideB))
-                                            , encodeElement group x
-                                            , encodeElement group y
-                                            , encodeElement group k
-                                            ]
-        Symmetric{bothSides} -> mconcat [ hashDigest password
-                                        , hashDigest (unSideID (sideID bothSides))
-                                        , symmetricElements
-                                        , encodeElement group k
-                                        ]
+        Asymmetric{sideA, sideB, us} ->
+          let (x, y) = case us of
+                         SideA -> (inbound, outbound)
+                         SideB -> (outbound, inbound)
+          in mconcat [ hashDigest password
+                     , hashDigest (unSideID (sideID sideA))
+                     , hashDigest (unSideID (sideID sideB))
+                     , encodeElement group x
+                     , encodeElement group y
+                     , encodeElement group k
+                     ]
+        Symmetric{bothSides} ->
+          mconcat [ hashDigest password
+                  , hashDigest (unSideID (sideID bothSides))
+                  , symmetricElements
+                  , encodeElement group k
+                  ]
 
     symmetricElements =
-      let [ firstMessage, secondMessage ] = sort [ encodeElement group x, encodeElement group y ]
+      let [ firstMessage, secondMessage ] = sort [ encodeElement group inbound, encodeElement group outbound ]
       in firstMessage <> secondMessage
diff --git a/tests/Integration.hs b/tests/Integration.hs
new file mode 100644
--- /dev/null
+++ b/tests/Integration.hs
@@ -0,0 +1,76 @@
+module Integration (tests) where
+
+import Protolude hiding (stdin, stdout)
+
+import Crypto.Hash (SHA256(..))
+import Data.ByteArray.Encoding (convertFromBase, convertToBase, Base(Base16))
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Char8
+import qualified System.IO as IO
+import qualified System.Process as Process
+import Test.Tasty (TestTree)
+import Test.Tasty.Hspec (testSpec, describe, it, shouldBe)
+
+import qualified Crypto.Spake2 as Spake2
+import Crypto.Spake2.Group (Group(arbitraryElement))
+import Crypto.Spake2.Groups (Ed25519(..))
+
+import qualified Paths_spake2
+
+tests :: IO TestTree
+tests = testSpec "Integration" $
+  describe "python-spake2" $ do
+    it "Generates the same SPAKE2 session key (symmetric)" $ do
+      let sideID = "treebeard"
+      let password = "mellon"
+      let protocol = Spake2.makeSymmetricProtocol SHA256 Ed25519 blindS (Spake2.SideID sideID)
+      exchangeWithPython protocol password
+        [ "--side=S"
+        , "--side-id=" <> toS sideID
+        ]
+
+    it "Generates the same SPAKE2 session key (asymmetric, we are side B)" $ do
+      let ourSideID = "alliance"
+      let theirSideID = "horde"
+      let password = "mellon"
+      let protocol = Spake2.makeAsymmetricProtocol SHA256 Ed25519 blindA blindB (Spake2.SideID theirSideID) (Spake2.SideID ourSideID) Spake2.SideB
+      exchangeWithPython protocol password
+        [ "--side=A"
+        , "--side-id=" <> toS theirSideID
+        , "--other-side-id=" <> toS ourSideID
+        ]
+
+    it "Generates the same SPAKE2 session key (asymmetric, we are side A)" $ do
+      let ourSideID = "alliance"
+      let theirSideID = "horde"
+      let password = "mellon"
+      let protocol = Spake2.makeAsymmetricProtocol SHA256 Ed25519 blindA blindB (Spake2.SideID ourSideID) (Spake2.SideID theirSideID) Spake2.SideA
+      exchangeWithPython protocol password
+        [ "--side=B"
+        , "--side-id=" <> toS theirSideID
+        , "--other-side-id=" <> toS ourSideID
+        ]
+
+  where
+    send h x = Char8.hPutStrLn h (convertToBase Base16 x)
+    receive h = convertFromBase Base16 <$> ByteString.hGetLine h
+    blindA = arbitraryElement Ed25519 ("M" :: ByteString)
+    blindB = arbitraryElement Ed25519 ("N" :: ByteString)
+    blindS = arbitraryElement Ed25519 ("symmetric" :: ByteString)
+
+    exchangeWithPython protocol password args = do
+      scriptExe <- Paths_spake2.getDataFileName "tests/python/spake2_exchange.py"
+      let testScript = (Process.proc "python" (scriptExe:("--code=" <> toS password):args))
+                       { Process.std_in = Process.CreatePipe
+                       , Process.std_out = Process.CreatePipe
+                       , Process.std_err = Process.Inherit  -- So we get stack traces printed during test runs.
+                       }
+      Process.withCreateProcess testScript $
+        \(Just stdin) (Just stdout) _stderr ph -> do
+          -- The inter-process protocol is line-based.
+          IO.hSetBuffering stdin IO.LineBuffering
+          IO.hSetBuffering stdout IO.LineBuffering
+          IO.hSetBuffering stderr IO.LineBuffering
+          (do Right sessionKey <- Spake2.spake2Exchange protocol (Spake2.makePassword password) (send stdin) (receive stdout)
+              theirSpakeKey <- ByteString.hGetLine stdout
+              theirSpakeKey `shouldBe` convertToBase Base16 sessionKey) `finally` Process.waitForProcess ph
diff --git a/tests/Spake2.hs b/tests/Spake2.hs
--- a/tests/Spake2.hs
+++ b/tests/Spake2.hs
@@ -14,76 +14,51 @@
   describe "Asymmetric protocol" $ do
     it "Produces matching session keys when passwords match" $ do
       let password = Spake2.makePassword "abc"
-      let hashAlg = SHA256
-      let group = Ed25519
-      let m = Group.arbitraryElement group ("M" :: ByteString)
-      let n = Group.arbitraryElement group ("N" :: ByteString)
-      let idA = Spake2.SideID ""
-      let idB = Spake2.SideID ""
-      let protocolA = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideA
-      let protocolB = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideB
-      sideA <- Spake2.startSpake2 protocolA password
-      sideB <- Spake2.startSpake2 protocolB password
-      let aOut = Spake2.computeOutboundMessage sideA
-      let bOut = Spake2.computeOutboundMessage sideB
-      let aKey = Spake2.generateKeyMaterial sideA bOut
-      let bKey = Spake2.generateKeyMaterial sideB aOut
-      let aSessionKey = Spake2.createSessionKey protocolA aOut bOut aKey password
-      let bSessionKey = Spake2.createSessionKey protocolA aOut bOut bKey password
+      let idA = Spake2.SideID "side-a"
+      let idB = Spake2.SideID "side-b"
+      let protocolA = defaultAsymmetricProtocol idA idB Spake2.SideA
+      let protocolB = defaultAsymmetricProtocol idA idB Spake2.SideB
+      (Right aSessionKey, Right bSessionKey) <- (protocolA, password) `versus` (protocolB, password)
       aSessionKey `shouldBe` bSessionKey
+
     it "Produces differing session keys when passwords do not match" $ do
       let password1 = Spake2.makePassword "abc"
       let password2 = Spake2.makePassword "cba"
-      let hashAlg = SHA256
-      let group = Ed25519
-      let m = Group.arbitraryElement group ("M" :: ByteString)
-      let n = Group.arbitraryElement group ("N" :: ByteString)
       let idA = Spake2.SideID ""
       let idB = Spake2.SideID ""
-      let protocolA = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideA
-      let protocolB = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideB
-      sideA <- Spake2.startSpake2 protocolA password1
-      sideB <- Spake2.startSpake2 protocolB password2
-      let aOut = Spake2.computeOutboundMessage sideA
-      let bOut = Spake2.computeOutboundMessage sideB
-      let aKey = Spake2.generateKeyMaterial sideA bOut
-      let bKey = Spake2.generateKeyMaterial sideB aOut
-      let aSessionKey = Spake2.createSessionKey protocolA aOut bOut aKey password1
-      let bSessionKey = Spake2.createSessionKey protocolA aOut bOut bKey password2
+      let protocolA = defaultAsymmetricProtocol idA idB Spake2.SideA
+      let protocolB = defaultAsymmetricProtocol idA idB Spake2.SideB
+      (Right aSessionKey, Right bSessionKey) <- (protocolA, password1) `versus` (protocolB, password2)
       aSessionKey `shouldNotBe` bSessionKey
+
   describe "Symmetric protocol" $ do
     it "Produces matching session keys when passwords match" $ do
       let password = Spake2.makePassword "abc"
-      let hashAlg = SHA256
-      let group = Ed25519
-      let s = Group.arbitraryElement group ("M" :: ByteString)
-      let idSymmetric = Spake2.SideID ""
-      let protocol1 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
-      let protocol2 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
-      side1 <- Spake2.startSpake2 protocol1 password
-      side2 <- Spake2.startSpake2 protocol2 password
-      let out1 = Spake2.computeOutboundMessage side1
-      let out2 = Spake2.computeOutboundMessage side2
-      let key1 = Spake2.generateKeyMaterial side1 out2
-      let key2 = Spake2.generateKeyMaterial side2 out1
-      let sessionKey1 = Spake2.createSessionKey protocol1 out1 out2 key1 password
-      let sessionKey2 = Spake2.createSessionKey protocol2 out1 out2 key2 password
+      let protocol = defaultSymmetricProtocol (Spake2.SideID "")
+      (Right sessionKey1, Right sessionKey2) <- (protocol, password) `versus` (protocol, password)
       sessionKey1 `shouldBe` sessionKey2
+
     it "Produces differing session keys when passwords do not match" $ do
       let password1 = Spake2.makePassword "abc"
       let password2 = Spake2.makePassword "cba"
-      let hashAlg = SHA256
-      let group = Ed25519
-      let s = Group.arbitraryElement group ("M" :: ByteString)
-      let idSymmetric = Spake2.SideID ""
-      let protocol1 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
-      let protocol2 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
-      side1 <- Spake2.startSpake2 protocol1 password1
-      side2 <- Spake2.startSpake2 protocol2 password2
-      let out1 = Spake2.computeOutboundMessage side1
-      let out2 = Spake2.computeOutboundMessage side2
-      let key1 = Spake2.generateKeyMaterial side1 out2
-      let key2 = Spake2.generateKeyMaterial side2 out1
-      let sessionKey1 = Spake2.createSessionKey protocol1 out1 out2 key1 password1
-      let sessionKey2 = Spake2.createSessionKey protocol2 out1 out2 key2 password2
+      let protocol = defaultSymmetricProtocol (Spake2.SideID "")
+      (Right sessionKey1, Right sessionKey2) <- (protocol, password1) `versus` (protocol, password2)
       sessionKey1 `shouldNotBe` sessionKey2
+
+  where
+    defaultAsymmetricProtocol = Spake2.makeAsymmetricProtocol SHA256 group m n
+    m = Group.arbitraryElement group ("M" :: ByteString)
+    n = Group.arbitraryElement group ("N" :: ByteString)
+
+    defaultSymmetricProtocol = Spake2.makeSymmetricProtocol SHA256 group s
+    s = Group.arbitraryElement group ("symmetric" :: ByteString)
+
+    group = Ed25519
+
+    -- | Run protocol A with password A against protocol B with password B.
+    versus (protocolA, passwordA) (protocolB, passwordB) = do
+      aOutVar <- newEmptyMVar
+      bOutVar <- newEmptyMVar
+      concurrently
+        (Spake2.spake2Exchange protocolA passwordA (putMVar aOutVar) (Right <$> readMVar bOutVar))
+        (Spake2.spake2Exchange protocolB passwordB (putMVar bOutVar) (Right <$> readMVar aOutVar))
diff --git a/tests/Tasty.hs b/tests/Tasty.hs
--- a/tests/Tasty.hs
+++ b/tests/Tasty.hs
@@ -8,6 +8,7 @@
 
 import qualified Spake2
 import qualified Groups
+import qualified Integration
 
 main :: IO ()
 main = sequence tests >>= defaultMain . testGroup "Spake2"
@@ -15,4 +16,5 @@
     tests =
       [ Spake2.tests
       , Groups.tests
+      , Integration.tests
       ]
diff --git a/tests/python/spake2_exchange.py b/tests/python/spake2_exchange.py
new file mode 100644
--- /dev/null
+++ b/tests/python/spake2_exchange.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+"""Exchange SPAKE2 keys and print out the session key.
+
+Assumes symmetric exchange and uses the default SPAKE2 parameters.
+"""
+import argparse
+from binascii import hexlify, unhexlify
+import attr
+import sys
+
+from spake2 import SPAKE2_A, SPAKE2_B, SPAKE2_Symmetric
+
+
+def main():
+    parser = argparse.ArgumentParser(prog='version_exchange')
+    parser.add_argument(
+        '--code', dest='code', type=unicode,
+        help='Password to use to connect to other side')
+    parser.add_argument(
+        '--side-id', dest='side_id', type=unicode,
+        help='Identifier for this side of the exchange')
+    parser.add_argument(
+        '--side', dest='side', choices=['A', 'B', 'S'],
+        help=('Which side this represents. '
+              'Decides whether we use symmetric or asymmetric variant.'))
+    parser.add_argument(
+        '--other-side-id', dest='other_side_id', type=unicode,
+        help=('Identifier for other side of the exchange. '
+              'Only necessary for asymmetric variants.'))
+    params = parser.parse_args(sys.argv[1:])
+    transport = Transport(input_stream=sys.stdin, output_stream=sys.stdout)
+    protocol = get_protocol(
+        params.code, params.side, params.side_id, params.other_side_id)
+    run_exchange(transport, protocol)
+
+
+def get_protocol(code, side, side_id, other_side_id):
+    code = code.encode('utf8')
+    side_id = side_id.encode('utf8')
+    if side == 'S':
+        return SPAKE2_Symmetric(code, idSymmetric=side_id)
+    other_side_id = other_side_id.encode('utf8')
+    if side == 'A':
+        return SPAKE2_A(code, idA=side_id, idB=other_side_id)
+    elif side == 'B':
+        return SPAKE2_B(code, idA=other_side_id, idB=side_id)
+    else:
+        raise AssertionError('Invalid side: %r' % (side,))
+
+
+def run_exchange(transport, protocol):
+    # Send the SPAKE2 message
+    outbound = protocol.start()
+    transport.send_line(hexlify(outbound))
+
+    # Receive SPAKE2 message
+    pake_msg = transport.receive_line()
+    inbound = unhexlify(pake_msg)
+    spake_key = protocol.finish(inbound)
+    transport.send_line(hexlify(spake_key))
+
+
+@attr.s
+class Transport(object):
+    input_stream = attr.ib()
+    output_stream = attr.ib()
+
+    def send_line(self, line):
+        self.output_stream.write(line.rstrip().encode('utf8'))
+        self.output_stream.write('\n')
+        self.output_stream.flush()
+
+    def receive_line(self):
+        return self.input_stream.readline().strip().decode('utf8')
+
+
+if __name__ == '__main__':
+    main()
