diff --git a/Changelog.md b/Changelog.md
new file mode 100644
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,9 @@
+# Change log for template project
+
+## Version 1.0.0 
+
+
++ Add support n26 import
++ Add support SepaDirectCoreScheme
++ Add a web UI
++ Add CLI parser
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Jappie Klooster
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Readme.md b/Readme.md
new file mode 100644
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,54 @@
+[![https://jappieklooster.nl](https://img.shields.io/badge/blog-jappieklooster.nl-lightgrey)](https://jappieklooster.nl/tag/haskell.html)
+[![Github actions build status](https://img.shields.io/github/workflow/status/jappeace/haskell-template-project/Test)](https://github.com/jappeace/haskell-template-project/actions)
+[![Jappiejappie](https://img.shields.io/badge/discord-jappiejappie-black?logo=discord)](https://discord.gg/Hp4agqy)
+[![Hackage version](https://img.shields.io/hackage/v/template.svg?label=Hackage)](https://hackage.haskell.org/package/template) 
+
+> In lyk man is in ryk man
+
+Dit is een snelstart importeer programma voor n26.
++ Pas SnelstartImport.hs met je account gegevens.
++ Exporteer n26 als csv en zet input.csv
++ nix develop
++ cabal run
++ nu kun je out.csv importeren in snelstart.
+
+Onderwater zet dit de gegevens om van n26 formaat naar
+dat van ING.
+
+Neem contact op met mij als er problemen zijn
+
+
+### Architecture
+
+generally we go:
+
+1. parse raw (potential partial) format such as csv or xml,
+2. then go to typed intermiedatry (eg SepaDirectCoreScheme, or N26).
+3. then go to ING.
+4. write out ING
+
+
+#### xml parsing
+choice appears to be out of
+- xml conduit, which is definetly maintained: https://hackage.haskell.org/package/xml-conduit
+- hexml, looks like it got an upload last year, so that's good https://hackage.haskell.org/package/hexml
+
+### Tools
+Enter the nix shell.
+```
+nix develop
+```
+You can checkout the makefile to see what's available:
+```
+cat makefile
+```
+
+### Running
+```
+make run
+```
+
+### Fast filewatch which runs tests
+```
+make ghcid
+```
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,7 @@
+
+module Main where
+
+import qualified SnelstartImport
+
+main :: IO ()
+main = SnelstartImport.main
diff --git a/snelstart-import.cabal b/snelstart-import.cabal
new file mode 100644
--- /dev/null
+++ b/snelstart-import.cabal
@@ -0,0 +1,122 @@
+cabal-version:      3.0
+
+-- This file has been generated from package.yaml by hpack version 0.34.7.
+--
+-- see: https://github.com/sol/hpack
+
+name:           snelstart-import
+version:        1.0.0
+homepage:       https://github.com/jappeace/snelstart-import#readme
+bug-reports:    https://github.com/jappeace/snelstart-import/issues
+description:    Import to snelstart from various formats such as sepa direct debit or n26 bank format. Converts the format to ING csv. Has a server for an easy UI and a cli for quick conversions.
+synopsis:       Import to snelstart.
+author:         Jappie Klooster
+maintainer:     jappieklooster@hotmail.com
+copyright:      2020 Jappie Klooster
+category:       Finance 
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    Readme.md
+    LICENSE
+extra-doc-files:
+    Changelog.md
+
+source-repository head
+  type: git
+  location: https://github.com/jappeace/snelstart-import
+
+common common-options
+  default-extensions: 
+      EmptyCase
+      FlexibleContexts
+      FlexibleInstances
+      InstanceSigs
+      MultiParamTypeClasses
+      LambdaCase
+      MultiWayIf
+      NamedFieldPuns
+      TupleSections
+      DeriveFoldable
+      DeriveFunctor
+      DeriveGeneric
+      DeriveLift
+      DeriveTraversable
+      DerivingStrategies
+      GeneralizedNewtypeDeriving
+      StandaloneDeriving
+      OverloadedStrings
+      TypeApplications
+      NumericUnderscores
+
+  ghc-options:
+    -O2 -Wall -Wincomplete-uni-patterns
+    -Wincomplete-record-updates -Widentities -Wredundant-constraints
+    -Wcpp-undef -fwarn-tabs -Wpartial-fields
+    -fdefer-diagnostics -Wunused-packages
+    -fenable-th-splice-warnings
+    -fno-omit-yields
+
+  build-depends:
+      base >=4.9.1.0 && <5,
+      cassava < 1,
+      text < 3,
+      time < 2, 
+      bytestring < 1,
+      vector < 1,
+      neat-interpolation < 1,
+      optparse-applicative < 1,
+      yesod-core < 2,
+      blaze-html < 1,
+      yesod-form < 2,
+      hexml < 1,
+      warp < 4, 
+      base64 >= 1 && < 2,
+      shakespeare < 3
+
+  default-language: Haskell2010
+
+library
+  import: common-options
+  exposed-modules:
+      SnelstartImport
+      SnelstartImport.N26
+      SnelstartImport.ING
+      SnelstartImport.Convert
+      SnelstartImport.Currency
+      SnelstartImport.Options
+      SnelstartImport.SepaDirectCoreScheme
+      SnelstartImport.Web.Routes
+      SnelstartImport.Web.Handler
+      SnelstartImport.Web
+      SnelstartImport.Web.Layout
+      SnelstartImport.Web.Message
+  hs-source-dirs:
+      src
+  other-modules:
+      Paths_snelstart_import
+  autogen-modules:
+      Paths_snelstart_import
+
+executable snelstart-import
+  import: common-options
+  main-is: Main.hs
+  hs-source-dirs:
+      app
+  build-depends:
+      snelstart-import
+  ghc-options: -Wno-unused-packages
+
+test-suite unit
+  import: common-options
+  type: exitcode-stdio-1.0
+  main-is: Test.hs
+  ghc-options: -Wno-unused-packages
+  hs-source-dirs:
+      test
+  build-depends:
+      tasty,
+      tasty-hunit,
+      tasty-quickcheck,
+      snelstart-import
diff --git a/src/SnelstartImport.hs b/src/SnelstartImport.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module SnelstartImport
+  ( main,
+  )
+where
+
+import qualified Data.ByteString.Lazy as BS
+import SnelstartImport.ING
+import SnelstartImport.N26
+import Data.Vector(toList)
+import SnelstartImport.Options
+import Paths_snelstart_import (version)
+import           Options.Applicative
+import Text.Printf
+import Data.Version (showVersion)
+import SnelstartImport.Web
+import SnelstartImport.Convert
+
+currentVersion :: String
+currentVersion = showVersion version
+
+readSettings :: IO (ProgramOptions)
+readSettings = customExecParser (prefs showHelpOnError) $ info
+  (helper <*> parseProgram)
+  (fullDesc <> header (printf "Snelstart importer %s" currentVersion) <> progDesc
+    "Converts various banks and programs to something snelstart understands"
+  )
+
+
+main :: IO ()
+main = do
+  putStrLn ""
+  putStrLn "starting snelstart import"
+  putStrLn ""
+
+  settings <- readSettings
+  case settings of
+    Convert cli -> convertCli cli
+    Webserver options -> webMain options
+
+convertCli :: CliOptions -> IO ()
+convertCli options = do
+  result <- readN26 (cliInputFile options)
+  case result of
+    Left x -> error x
+    Right n26Vec -> BS.writeFile (cliOutputFile options) $ let
+        n26 :: [ING]
+        n26 = n26ToING (cliOwnAccount options) <$> toList n26Vec
+      in
+        writeCsv n26
diff --git a/src/SnelstartImport/Convert.hs b/src/SnelstartImport/Convert.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Convert.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | Go from various formats to snelstart (ing)
+module SnelstartImport.Convert
+  ( n26ToING,
+    sepaDirectCoreSchemeToING
+  )
+where
+
+import SnelstartImport.ING
+import SnelstartImport.N26
+import Data.Text(Text)
+import SnelstartImport.SepaDirectCoreScheme (SepaDirectCoreScheme(..))
+import Data.Time
+
+sepaDirectCoreSchemeToING :: Text -> SepaDirectCoreScheme -> ING
+sepaDirectCoreSchemeToING ownAccoun SepaDirectCoreScheme{..} = ING{
+  datum = UTCTime{ utctDay = dtOfSgntr, utctDayTime = 0},
+  naamBescrhijving = dbtr,
+  rekening = ownAccoun,
+  tegenRekening  = dbtrAcct,
+  mutatieSoort = Overschijving, -- TODO how can we figure this out?
+  bijAf = Af, -- TODO looks like it only deducts from the account, is this right?
+  bedragEur = instdAmt ,
+  mededeling = ""
+  }
+
+n26ToING :: Text -> N26 -> ING
+n26ToING ownAccoun N26{..} = ING {
+  datum = unDate date,
+  naamBescrhijving = payee,
+  rekening = ownAccoun,
+  tegenRekening  = accountNumber,
+  mutatieSoort = toType transactionType , -- eg ook voor code
+  bijAf = if amountEur < 0 then Af else Bij,
+  bedragEur = abs amountEur ,
+  mededeling = paymentReference
+  }
+
+toType :: TransactionType -> MutatieSoort
+toType = \case
+  MastercardPayment -> Diversen
+  OutgoingTransfer -> Overschijving
+  Income -> Overschijving
+  N26Referal -> Diversen
+  DirectDebit -> Overschijving
diff --git a/src/SnelstartImport/Currency.hs b/src/SnelstartImport/Currency.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Currency.hs
@@ -0,0 +1,10 @@
+{-# LANGUAGE DeriveAnyClass #-}
+module SnelstartImport.Currency
+  ( Currency(..)
+  )
+where
+
+import Data.Csv
+
+newtype Currency = Currency Double
+  deriving newtype (Ord, Show, Eq, FromField, Num, Fractional)
diff --git a/src/SnelstartImport/ING.hs b/src/SnelstartImport/ING.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/ING.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+-- | We target the ING bank as the output format.
+--   "snelstart" understands this csv format
+module SnelstartImport.ING
+  ( ING(..),
+    toCode,
+    writeCsv,
+    MutatieSoort (..),
+    BijAf(..)
+  )
+where
+
+import SnelstartImport.Currency
+import Data.Text
+import Data.Text.Encoding
+import Data.Csv
+import Data.Time
+import GHC.Generics
+import qualified Data.Vector as Vector
+import NeatInterpolation(text)
+import qualified Data.ByteString.Lazy as LBS
+
+data MutatieSoort = Overschijving | Diversen | Incasso
+  deriving Show
+
+mutatieSoortToFIeld :: MutatieSoort -> Text
+mutatieSoortToFIeld = \case
+  Overschijving -> "Overschrijving"
+  Diversen -> "Diversen"
+  Incasso -> "Incasso"
+
+toCode :: MutatieSoort -> Text
+toCode = \case
+  Overschijving -> "OV"
+  Diversen -> "DV"
+  Incasso -> "IC"
+
+data BijAf = Bij | Af
+  deriving Show
+
+bijAfToField :: BijAf -> Text
+bijAfToField = \case
+  Bij -> "Bij"
+  Af -> "Af"
+
+
+data ING = ING {
+  datum :: UTCTime,
+  naamBescrhijving :: Text,
+  rekening :: Text,
+  tegenRekening :: Text,
+  mutatieSoort ::  MutatieSoort, -- eg ook voor code
+  bijAf :: BijAf,
+  bedragEur :: Currency ,
+  mededeling :: Text
+  }
+  deriving stock (Generic, Show)
+
+instance ToRecord ING where
+  toRecord (ING{..}) =
+    Vector.fromList
+      [ toField $ formatTime defaultTimeLocale "%Y%m%d" datum
+      , toField naamBescrhijving
+      , toField rekening
+      , toField tegenRekening
+      , toField $ toCode mutatieSoort
+      , toField $ bijAfToField bijAf
+      , toField $ replaceDotWithComma <$>  Prelude.show bedragEur
+      , toField $ mutatieSoortToFIeld mutatieSoort
+      , toField mededeling
+      ]
+
+data TransactionType = MastercardPayment | OutgoingTransfer | Income | N26Referal | DirectDebit
+  deriving stock (Show, Eq)
+
+replaceDotWithComma :: Char -> Char
+replaceDotWithComma '.' = ','
+replaceDotWithComma other = other
+
+instance FromField TransactionType where
+  parseField field = case decodeUtf8 field of
+    "MasterCard Payment" -> pure MastercardPayment
+    "Outgoing Transfer" -> pure OutgoingTransfer
+    "Income" -> pure Income
+    "N26 Referral" -> pure N26Referal
+    "Direct Debit" -> pure DirectDebit
+    other -> fail $ "TransactionType unkown" <> unpack other
+
+newtype Date = Date UTCTime
+  deriving newtype (Show, Eq, Read)
+
+instance FromField Date where
+  parseField field =
+    fmap Date $ parseTimeM True defaultTimeLocale "%Y-%m-%d" $ unpack $ decodeUtf8 field
+
+
+writeCsv :: [ING] -> LBS.ByteString
+writeCsv lines' = LBS.fromStrict header' <> data'
+  where
+        data' :: LBS.ByteString
+        data' = encodeWith opts lines'
+        header' = encodeUtf8 $ [text|"Datum","Naam / Omschrijving","Rekening","Tegenrekening","Code","Af Bij","Bedrag (EUR)","Mutatiesoort","Mededelingen"|] <> "\n"
+
+opts :: EncodeOptions
+opts = defaultEncodeOptions { encQuoting = QuoteAll}
diff --git a/src/SnelstartImport/N26.hs b/src/SnelstartImport/N26.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/N26.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE DeriveAnyClass #-}
+-- | this is whatever N26 spits out, it's some custom csv format.
+--   it appears unstable. (has changed once before)
+module SnelstartImport.N26
+  ( readN26,
+    readN26BS,
+    N26 (..),
+    TransactionType(..),
+    Date(..)
+  )
+where
+
+import SnelstartImport.Currency
+import Data.Text
+import Data.Text.Encoding
+import Data.Csv
+import Data.Time
+import GHC.Generics
+import qualified Data.ByteString.Lazy as BS
+import Data.Vector
+
+data MutatieSoort = Overschijving | Diversen | Incasso
+  deriving Show
+
+data BijAf = Bij | Af
+  deriving Show
+
+data TransactionType = MastercardPayment | OutgoingTransfer | Income | N26Referal | DirectDebit
+  deriving stock (Show, Eq)
+
+
+instance FromField TransactionType where
+  parseField field = case decodeUtf8 field of
+    "Presentment" -> pure MastercardPayment
+    "Debit Transfer" -> pure OutgoingTransfer
+    "Income" -> pure Income
+    "Credit Transfer" -> pure Income
+    "Presentment Refund" -> pure Income
+    "Reward" -> pure N26Referal
+    "Direct Debit" -> pure DirectDebit
+    other -> fail $ "TransactionType unkown" <> unpack other
+
+newtype Date = Date { unDate :: UTCTime }
+  deriving newtype (Show, Eq, Read)
+
+instance FromField Date where
+  parseField field =
+    fmap Date $ parseTimeM True defaultTimeLocale "%Y-%m-%d" $ unpack $ decodeUtf8 field
+
+data N26 = N26  {
+  date :: Date, -- booking date
+  valueDate :: Date,
+  payee :: Text, -- partner name
+  accountNumber :: Text, -- partner iban
+  transactionType :: TransactionType,
+  paymentReference :: Text,
+  accountName :: Text,
+  amountEur :: Currency , -- amount (eur)
+  amountForegin :: Maybe Currency , -- original amount
+  typeForeign :: Text, -- original currency
+  exchangeRate :: Text
+  }
+  deriving stock (Generic, Show, Eq)
+  deriving anyclass FromRecord
+
+
+readN26 :: FilePath -> IO (Either String (Vector N26))
+readN26 path = do
+  lines' <- BS.readFile path
+  pure $ readN26BS lines'
+
+
+readN26BS :: BS.ByteString -> Either String (Vector N26)
+readN26BS = decode HasHeader
diff --git a/src/SnelstartImport/Options.hs b/src/SnelstartImport/Options.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Options.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | deal with cli options
+module SnelstartImport.Options
+  ( parseProgram,
+    CliOptions(..),
+    ProgramOptions (..),
+    WebOptions(..)
+  )
+where
+
+import Data.Text
+import           Options.Applicative
+
+data CliOptions = CliOptions {
+    cliOwnAccount :: Text
+  , cliInputFile :: FilePath
+  , cliOutputFile :: FilePath
+  }
+
+data WebOptions = WebOptions {
+  webPort :: Int
+  }
+
+data ProgramOptions = Convert CliOptions
+                    | Webserver WebOptions
+
+parseWebOptions :: Parser WebOptions
+parseWebOptions = do
+  webPort <- (option
+            auto
+            (  long "port"
+            <> help
+                 "port to bind on"
+            <> value 3005
+            )
+          )
+  pure $ WebOptions{..}
+
+parseCli :: Parser CliOptions
+parseCli = do
+  cliInputFile <- option str (short 'i' <> long "input-file" <> metavar "FILE" <> help "The input file" <> value "input.csv" <> showDefault)
+  cliOutputFile <- option str (short 'o' <> long "output-file" <> metavar "FILE" <> help "The output file" <> value "out.csv" <> showDefault)
+  -- default is set for my own use
+  cliOwnAccount <- option str (short 'a' <> long "own-account" <> metavar "ACCOUNT" <> help "The account written into" <> value "DE92100110012623092722" <> showDefault)
+  pure $ CliOptions {..}
+
+parseProgram :: Parser ProgramOptions
+parseProgram =
+  subparser $
+    command "convert" (info (Convert <$> parseCli <**> helper) $ progDesc "Convert from cli to snelstart format")
+    <>
+    command "server" (info (Webserver <$> parseWebOptions <**> helper) $ progDesc "Start a server to give a UI to do conversion")
diff --git a/src/SnelstartImport/SepaDirectCoreScheme.hs b/src/SnelstartImport/SepaDirectCoreScheme.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/SepaDirectCoreScheme.hs
@@ -0,0 +1,86 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | https://www.europeanpaymentscouncil.eu/sites/default/files/kb/file/2022-06/EPC130-08%20SDD%20Core%20C2PSP%20IG%202023%20V1.0.pdf
+--   this is some xml format the accountents asked support for
+module SnelstartImport.SepaDirectCoreScheme
+  ( SepaDirectCoreScheme(..)
+  , readSepaDirectCoreScheme
+  )
+where
+
+import SnelstartImport.Currency
+import Text.XML.Hexml(parse, children, name, Node, inner)
+import Data.Text (Text)
+import Data.ByteString (ByteString)
+import Data.Text.Encoding (decodeUtf8)
+import qualified Data.Text as Text
+import Data.List
+import Data.Bifunctor(first)
+import Text.Read(readMaybe)
+import Data.Time(Day, parseTimeM, defaultTimeLocale)
+
+data SepaDirectCoreScheme = SepaDirectCoreScheme {
+  -- -- | Unambiguous identification of the account of the
+  -- -- creditor to which a credit entry will be posted as a
+  -- -- result of the payment transaction.
+  -- cdtrAcct :: Text,
+  endToEndId :: Text,
+  dbtrAcct :: Text,
+  dbtr :: Text,
+  instdAmt :: Currency,
+  dtOfSgntr :: Day
+  } deriving Show
+
+name_ :: Node -> Text
+name_ = Text.toLower . decodeUtf8 . name
+
+dig :: Text -> Node -> [Node]
+dig tag parent = filter (\x -> name_ x == Text.toLower tag) $ children parent
+
+data SepaParseErrors = ParseXmlError ByteString
+                     | SepaParseIssues SepaIssues
+                     deriving Show
+
+readSepaDirectCoreScheme :: ByteString -> Either SepaParseErrors [SepaDirectCoreScheme]
+readSepaDirectCoreScheme contents = do
+  nodeRes <- first ParseXmlError $ parse contents
+
+  traverse (first SepaParseIssues . parseSepa) (((dig "DrctDbtTxInf")) =<< ((dig "PmtInf") =<< ((dig "CstmrDrctDbtInitn") =<< dig "document" nodeRes)))
+--
+
+data SepaIssues = ExpectedOne [Node] Text
+                | ExpectedNumber Node Text
+                | ExpectedDate Node Text
+                deriving Show
+
+
+assertOne :: Text -> [Node] -> Either SepaIssues Node
+assertOne label nodes =
+  case uncons nodes of
+    Just (x, _) -> Right x
+    Nothing -> Left (ExpectedOne nodes label)
+
+inner_ :: Node -> Text
+inner_ = decodeUtf8 . inner
+
+parseCurrency :: Node -> Either SepaIssues Currency
+parseCurrency node = case readMaybe (Text.unpack (inner_ node)) of
+  Just number -> Right $ Currency number
+  Nothing -> Left (ExpectedNumber node (inner_ node))
+
+parseDay :: Node -> Either SepaIssues Day
+parseDay node = case parseTimeM True defaultTimeLocale "%F" (Text.unpack (inner_ node)) of
+  Nothing -> Left $ ExpectedDate node (inner_ node)
+  Just day -> Right day
+
+parseSepa :: Node -> Either SepaIssues SepaDirectCoreScheme
+parseSepa node = do
+  dbtr <- inner_ <$> assertOne "dbtr" (dig "nm" =<< dig "dbtr" node)
+  dbtrAcct <- inner_ <$> assertOne "dbtracct" (dig "IBAN" =<< dig "Id" =<< dig "DbtrAcct" node)
+  endToEndId <- inner_ <$> assertOne "endToEndId" (dig "EndToEndId" =<< dig "PmtId" node)
+  instdAmt <- parseCurrency =<< assertOne "instdAmt" (dig "instdAmt" node)
+  dtOfSgntr <- parseDay =<< assertOne "dtOfSgntr" (dig "DtOfSgntr" =<< dig "MndtRltdInf" =<< dig "DrctDbtTx" node)
+  Right $ SepaDirectCoreScheme {
+    ..
+    }
diff --git a/src/SnelstartImport/Web.hs b/src/SnelstartImport/Web.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Web.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+
+-- | small web frontend
+module SnelstartImport.Web
+  ( webMain
+  )
+where
+
+import SnelstartImport.Options
+import Yesod.Core.Dispatch(mkYesodDispatch)
+import Yesod.Core(toWaiApp)
+import Network.Wai.Handler.Warp(run)
+import SnelstartImport.Web.Routes
+import SnelstartImport.Web.Handler
+
+mkYesodDispatch "App" resourcesApp
+
+
+webMain :: WebOptions -> IO ()
+webMain options = do
+  putStrLn $ "listening on port " <> show (webPort options)
+  waiApp <- toWaiApp App
+  run (webPort options) waiApp
diff --git a/src/SnelstartImport/Web/Handler.hs b/src/SnelstartImport/Web/Handler.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Web/Handler.hs
@@ -0,0 +1,149 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | handlers
+module SnelstartImport.Web.Handler
+  ( getRootR
+  , postRootR
+  )
+where
+
+import Data.Vector(toList)
+import qualified Data.ByteString.Lazy as LBS
+import SnelstartImport.Convert
+import SnelstartImport.ING
+import SnelstartImport.Web.Routes
+import SnelstartImport.N26
+import Text.Blaze.Html(Html)
+import Yesod.Form
+import Yesod.Core.Widget
+import Yesod.Core.Handler
+import Data.Text(Text, pack)
+import Data.Text.Encoding
+import Data.ByteString.Base64
+import Data.Base64.Types(extractBase64)
+import qualified Data.Text as Text
+import SnelstartImport.SepaDirectCoreScheme(readSepaDirectCoreScheme)
+import SnelstartImport.Web.Layout(layout)
+import Yesod.Core(lucius)
+import SnelstartImport.Web.Message
+import Data.Time
+import Control.Monad.IO.Class(liftIO)
+
+
+type Form a = Html -> MForm Handler (FormResult a, Widget)
+
+
+data InputFileForm = InputFileForm {
+  ifBank :: Text ,
+  ifFileInfo :: FileInfo
+  }
+
+inputFileForm :: Form InputFileForm
+inputFileForm csrf = do
+  (bankRes, bankView) <- mreq textField "own bank account" Nothing
+  (inputRes, inputView) <- mreq fileField "xml file" Nothing
+
+  let view = do
+       toWidget [lucius|
+          form label {
+            width: 100%;
+            display: inline-block;
+          }
+          form input{
+            margin-bottom: 1em;
+          }
+       |]
+       [whamlet|
+        ^{csrf}
+        <div>
+          <label for=#{fvId bankView}>_{MsgOwnBank}
+          ^{fvInput bankView}
+        <div>
+          <label for=#{fvId bankView}>_{MsgXmlFile}
+          ^{fvInput inputView}
+        <div>
+          <button type=submit >_{MsgConvert}
+  |]
+  pure $ (InputFileForm <$> bankRes <*> inputRes, view)
+
+getRootR :: Handler Html
+getRootR = do
+  ((_res, form), enctype) <- runFormPost inputFileForm
+  layout $ inputForm [] enctype form
+
+inputForm :: [Text] -> Enctype -> Widget -> Widget
+inputForm issues enctype form =
+  let
+    issuesWidget = if Prelude.null issues then "" else
+      [whamlet|
+        <ul>
+          $forall issue <- issues
+            <li> #{issue}
+      |]
+  in
+
+  [whamlet|
+<h1>_{MsgTitle}
+^{issuesWidget}
+<form method=post enctype=#{enctype}>
+    ^{form}
+|]
+
+postRootR :: Handler Html
+postRootR = do
+  ((res, form), enctype) <- runFormPost inputFileForm
+
+  case res of
+    FormMissing     -> layout $ inputForm ["error - missing data"] enctype form
+    FormFailure x   -> layout $ inputForm x enctype form
+    FormSuccess formRes -> do
+      contents <- fileSourceByteString $ ifFileInfo formRes
+      let filename = fileName $ ifFileInfo formRes
+      if Text.isSuffixOf "xml" filename then
+        case readSepaDirectCoreScheme contents of
+          Left err -> layout $ inputForm [pack $ show err] enctype form
+          Right res' -> renderDownload formRes (sepaDirectCoreSchemeToING (ifBank formRes) <$> res')
+      else case readN26BS $ LBS.fromStrict contents of
+        Left err -> layout $ inputForm [pack err] enctype form
+        Right n26 -> renderDownload formRes (n26ToING (ifBank formRes) <$> toList n26)
+
+renderDownload :: InputFileForm -> [ING] -> Handler Html
+renderDownload form ings =
+          let
+            csvOut  = (writeCsv ings)
+            contentText = decodeUtf8 $ LBS.toStrict csvOut
+            downloadText = "data:text/plain;base64," <> (extractBase64 $ encodeBase64 $ LBS.toStrict csvOut)
+          in
+          layout $ do
+            curTime <- liftIO getCurrentTime
+            let
+                timeStr :: String
+                timeStr = formatTime defaultTimeLocale "%F_%H-%M" curTime
+                downloadFileName = "snelstart-import-" <> timeStr <> ".csv"
+            toWidget [lucius|
+                pre {
+                  overflow: scroll;
+                  width: 100%;
+                  position: absolute;
+                  left: 0;
+                  background-color: lightgray;
+                  padding: 1em;
+                }
+            |]
+            [whamlet|
+                <table>
+                  <tr>
+                    <th>_{MsgBank}
+                    <td>#{ifBank form }
+                  <tr>
+                    <th>_{MsgFilename}
+                    <td>#{fileName $ ifFileInfo form }
+
+                <h2>_{MsgContents}
+                <a href=#{downloadText} download=#{downloadFileName}>_{MsgDownload}
+                <pre>
+                  <code>
+                    #{contentText}
+                |]
diff --git a/src/SnelstartImport/Web/Layout.hs b/src/SnelstartImport/Web/Layout.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Web/Layout.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | layout, deal with styling
+module SnelstartImport.Web.Layout
+  ( layout
+  )
+where
+
+import SnelstartImport.Web.Routes
+import Text.Blaze.Html(Html)
+import Yesod.Core.Widget
+import Yesod.Core(defaultLayout, lucius)
+
+layout :: Widget -> Handler Html
+layout widget = do
+  defaultLayout $ do
+    toWidget [lucius|
+      html {
+        text-align: center;
+      }
+      body {
+        text-align: left;
+        margin: 0 auto;
+        width: 50%;
+        max-width: 25em;
+        padding: 1em;
+      }
+      |]
+    widget
diff --git a/src/SnelstartImport/Web/Message.hs b/src/SnelstartImport/Web/Message.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Web/Message.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | translations
+module SnelstartImport.Web.Message where
+
+
+import Text.Shakespeare.I18N
+import SnelstartImport.Web.Routes
+
+mkMessage "App" "i18n" "en"
diff --git a/src/SnelstartImport/Web/Routes.hs b/src/SnelstartImport/Web/Routes.hs
new file mode 100644
--- /dev/null
+++ b/src/SnelstartImport/Web/Routes.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+-- | routes for the app, seperated due to stage restriction
+module SnelstartImport.Web.Routes
+  (
+    resourcesApp,
+    App(..),
+    Handler,
+    Route(..),
+    Widget
+  )
+where
+
+import Yesod.Form.Fields(FormMessage(..), defaultFormMessage)
+import Yesod.Core
+
+data App = App
+
+instance RenderMessage App FormMessage where
+  renderMessage _ _ = defaultFormMessage
+
+instance RenderRoute App => Yesod App
+
+mkYesodData "App" [parseRoutes|
+/ RootR GET POST
+|]
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -0,0 +1,77 @@
+module Main where
+
+import qualified Data.Vector as Vector
+import SnelstartImport.N26
+import Test.Tasty
+import Test.Tasty.HUnit
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests" [unitTests]
+
+unitTests :: TestTree
+unitTests =
+    testGroup
+        "Unit tests"
+        [ testCase "List comparison (different length)" $ do
+            result <- readN26 "n26-v2.csv"
+            result
+                @?= Right
+                    ( Vector.fromList
+                        [ N26
+                            { date = read "2024-07-10 00:00:00 UTC"
+                            , valueDate = read "2024-07-10 00:00:00 UTC"
+                            , payee = "MY COMPUTER"
+                            , accountNumber = ""
+                            , transactionType = MastercardPayment
+                            , paymentReference = ""
+                            , accountName = "Main Account"
+                            , amountEur = -206.27
+                            , amountForegin = Just 222.85
+                            , typeForeign = "USD"
+                            , exchangeRate = "0.9256001795"
+                            }
+                        , N26
+                            { date = read "2024-07-15 00:00:00 UTC"
+                            , valueDate = read "2024-07-15 00:00:00 UTC"
+                            , payee = "JT Klooster"
+                            , accountNumber = "NL74INGB0798449241"
+                            , transactionType = OutgoingTransfer
+                            , paymentReference = ""
+                            , accountName = "Main Account"
+                            , amountEur = -800.0
+                            , amountForegin = Nothing
+                            , typeForeign = ""
+                            , exchangeRate = ""
+                            }
+                        , N26
+                            { date = read "2024-07-17 00:00:00 UTC"
+                            , valueDate = read "2024-07-17 00:00:00 UTC"
+                            , payee = "Oeverture belasting advies b.v."
+                            , accountNumber = "NL90RABO0136021050"
+                            , transactionType = OutgoingTransfer
+                            , paymentReference = ""
+                            , accountName = "Main Account"
+                            , amountEur = -1185.8
+                            , amountForegin = Nothing
+                            , typeForeign = ""
+                            , exchangeRate = ""
+                            }
+                        , N26
+                            { date = read "2024-07-17 00:00:00 UTC"
+                            , valueDate = read "2024-07-17 00:00:00 UTC"
+                            , payee = "Administratiekantoor De Wilde via Stichting Mollie Payments"
+                            , accountNumber = "NL51DEUT0265262461"
+                            , transactionType = OutgoingTransfer
+                            , paymentReference = "c37f91dbcae0a3e00b91496426f3c954 8152518244016285 20240754 Administratiekantoor De Wilde"
+                            , accountName = "Main Account"
+                            , amountEur = -656.43
+                            , amountForegin = Nothing
+                            , typeForeign = ""
+                            , exchangeRate = ""
+                            }
+                        ]
+                    )
+        ]
